blob: 4baee778831d229386738d1a42d1a3b2b5ebef88 [file] [log] [blame]
[email protected]725f1c32011-04-01 20:24:541#!/usr/bin/env python
[email protected]3bbf2942012-01-10 16:52:062# Copyright (c) 2012 The Chromium Authors. All rights reserved.
[email protected]fb2b8eb2009-04-23 21:03:423# 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
[email protected]f7d31f52014-01-03 20:14:469__version__ = '1.8.0'
[email protected]fb2b8eb2009-04-23 21:03:4210
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
[email protected]e72c5f52013-04-16 00:36:4015import cpplint
[email protected]fb2b8eb2009-04-23 21:03:4216import cPickle # Exposed through the API.
17import cStringIO # Exposed through the API.
[email protected]8a4a2bc2013-03-08 08:13:2018import contextlib
[email protected]fb2b8eb2009-04-23 21:03:4219import fnmatch
20import glob
[email protected]15169952011-09-27 14:30:5321import inspect
[email protected]58a69cb2014-03-01 02:08:2922import itertools
[email protected]4f6852c2012-04-20 20:39:2023import json # Exposed through the API.
[email protected]df1595a2009-06-11 02:00:1324import logging
[email protected]fb2b8eb2009-04-23 21:03:4225import marshal # Exposed through the API.
[email protected]bc117312013-04-20 03:57:5626import multiprocessing
[email protected]fb2b8eb2009-04-23 21:03:4227import optparse
28import os # Somewhat exposed through the API.
29import pickle # Exposed through the API.
[email protected]ce8e46b2009-06-26 22:31:5130import random
[email protected]fb2b8eb2009-04-23 21:03:4231import re # Exposed through the API.
[email protected]fb2b8eb2009-04-23 21:03:4232import sys # Parts exposed through API.
33import tempfile # Exposed through the API.
[email protected]2a891dc2009-08-20 20:33:3734import time
[email protected]d7dccf52009-06-06 18:51:5835import traceback # Exposed through the API.
[email protected]fb2b8eb2009-04-23 21:03:4236import types
[email protected]1487d532009-06-06 00:22:5737import unittest # Exposed through the API.
[email protected]fb2b8eb2009-04-23 21:03:4238import urllib2 # Exposed through the API.
[email protected]cb2985f2010-11-03 14:08:3139from warnings import warn
[email protected]fb2b8eb2009-04-23 21:03:4240
41# Local imports.
[email protected]cf6a5d22015-04-09 22:02:0042import auth
[email protected]35625c72011-03-23 17:34:0243import fix_encoding
[email protected]5aeb7dd2009-11-17 18:09:0144import gclient_utils
[email protected]2a009622011-03-01 02:43:3145import owners
[email protected]fb2b8eb2009-04-23 21:03:4246import presubmit_canned_checks
[email protected]239f4112011-06-03 20:08:2347import rietveld
[email protected]5aeb7dd2009-11-17 18:09:0148import scm
[email protected]84f4fe32011-04-06 13:26:4549import subprocess2 as subprocess # Exposed through the API.
[email protected]fb2b8eb2009-04-23 21:03:4250
51
[email protected]ce8e46b2009-06-26 22:31:5152# Ask for feedback only once in program lifetime.
53_ASKED_FOR_FEEDBACK = False
54
55
[email protected]899e1c12011-04-07 17:03:1856class PresubmitFailure(Exception):
[email protected]fb2b8eb2009-04-23 21:03:4257 pass
58
59
[email protected]ffeb2f32013-12-03 13:55:2260class CommandData(object):
61 def __init__(self, name, cmd, kwargs, message):
62 self.name = name
63 self.cmd = cmd
64 self.kwargs = kwargs
65 self.message = message
66 self.info = None
67
[email protected]bc117312013-04-20 03:57:5668
[email protected]fb2b8eb2009-04-23 21:03:4269def normpath(path):
70 '''Version of os.path.normpath that also changes backward slashes to
71 forward slashes when not running on Windows.
72 '''
73 # This is safe to always do because the Windows version of os.path.normpath
74 # will replace forward slashes with backward slashes.
75 path = path.replace(os.sep, '/')
76 return os.path.normpath(path)
77
[email protected]cb2985f2010-11-03 14:08:3178
[email protected]cb2985f2010-11-03 14:08:3179def _RightHandSideLinesImpl(affected_files):
80 """Implements RightHandSideLines for InputApi and GclChange."""
81 for af in affected_files:
[email protected]ab05d582011-02-09 23:41:2282 lines = af.ChangedContents()
[email protected]cb2985f2010-11-03 14:08:3183 for line in lines:
[email protected]ab05d582011-02-09 23:41:2284 yield (af, line[0], line[1])
[email protected]cb2985f2010-11-03 14:08:3185
86
[email protected]5ac21012011-03-16 02:58:2587class PresubmitOutput(object):
88 def __init__(self, input_stream=None, output_stream=None):
89 self.input_stream = input_stream
90 self.output_stream = output_stream
91 self.reviewers = []
92 self.written_output = []
93 self.error_count = 0
94
95 def prompt_yes_no(self, prompt_string):
96 self.write(prompt_string)
97 if self.input_stream:
98 response = self.input_stream.readline().strip().lower()
99 if response not in ('y', 'yes'):
100 self.fail()
101 else:
102 self.fail()
103
104 def fail(self):
105 self.error_count += 1
106
107 def should_continue(self):
108 return not self.error_count
109
110 def write(self, s):
111 self.written_output.append(s)
112 if self.output_stream:
113 self.output_stream.write(s)
114
115 def getvalue(self):
116 return ''.join(self.written_output)
117
118
[email protected]bc117312013-04-20 03:57:56119# Top level object so multiprocessing can pickle
120# Public access through OutputApi object.
121class _PresubmitResult(object):
122 """Base class for result objects."""
123 fatal = False
124 should_prompt = False
125
126 def __init__(self, message, items=None, long_text=''):
127 """
128 message: A short one-line message to indicate errors.
129 items: A list of short strings to indicate where errors occurred.
130 long_text: multi-line text output, e.g. from another tool
131 """
132 self._message = message
133 self._items = items or []
134 if items:
135 self._items = items
136 self._long_text = long_text.rstrip()
137
138 def handle(self, output):
139 output.write(self._message)
140 output.write('\n')
141 for index, item in enumerate(self._items):
142 output.write(' ')
143 # Write separately in case it's unicode.
144 output.write(str(item))
145 if index < len(self._items) - 1:
146 output.write(' \\')
147 output.write('\n')
148 if self._long_text:
149 output.write('\n***************\n')
150 # Write separately in case it's unicode.
151 output.write(self._long_text)
152 output.write('\n***************\n')
153 if self.fatal:
154 output.fail()
155
156
157# Top level object so multiprocessing can pickle
158# Public access through OutputApi object.
159class _PresubmitAddReviewers(_PresubmitResult):
160 """Add some suggested reviewers to the change."""
161 def __init__(self, reviewers):
162 super(_PresubmitAddReviewers, self).__init__('')
163 self.reviewers = reviewers
164
165 def handle(self, output):
166 output.reviewers.extend(self.reviewers)
167
168
169# Top level object so multiprocessing can pickle
170# Public access through OutputApi object.
171class _PresubmitError(_PresubmitResult):
172 """A hard presubmit error."""
173 fatal = True
174
175
176# Top level object so multiprocessing can pickle
177# Public access through OutputApi object.
178class _PresubmitPromptWarning(_PresubmitResult):
179 """An warning that prompts the user if they want to continue."""
180 should_prompt = True
181
182
183# Top level object so multiprocessing can pickle
184# Public access through OutputApi object.
185class _PresubmitNotifyResult(_PresubmitResult):
186 """Just print something to the screen -- but it's not even a warning."""
187 pass
188
189
190# Top level object so multiprocessing can pickle
191# Public access through OutputApi object.
192class _MailTextResult(_PresubmitResult):
193 """A warning that should be included in the review request email."""
194 def __init__(self, *args, **kwargs):
195 super(_MailTextResult, self).__init__()
196 raise NotImplementedError()
197
198
[email protected]fb2b8eb2009-04-23 21:03:42199class OutputApi(object):
[email protected]a6d011e2013-03-26 17:31:49200 """An instance of OutputApi gets passed to presubmit scripts so that they
201 can output various types of results.
[email protected]fb2b8eb2009-04-23 21:03:42202 """
[email protected]bc117312013-04-20 03:57:56203 PresubmitResult = _PresubmitResult
204 PresubmitAddReviewers = _PresubmitAddReviewers
205 PresubmitError = _PresubmitError
206 PresubmitPromptWarning = _PresubmitPromptWarning
207 PresubmitNotifyResult = _PresubmitNotifyResult
208 MailTextResult = _MailTextResult
209
[email protected]a6d011e2013-03-26 17:31:49210 def __init__(self, is_committing):
211 self.is_committing = is_committing
212
[email protected]a6d011e2013-03-26 17:31:49213 def PresubmitPromptOrNotify(self, *args, **kwargs):
214 """Warn the user when uploading, but only notify if committing."""
215 if self.is_committing:
216 return self.PresubmitNotifyResult(*args, **kwargs)
217 return self.PresubmitPromptWarning(*args, **kwargs)
218
[email protected]fb2b8eb2009-04-23 21:03:42219
220class InputApi(object):
221 """An instance of this object is passed to presubmit scripts so they can
222 know stuff about the change they're looking at.
223 """
[email protected]b17b55b2010-11-03 14:42:37224 # Method could be a function
225 # pylint: disable=R0201
[email protected]fb2b8eb2009-04-23 21:03:42226
[email protected]3410d912009-06-09 20:56:16227 # File extensions that are considered source files from a style guide
228 # perspective. Don't modify this list from a presubmit script!
[email protected]c33455a2011-06-24 19:14:18229 #
230 # Files without an extension aren't included in the list. If you want to
231 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
232 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
[email protected]3410d912009-06-09 20:56:16233 DEFAULT_WHITE_LIST = (
234 # C++ and friends
[email protected]fe1211a2011-05-28 18:54:17235 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
236 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
[email protected]3410d912009-06-09 20:56:16237 # Scripts
[email protected]fe1211a2011-05-28 18:54:17238 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
[email protected]3410d912009-06-09 20:56:16239 # Other
[email protected]ae7af922012-01-27 14:51:13240 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
[email protected]3410d912009-06-09 20:56:16241 )
242
243 # Path regexp that should be excluded from being considered containing source
244 # files. Don't modify this list from a presubmit script!
245 DEFAULT_BLACK_LIST = (
[email protected]656326d2012-08-13 00:43:57246 r"testing_support[\\\/]google_appengine[\\\/].*",
[email protected]3410d912009-06-09 20:56:16247 r".*\bexperimental[\\\/].*",
248 r".*\bthird_party[\\\/].*",
249 # Output directories (just in case)
250 r".*\bDebug[\\\/].*",
251 r".*\bRelease[\\\/].*",
252 r".*\bxcodebuild[\\\/].*",
[email protected]c1c96352013-10-09 19:50:27253 r".*\bout[\\\/].*",
[email protected]3410d912009-06-09 20:56:16254 # All caps files like README and LICENCE.
[email protected]ab05d582011-02-09 23:41:22255 r".*\b[A-Z0-9_]{2,}$",
[email protected]df1595a2009-06-11 02:00:13256 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
[email protected]5d0dc432011-01-03 02:40:37257 r"(|.*[\\\/])\.git[\\\/].*",
258 r"(|.*[\\\/])\.svn[\\\/].*",
[email protected]7ccb4bb2011-11-07 20:26:20259 # There is no point in processing a patch file.
260 r".+\.diff$",
261 r".+\.patch$",
[email protected]3410d912009-06-09 20:56:16262 )
263
[email protected]cc73ad62011-07-06 17:39:26264 def __init__(self, change, presubmit_path, is_committing,
[email protected]239f4112011-06-03 20:08:23265 rietveld_obj, verbose):
[email protected]fb2b8eb2009-04-23 21:03:42266 """Builds an InputApi object.
267
268 Args:
[email protected]4ff922a2009-06-12 20:20:19269 change: A presubmit.Change object.
[email protected]fb2b8eb2009-04-23 21:03:42270 presubmit_path: The path to the presubmit script being processed.
[email protected]d7dccf52009-06-06 18:51:58271 is_committing: True if the change is about to be committed.
[email protected]239f4112011-06-03 20:08:23272 rietveld_obj: rietveld.Rietveld client object
[email protected]fb2b8eb2009-04-23 21:03:42273 """
[email protected]9711bba2009-05-22 23:51:39274 # Version number of the presubmit_support script.
275 self.version = [int(x) for x in __version__.split('.')]
[email protected]fb2b8eb2009-04-23 21:03:42276 self.change = change
[email protected]d7dccf52009-06-06 18:51:58277 self.is_committing = is_committing
[email protected]239f4112011-06-03 20:08:23278 self.rietveld = rietveld_obj
[email protected]cab38e92011-04-09 00:30:51279 # TBD
280 self.host_url = 'http://codereview.chromium.org'
281 if self.rietveld:
[email protected]239f4112011-06-03 20:08:23282 self.host_url = self.rietveld.url
[email protected]fb2b8eb2009-04-23 21:03:42283
284 # We expose various modules and functions as attributes of the input_api
285 # so that presubmit scripts don't have to import them.
286 self.basename = os.path.basename
287 self.cPickle = cPickle
[email protected]e72c5f52013-04-16 00:36:40288 self.cpplint = cpplint
[email protected]fb2b8eb2009-04-23 21:03:42289 self.cStringIO = cStringIO
[email protected]17cc2442012-10-17 21:12:09290 self.glob = glob.glob
[email protected]fb11c7b2010-03-18 18:22:14291 self.json = json
[email protected]6fba34d2011-06-02 13:45:12292 self.logging = logging.getLogger('PRESUBMIT')
[email protected]2b5ce562011-03-31 16:15:44293 self.os_listdir = os.listdir
294 self.os_walk = os.walk
[email protected]fb2b8eb2009-04-23 21:03:42295 self.os_path = os.path
[email protected]bd0cace2014-10-02 23:23:46296 self.os_stat = os.stat
[email protected]fb2b8eb2009-04-23 21:03:42297 self.pickle = pickle
298 self.marshal = marshal
299 self.re = re
300 self.subprocess = subprocess
301 self.tempfile = tempfile
[email protected]0d1bdea2011-03-24 22:54:38302 self.time = time
[email protected]d7dccf52009-06-06 18:51:58303 self.traceback = traceback
[email protected]1487d532009-06-06 00:22:57304 self.unittest = unittest
[email protected]fb2b8eb2009-04-23 21:03:42305 self.urllib2 = urllib2
306
[email protected]c0b22972009-06-25 16:19:14307 # To easily fork python.
308 self.python_executable = sys.executable
309 self.environ = os.environ
310
[email protected]fb2b8eb2009-04-23 21:03:42311 # InputApi.platform is the platform you're currently running on.
312 self.platform = sys.platform
313
[email protected]0af3bb32015-06-12 20:44:35314 self.cpu_count = multiprocessing.cpu_count()
315
[email protected]fb2b8eb2009-04-23 21:03:42316 # The local path of the currently-being-processed presubmit script.
[email protected]3d235242009-05-15 12:40:48317 self._current_presubmit_path = os.path.dirname(presubmit_path)
[email protected]fb2b8eb2009-04-23 21:03:42318
319 # We carry the canned checks so presubmit scripts can easily use them.
320 self.canned_checks = presubmit_canned_checks
321
[email protected]2a009622011-03-01 02:43:31322 # TODO(dpranke): figure out a list of all approved owners for a repo
323 # in order to be able to handle wildcard OWNERS files?
324 self.owners_db = owners.Database(change.RepositoryRoot(),
[email protected]17cc2442012-10-17 21:12:09325 fopen=file, os_path=self.os_path, glob=self.glob)
[email protected]899e1c12011-04-07 17:03:18326 self.verbose = verbose
[email protected]bc117312013-04-20 03:57:56327 self.Command = CommandData
[email protected]2a009622011-03-01 02:43:31328
[email protected]e72c5f52013-04-16 00:36:40329 # Replace <hash_map> and <hash_set> as headers that need to be included
[email protected]18278522013-06-11 22:42:32330 # with "base/containers/hash_tables.h" instead.
[email protected]e72c5f52013-04-16 00:36:40331 # Access to a protected member _XX of a client class
332 # pylint: disable=W0212
333 self.cpplint._re_pattern_templates = [
[email protected]18278522013-06-11 22:42:32334 (a, b, 'base/containers/hash_tables.h')
[email protected]e72c5f52013-04-16 00:36:40335 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
336 for (a, b, header) in cpplint._re_pattern_templates
337 ]
338
[email protected]fb2b8eb2009-04-23 21:03:42339 def PresubmitLocalPath(self):
340 """Returns the local path of the presubmit script currently being run.
341
342 This is useful if you don't want to hard-code absolute paths in the
343 presubmit script. For example, It can be used to find another file
344 relative to the PRESUBMIT.py script, so the whole tree can be branched and
345 the presubmit script still works, without editing its content.
346 """
[email protected]3d235242009-05-15 12:40:48347 return self._current_presubmit_path
[email protected]fb2b8eb2009-04-23 21:03:42348
[email protected]1e08c002009-05-28 19:09:33349 def DepotToLocalPath(self, depot_path):
[email protected]fb2b8eb2009-04-23 21:03:42350 """Translate a depot path to a local path (relative to client root).
351
352 Args:
353 Depot path as a string.
354
355 Returns:
356 The local path of the depot path under the user's current client, or None
357 if the file is not mapped.
358
359 Remember to check for the None case and show an appropriate error!
360 """
[email protected]d579fcf2011-12-13 20:36:03361 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
362 ).get('Path')
[email protected]fb2b8eb2009-04-23 21:03:42363
[email protected]1e08c002009-05-28 19:09:33364 def LocalToDepotPath(self, local_path):
[email protected]fb2b8eb2009-04-23 21:03:42365 """Translate a local path to a depot path.
366
367 Args:
368 Local path (relative to current directory, or absolute) as a string.
369
370 Returns:
371 The depot path (SVN URL) of the file if mapped, otherwise None.
372 """
[email protected]d579fcf2011-12-13 20:36:03373 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
374 ).get('URL')
[email protected]fb2b8eb2009-04-23 21:03:42375
[email protected]5538e022011-05-12 17:53:16376 def AffectedFiles(self, include_dirs=False, include_deletes=True,
377 file_filter=None):
[email protected]fb2b8eb2009-04-23 21:03:42378 """Same as input_api.change.AffectedFiles() except only lists files
379 (and optionally directories) in the same directory as the current presubmit
380 script, or subdirectories thereof.
381 """
[email protected]3d235242009-05-15 12:40:48382 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
[email protected]fb2b8eb2009-04-23 21:03:42383 if len(dir_with_slash) == 1:
384 dir_with_slash = ''
[email protected]5538e022011-05-12 17:53:16385
[email protected]4661e0c2009-06-04 00:45:26386 return filter(
387 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
[email protected]5538e022011-05-12 17:53:16388 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
[email protected]fb2b8eb2009-04-23 21:03:42389
390 def LocalPaths(self, include_dirs=False):
391 """Returns local paths of input_api.AffectedFiles()."""
[email protected]2f64f782014-04-25 00:12:33392 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
393 logging.debug("LocalPaths: %s", paths)
394 return paths
[email protected]fb2b8eb2009-04-23 21:03:42395
396 def AbsoluteLocalPaths(self, include_dirs=False):
397 """Returns absolute local paths of input_api.AffectedFiles()."""
398 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
399
400 def ServerPaths(self, include_dirs=False):
401 """Returns server paths of input_api.AffectedFiles()."""
402 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
403
[email protected]77c4f0f2009-05-29 18:53:04404 def AffectedTextFiles(self, include_deletes=None):
[email protected]fb2b8eb2009-04-23 21:03:42405 """Same as input_api.change.AffectedTextFiles() except only lists files
406 in the same directory as the current presubmit script, or subdirectories
407 thereof.
[email protected]fb2b8eb2009-04-23 21:03:42408 """
[email protected]77c4f0f2009-05-29 18:53:04409 if include_deletes is not None:
[email protected]cb2985f2010-11-03 14:08:31410 warn("AffectedTextFiles(include_deletes=%s)"
411 " is deprecated and ignored" % str(include_deletes),
412 category=DeprecationWarning,
413 stacklevel=2)
[email protected]77c4f0f2009-05-29 18:53:04414 return filter(lambda x: x.IsTextFile(),
415 self.AffectedFiles(include_dirs=False, include_deletes=False))
[email protected]fb2b8eb2009-04-23 21:03:42416
[email protected]3410d912009-06-09 20:56:16417 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
418 """Filters out files that aren't considered "source file".
419
420 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
421 and InputApi.DEFAULT_BLACK_LIST is used respectively.
422
423 The lists will be compiled as regular expression and
424 AffectedFile.LocalPath() needs to pass both list.
425
426 Note: Copy-paste this function to suit your needs or use a lambda function.
427 """
[email protected]cb2985f2010-11-03 14:08:31428 def Find(affected_file, items):
[email protected]ab05d582011-02-09 23:41:22429 local_path = affected_file.LocalPath()
[email protected]cb2985f2010-11-03 14:08:31430 for item in items:
[email protected]df1595a2009-06-11 02:00:13431 if self.re.match(item, local_path):
432 logging.debug("%s matched %s" % (item, local_path))
[email protected]3410d912009-06-09 20:56:16433 return True
434 return False
435 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
436 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
437
438 def AffectedSourceFiles(self, source_file):
439 """Filter the list of AffectedTextFiles by the function source_file.
440
441 If source_file is None, InputApi.FilterSourceFile() is used.
442 """
443 if not source_file:
444 source_file = self.FilterSourceFile
445 return filter(source_file, self.AffectedTextFiles())
446
447 def RightHandSideLines(self, source_file_filter=None):
[email protected]fb2b8eb2009-04-23 21:03:42448 """An iterator over all text lines in "new" version of changed files.
449
450 Only lists lines from new or modified text files in the change that are
451 contained by the directory of the currently executing presubmit script.
452
453 This is useful for doing line-by-line regex checks, like checking for
454 trailing whitespace.
455
456 Yields:
457 a 3 tuple:
458 the AffectedFile instance of the current file;
459 integer line number (1-based); and
460 the contents of the line as a string.
[email protected]1487d532009-06-06 00:22:57461
[email protected]2a3ab7e2011-04-27 22:06:27462 Note: The carriage return (LF or CR) is stripped off.
[email protected]fb2b8eb2009-04-23 21:03:42463 """
[email protected]3410d912009-06-09 20:56:16464 files = self.AffectedSourceFiles(source_file_filter)
[email protected]cb2985f2010-11-03 14:08:31465 return _RightHandSideLinesImpl(files)
[email protected]fb2b8eb2009-04-23 21:03:42466
[email protected]e3608df2009-11-10 20:22:57467 def ReadFile(self, file_item, mode='r'):
[email protected]44a17ad2009-06-08 14:14:35468 """Reads an arbitrary file.
[email protected]da8cddd2009-08-13 00:25:55469
[email protected]44a17ad2009-06-08 14:14:35470 Deny reading anything outside the repository.
471 """
[email protected]e3608df2009-11-10 20:22:57472 if isinstance(file_item, AffectedFile):
473 file_item = file_item.AbsoluteLocalPath()
474 if not file_item.startswith(self.change.RepositoryRoot()):
[email protected]44a17ad2009-06-08 14:14:35475 raise IOError('Access outside the repository root is denied.')
[email protected]5aeb7dd2009-11-17 18:09:01476 return gclient_utils.FileRead(file_item, mode)
[email protected]44a17ad2009-06-08 14:14:35477
[email protected]cc73ad62011-07-06 17:39:26478 @property
479 def tbr(self):
480 """Returns if a change is TBR'ed."""
481 return 'TBR' in self.change.tags
482
[email protected]ffeb2f32013-12-03 13:55:22483 def RunTests(self, tests_mix, parallel=True):
[email protected]bc117312013-04-20 03:57:56484 tests = []
485 msgs = []
486 for t in tests_mix:
487 if isinstance(t, OutputApi.PresubmitResult):
488 msgs.append(t)
489 else:
490 assert issubclass(t.message, _PresubmitResult)
491 tests.append(t)
[email protected]ffeb2f32013-12-03 13:55:22492 if self.verbose:
493 t.info = _PresubmitNotifyResult
[email protected]5678d332013-05-18 01:34:14494 if len(tests) > 1 and parallel:
[email protected]bc117312013-04-20 03:57:56495 pool = multiprocessing.Pool()
496 # async recipe works around multiprocessing bug handling Ctrl-C
497 msgs.extend(pool.map_async(CallCommand, tests).get(99999))
498 pool.close()
499 pool.join()
500 else:
501 msgs.extend(map(CallCommand, tests))
502 return [m for m in msgs if m]
503
[email protected]fb2b8eb2009-04-23 21:03:42504
[email protected]ff526192013-06-10 19:30:26505class _DiffCache(object):
506 """Caches diffs retrieved from a particular SCM."""
[email protected]ea84ef12014-04-30 19:55:12507 def __init__(self, upstream=None):
508 """Stores the upstream revision against which all diffs will be computed."""
509 self._upstream = upstream
[email protected]ff526192013-06-10 19:30:26510
511 def GetDiff(self, path, local_root):
512 """Get the diff for a particular path."""
513 raise NotImplementedError()
514
515
516class _SvnDiffCache(_DiffCache):
517 """DiffCache implementation for subversion."""
[email protected]ea84ef12014-04-30 19:55:12518 def __init__(self, *args, **kwargs):
519 super(_SvnDiffCache, self).__init__(*args, **kwargs)
[email protected]ff526192013-06-10 19:30:26520 self._diffs_by_file = {}
521
522 def GetDiff(self, path, local_root):
523 if path not in self._diffs_by_file:
524 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
525 False, None)
526 return self._diffs_by_file[path]
527
528
529class _GitDiffCache(_DiffCache):
530 """DiffCache implementation for git; gets all file diffs at once."""
[email protected]ea84ef12014-04-30 19:55:12531 def __init__(self, upstream):
532 super(_GitDiffCache, self).__init__(upstream=upstream)
[email protected]ff526192013-06-10 19:30:26533 self._diffs_by_file = None
534
535 def GetDiff(self, path, local_root):
536 if not self._diffs_by_file:
537 # Compute a single diff for all files and parse the output; should
538 # with git this is much faster than computing one diff for each file.
539 diffs = {}
540
541 # Don't specify any filenames below, because there are command line length
542 # limits on some platforms and GenerateDiff would fail.
[email protected]ea84ef12014-04-30 19:55:12543 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
544 branch=self._upstream)
[email protected]ff526192013-06-10 19:30:26545
546 # This regex matches the path twice, separated by a space. Note that
547 # filename itself may contain spaces.
548 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
549 current_diff = []
550 keep_line_endings = True
551 for x in unified_diff.splitlines(keep_line_endings):
552 match = file_marker.match(x)
553 if match:
554 # Marks the start of a new per-file section.
555 diffs[match.group('filename')] = current_diff = [x]
556 elif x.startswith('diff --git'):
557 raise PresubmitFailure('Unexpected diff line: %s' % x)
558 else:
559 current_diff.append(x)
560
561 self._diffs_by_file = dict(
562 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
563
564 if path not in self._diffs_by_file:
565 raise PresubmitFailure(
566 'Unified diff did not contain entry for file %s' % path)
567
568 return self._diffs_by_file[path]
569
570
[email protected]fb2b8eb2009-04-23 21:03:42571class AffectedFile(object):
572 """Representation of a file in a change."""
[email protected]ff526192013-06-10 19:30:26573
574 DIFF_CACHE = _DiffCache
575
[email protected]b17b55b2010-11-03 14:42:37576 # Method could be a function
577 # pylint: disable=R0201
[email protected]ea84ef12014-04-30 19:55:12578 def __init__(self, path, action, repository_root, diff_cache):
[email protected]15bdffa2009-05-29 11:16:29579 self._path = path
580 self._action = action
[email protected]4ff922a2009-06-12 20:20:19581 self._local_root = repository_root
[email protected]15bdffa2009-05-29 11:16:29582 self._is_directory = None
583 self._properties = {}
[email protected]2a3ab7e2011-04-27 22:06:27584 self._cached_changed_contents = None
585 self._cached_new_contents = None
[email protected]ea84ef12014-04-30 19:55:12586 self._diff_cache = diff_cache
[email protected]5d0dc432011-01-03 02:40:37587 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
[email protected]fb2b8eb2009-04-23 21:03:42588
589 def ServerPath(self):
590 """Returns a path string that identifies the file in the SCM system.
591
592 Returns the empty string if the file does not exist in SCM.
593 """
[email protected]ff526192013-06-10 19:30:26594 return ''
[email protected]fb2b8eb2009-04-23 21:03:42595
596 def LocalPath(self):
597 """Returns the path of this file on the local disk relative to client root.
598 """
[email protected]15bdffa2009-05-29 11:16:29599 return normpath(self._path)
[email protected]fb2b8eb2009-04-23 21:03:42600
601 def AbsoluteLocalPath(self):
602 """Returns the absolute path of this file on the local disk.
603 """
[email protected]8e416c82009-10-06 04:30:44604 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
[email protected]fb2b8eb2009-04-23 21:03:42605
606 def IsDirectory(self):
607 """Returns true if this object is a directory."""
[email protected]15bdffa2009-05-29 11:16:29608 if self._is_directory is None:
609 path = self.AbsoluteLocalPath()
610 self._is_directory = (os.path.exists(path) and
611 os.path.isdir(path))
612 return self._is_directory
[email protected]fb2b8eb2009-04-23 21:03:42613
614 def Action(self):
615 """Returns the action on this opened file, e.g. A, M, D, etc."""
[email protected]dbbeedc2009-05-22 20:26:17616 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
617 # different for other SCM.
[email protected]15bdffa2009-05-29 11:16:29618 return self._action
[email protected]fb2b8eb2009-04-23 21:03:42619
[email protected]dbbeedc2009-05-22 20:26:17620 def Property(self, property_name):
621 """Returns the specified SCM property of this file, or None if no such
622 property.
623 """
[email protected]15bdffa2009-05-29 11:16:29624 return self._properties.get(property_name, None)
[email protected]dbbeedc2009-05-22 20:26:17625
[email protected]1e08c002009-05-28 19:09:33626 def IsTextFile(self):
[email protected]77c4f0f2009-05-29 18:53:04627 """Returns True if the file is a text file and not a binary file.
[email protected]da8cddd2009-08-13 00:25:55628
[email protected]77c4f0f2009-05-29 18:53:04629 Deleted files are not text file."""
[email protected]1e08c002009-05-28 19:09:33630 raise NotImplementedError() # Implement when needed
631
[email protected]fb2b8eb2009-04-23 21:03:42632 def NewContents(self):
633 """Returns an iterator over the lines in the new version of file.
634
635 The new version is the file in the user's workspace, i.e. the "right hand
636 side".
637
638 Contents will be empty if the file is a directory or does not exist.
[email protected]2a3ab7e2011-04-27 22:06:27639 Note: The carriage returns (LF or CR) are stripped off.
[email protected]fb2b8eb2009-04-23 21:03:42640 """
[email protected]2a3ab7e2011-04-27 22:06:27641 if self._cached_new_contents is None:
642 self._cached_new_contents = []
643 if not self.IsDirectory():
644 try:
645 self._cached_new_contents = gclient_utils.FileRead(
646 self.AbsoluteLocalPath(), 'rU').splitlines()
647 except IOError:
648 pass # File not found? That's fine; maybe it was deleted.
649 return self._cached_new_contents[:]
[email protected]fb2b8eb2009-04-23 21:03:42650
[email protected]ab05d582011-02-09 23:41:22651 def ChangedContents(self):
652 """Returns a list of tuples (line number, line text) of all new lines.
653
654 This relies on the scm diff output describing each changed code section
655 with a line of the form
656
657 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
658 """
[email protected]2a3ab7e2011-04-27 22:06:27659 if self._cached_changed_contents is not None:
660 return self._cached_changed_contents[:]
661 self._cached_changed_contents = []
[email protected]ab05d582011-02-09 23:41:22662 line_num = 0
663
664 if self.IsDirectory():
665 return []
666
667 for line in self.GenerateScmDiff().splitlines():
668 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
669 if m:
670 line_num = int(m.groups(1)[0])
671 continue
672 if line.startswith('+') and not line.startswith('++'):
[email protected]2a3ab7e2011-04-27 22:06:27673 self._cached_changed_contents.append((line_num, line[1:]))
[email protected]ab05d582011-02-09 23:41:22674 if not line.startswith('-'):
675 line_num += 1
[email protected]2a3ab7e2011-04-27 22:06:27676 return self._cached_changed_contents[:]
[email protected]ab05d582011-02-09 23:41:22677
[email protected]5de13972009-06-10 18:16:06678 def __str__(self):
679 return self.LocalPath()
680
[email protected]ab05d582011-02-09 23:41:22681 def GenerateScmDiff(self):
[email protected]ff526192013-06-10 19:30:26682 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
[email protected]fb2b8eb2009-04-23 21:03:42683
[email protected]58407af2011-04-12 23:15:57684
[email protected]dbbeedc2009-05-22 20:26:17685class SvnAffectedFile(AffectedFile):
686 """Representation of a file in a change out of a Subversion checkout."""
[email protected]b17b55b2010-11-03 14:42:37687 # Method 'NNN' is abstract in class 'NNN' but is not overridden
688 # pylint: disable=W0223
[email protected]dbbeedc2009-05-22 20:26:17689
[email protected]ff526192013-06-10 19:30:26690 DIFF_CACHE = _SvnDiffCache
691
[email protected]15bdffa2009-05-29 11:16:29692 def __init__(self, *args, **kwargs):
693 AffectedFile.__init__(self, *args, **kwargs)
694 self._server_path = None
695 self._is_text_file = None
[email protected]15bdffa2009-05-29 11:16:29696
[email protected]dbbeedc2009-05-22 20:26:17697 def ServerPath(self):
[email protected]15bdffa2009-05-29 11:16:29698 if self._server_path is None:
[email protected]d579fcf2011-12-13 20:36:03699 self._server_path = scm.SVN.CaptureLocalInfo(
700 [self.LocalPath()], self._local_root).get('URL', '')
[email protected]15bdffa2009-05-29 11:16:29701 return self._server_path
[email protected]dbbeedc2009-05-22 20:26:17702
703 def IsDirectory(self):
[email protected]15bdffa2009-05-29 11:16:29704 if self._is_directory is None:
705 path = self.AbsoluteLocalPath()
[email protected]dbbeedc2009-05-22 20:26:17706 if os.path.exists(path):
707 # Retrieve directly from the file system; it is much faster than
708 # querying subversion, especially on Windows.
[email protected]15bdffa2009-05-29 11:16:29709 self._is_directory = os.path.isdir(path)
[email protected]dbbeedc2009-05-22 20:26:17710 else:
[email protected]d579fcf2011-12-13 20:36:03711 self._is_directory = scm.SVN.CaptureLocalInfo(
712 [self.LocalPath()], self._local_root
713 ).get('Node Kind') in ('dir', 'directory')
[email protected]15bdffa2009-05-29 11:16:29714 return self._is_directory
[email protected]dbbeedc2009-05-22 20:26:17715
716 def Property(self, property_name):
[email protected]15bdffa2009-05-29 11:16:29717 if not property_name in self._properties:
[email protected]5aeb7dd2009-11-17 18:09:01718 self._properties[property_name] = scm.SVN.GetFileProperty(
[email protected]d579fcf2011-12-13 20:36:03719 self.LocalPath(), property_name, self._local_root).rstrip()
[email protected]15bdffa2009-05-29 11:16:29720 return self._properties[property_name]
[email protected]dbbeedc2009-05-22 20:26:17721
[email protected]1e08c002009-05-28 19:09:33722 def IsTextFile(self):
[email protected]15bdffa2009-05-29 11:16:29723 if self._is_text_file is None:
724 if self.Action() == 'D':
725 # A deleted file is not a text file.
726 self._is_text_file = False
727 elif self.IsDirectory():
728 self._is_text_file = False
729 else:
[email protected]d579fcf2011-12-13 20:36:03730 mime_type = scm.SVN.GetFileProperty(
731 self.LocalPath(), 'svn:mime-type', self._local_root)
[email protected]15bdffa2009-05-29 11:16:29732 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
733 return self._is_text_file
[email protected]1e08c002009-05-28 19:09:33734
[email protected]dbbeedc2009-05-22 20:26:17735
[email protected]c70a2202009-06-17 12:55:10736class GitAffectedFile(AffectedFile):
737 """Representation of a file in a change out of a git checkout."""
[email protected]b17b55b2010-11-03 14:42:37738 # Method 'NNN' is abstract in class 'NNN' but is not overridden
739 # pylint: disable=W0223
[email protected]c70a2202009-06-17 12:55:10740
[email protected]ff526192013-06-10 19:30:26741 DIFF_CACHE = _GitDiffCache
742
[email protected]c70a2202009-06-17 12:55:10743 def __init__(self, *args, **kwargs):
744 AffectedFile.__init__(self, *args, **kwargs)
745 self._server_path = None
746 self._is_text_file = None
[email protected]c70a2202009-06-17 12:55:10747
748 def ServerPath(self):
749 if self._server_path is None:
[email protected]899e1c12011-04-07 17:03:18750 raise NotImplementedError('TODO(maruel) Implement.')
[email protected]c70a2202009-06-17 12:55:10751 return self._server_path
752
753 def IsDirectory(self):
754 if self._is_directory is None:
755 path = self.AbsoluteLocalPath()
756 if os.path.exists(path):
757 # Retrieve directly from the file system; it is much faster than
758 # querying subversion, especially on Windows.
759 self._is_directory = os.path.isdir(path)
760 else:
[email protected]c70a2202009-06-17 12:55:10761 self._is_directory = False
762 return self._is_directory
763
764 def Property(self, property_name):
765 if not property_name in self._properties:
[email protected]899e1c12011-04-07 17:03:18766 raise NotImplementedError('TODO(maruel) Implement.')
[email protected]c70a2202009-06-17 12:55:10767 return self._properties[property_name]
768
769 def IsTextFile(self):
770 if self._is_text_file is None:
771 if self.Action() == 'D':
772 # A deleted file is not a text file.
773 self._is_text_file = False
774 elif self.IsDirectory():
775 self._is_text_file = False
776 else:
[email protected]c70a2202009-06-17 12:55:10777 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
778 return self._is_text_file
779
[email protected]c1938752011-04-12 23:11:13780
[email protected]4ff922a2009-06-12 20:20:19781class Change(object):
[email protected]6ebe68a2009-05-27 23:43:40782 """Describe a change.
783
784 Used directly by the presubmit scripts to query the current change being
785 tested.
[email protected]da8cddd2009-08-13 00:25:55786
[email protected]6ebe68a2009-05-27 23:43:40787 Instance members:
[email protected]ff526192013-06-10 19:30:26788 tags: Dictionary of KEY=VALUE pairs found in the change description.
[email protected]6ebe68a2009-05-27 23:43:40789 self.KEY: equivalent to tags['KEY']
790 """
791
[email protected]4ff922a2009-06-12 20:20:19792 _AFFECTED_FILES = AffectedFile
793
[email protected]6ebe68a2009-05-27 23:43:40794 # Matches key/value (or "tag") lines in changelist descriptions.
[email protected]428342a2011-11-10 15:46:33795 TAG_LINE_RE = re.compile(
[email protected]c6f60e82013-04-19 17:01:57796 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
[email protected]c1938752011-04-12 23:11:13797 scm = ''
[email protected]fb2b8eb2009-04-23 21:03:42798
[email protected]58407af2011-04-12 23:15:57799 def __init__(
[email protected]ea84ef12014-04-30 19:55:12800 self, name, description, local_root, files, issue, patchset, author,
801 upstream=None):
[email protected]4ff922a2009-06-12 20:20:19802 if files is None:
803 files = []
804 self._name = name
[email protected]8e416c82009-10-06 04:30:44805 # Convert root into an absolute path.
806 self._local_root = os.path.abspath(local_root)
[email protected]ea84ef12014-04-30 19:55:12807 self._upstream = upstream
[email protected]4ff922a2009-06-12 20:20:19808 self.issue = issue
809 self.patchset = patchset
[email protected]58407af2011-04-12 23:15:57810 self.author_email = author
[email protected]fb2b8eb2009-04-23 21:03:42811
[email protected]b5cded62014-03-25 17:47:57812 self._full_description = ''
[email protected]fb2b8eb2009-04-23 21:03:42813 self.tags = {}
[email protected]b5cded62014-03-25 17:47:57814 self._description_without_tags = ''
815 self.SetDescriptionText(description)
[email protected]fb2b8eb2009-04-23 21:03:42816
[email protected]e085d812011-10-10 19:49:15817 assert all(
818 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
819
[email protected]ea84ef12014-04-30 19:55:12820 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
[email protected]6ebe68a2009-05-27 23:43:40821 self._affected_files = [
[email protected]ff526192013-06-10 19:30:26822 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
823 for action, path in files
[email protected]dbbeedc2009-05-22 20:26:17824 ]
[email protected]fb2b8eb2009-04-23 21:03:42825
[email protected]92022ec2009-06-11 01:59:28826 def Name(self):
[email protected]fb2b8eb2009-04-23 21:03:42827 """Returns the change name."""
[email protected]6ebe68a2009-05-27 23:43:40828 return self._name
[email protected]fb2b8eb2009-04-23 21:03:42829
[email protected]fb2b8eb2009-04-23 21:03:42830 def DescriptionText(self):
831 """Returns the user-entered changelist description, minus tags.
832
833 Any line in the user-provided description starting with e.g. "FOO="
834 (whitespace permitted before and around) is considered a tag line. Such
835 lines are stripped out of the description this function returns.
836 """
[email protected]6ebe68a2009-05-27 23:43:40837 return self._description_without_tags
[email protected]fb2b8eb2009-04-23 21:03:42838
839 def FullDescriptionText(self):
840 """Returns the complete changelist description including tags."""
[email protected]6ebe68a2009-05-27 23:43:40841 return self._full_description
[email protected]fb2b8eb2009-04-23 21:03:42842
[email protected]b5cded62014-03-25 17:47:57843 def SetDescriptionText(self, description):
844 """Sets the full description text (including tags) to |description|.
[email protected]92c30092014-04-15 00:30:37845
[email protected]b5cded62014-03-25 17:47:57846 Also updates the list of tags."""
847 self._full_description = description
848
849 # From the description text, build up a dictionary of key/value pairs
850 # plus the description minus all key/value or "tag" lines.
851 description_without_tags = []
852 self.tags = {}
853 for line in self._full_description.splitlines():
854 m = self.TAG_LINE_RE.match(line)
855 if m:
856 self.tags[m.group('key')] = m.group('value')
857 else:
858 description_without_tags.append(line)
859
860 # Change back to text and remove whitespace at end.
861 self._description_without_tags = (
862 '\n'.join(description_without_tags).rstrip())
863
[email protected]fb2b8eb2009-04-23 21:03:42864 def RepositoryRoot(self):
[email protected]92022ec2009-06-11 01:59:28865 """Returns the repository (checkout) root directory for this change,
866 as an absolute path.
867 """
[email protected]4ff922a2009-06-12 20:20:19868 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42869
870 def __getattr__(self, attr):
[email protected]92022ec2009-06-11 01:59:28871 """Return tags directly as attributes on the object."""
872 if not re.match(r"^[A-Z_]*$", attr):
873 raise AttributeError(self, attr)
[email protected]e1a524f2009-05-27 14:43:46874 return self.tags.get(attr)
[email protected]fb2b8eb2009-04-23 21:03:42875
[email protected]40a3d0b2014-05-15 01:59:16876 def AllFiles(self, root=None):
877 """List all files under source control in the repo."""
878 raise NotImplementedError()
879
[email protected]5538e022011-05-12 17:53:16880 def AffectedFiles(self, include_dirs=False, include_deletes=True,
881 file_filter=None):
[email protected]fb2b8eb2009-04-23 21:03:42882 """Returns a list of AffectedFile instances for all files in the change.
883
884 Args:
885 include_deletes: If false, deleted files will be filtered out.
886 include_dirs: True to include directories in the list
[email protected]5538e022011-05-12 17:53:16887 file_filter: An additional filter to apply.
[email protected]fb2b8eb2009-04-23 21:03:42888
889 Returns:
890 [AffectedFile(path, action), AffectedFile(path, action)]
891 """
892 if include_dirs:
[email protected]6ebe68a2009-05-27 23:43:40893 affected = self._affected_files
[email protected]fb2b8eb2009-04-23 21:03:42894 else:
[email protected]6ebe68a2009-05-27 23:43:40895 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
[email protected]fb2b8eb2009-04-23 21:03:42896
[email protected]5538e022011-05-12 17:53:16897 affected = filter(file_filter, affected)
898
[email protected]fb2b8eb2009-04-23 21:03:42899 if include_deletes:
900 return affected
901 else:
902 return filter(lambda x: x.Action() != 'D', affected)
903
[email protected]77c4f0f2009-05-29 18:53:04904 def AffectedTextFiles(self, include_deletes=None):
905 """Return a list of the existing text files in a change."""
906 if include_deletes is not None:
[email protected]cb2985f2010-11-03 14:08:31907 warn("AffectedTextFiles(include_deletes=%s)"
908 " is deprecated and ignored" % str(include_deletes),
909 category=DeprecationWarning,
910 stacklevel=2)
[email protected]77c4f0f2009-05-29 18:53:04911 return filter(lambda x: x.IsTextFile(),
912 self.AffectedFiles(include_dirs=False, include_deletes=False))
[email protected]fb2b8eb2009-04-23 21:03:42913
914 def LocalPaths(self, include_dirs=False):
915 """Convenience function."""
916 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
917
918 def AbsoluteLocalPaths(self, include_dirs=False):
919 """Convenience function."""
920 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
921
922 def ServerPaths(self, include_dirs=False):
923 """Convenience function."""
924 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
925
926 def RightHandSideLines(self):
927 """An iterator over all text lines in "new" version of changed files.
928
929 Lists lines from new or modified text files in the change.
930
931 This is useful for doing line-by-line regex checks, like checking for
932 trailing whitespace.
933
934 Yields:
935 a 3 tuple:
936 the AffectedFile instance of the current file;
937 integer line number (1-based); and
938 the contents of the line as a string.
939 """
[email protected]cb2985f2010-11-03 14:08:31940 return _RightHandSideLinesImpl(
941 x for x in self.AffectedFiles(include_deletes=False)
942 if x.IsTextFile())
[email protected]fb2b8eb2009-04-23 21:03:42943
944
[email protected]4ff922a2009-06-12 20:20:19945class SvnChange(Change):
946 _AFFECTED_FILES = SvnAffectedFile
[email protected]c1938752011-04-12 23:11:13947 scm = 'svn'
948 _changelists = None
[email protected]6bd31702009-09-02 23:29:07949
950 def _GetChangeLists(self):
951 """Get all change lists."""
952 if self._changelists == None:
953 previous_cwd = os.getcwd()
954 os.chdir(self.RepositoryRoot())
[email protected]cb2985f2010-11-03 14:08:31955 # Need to import here to avoid circular dependency.
956 import gcl
[email protected]6bd31702009-09-02 23:29:07957 self._changelists = gcl.GetModifiedFiles()
958 os.chdir(previous_cwd)
959 return self._changelists
[email protected]da8cddd2009-08-13 00:25:55960
961 def GetAllModifiedFiles(self):
962 """Get all modified files."""
[email protected]6bd31702009-09-02 23:29:07963 changelists = self._GetChangeLists()
[email protected]da8cddd2009-08-13 00:25:55964 all_modified_files = []
965 for cl in changelists.values():
[email protected]6bd31702009-09-02 23:29:07966 all_modified_files.extend(
967 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
[email protected]da8cddd2009-08-13 00:25:55968 return all_modified_files
969
970 def GetModifiedFiles(self):
971 """Get modified files in the current CL."""
[email protected]6bd31702009-09-02 23:29:07972 changelists = self._GetChangeLists()
973 return [os.path.join(self.RepositoryRoot(), f[1])
974 for f in changelists[self.Name()]]
[email protected]da8cddd2009-08-13 00:25:55975
[email protected]40a3d0b2014-05-15 01:59:16976 def AllFiles(self, root=None):
977 """List all files under source control in the repo."""
978 root = root or self.RepositoryRoot()
979 return subprocess.check_output(
980 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
981
[email protected]4ff922a2009-06-12 20:20:19982
[email protected]c70a2202009-06-17 12:55:10983class GitChange(Change):
984 _AFFECTED_FILES = GitAffectedFile
[email protected]c1938752011-04-12 23:11:13985 scm = 'git'
[email protected]da8cddd2009-08-13 00:25:55986
[email protected]40a3d0b2014-05-15 01:59:16987 def AllFiles(self, root=None):
988 """List all files under source control in the repo."""
989 root = root or self.RepositoryRoot()
990 return subprocess.check_output(
991 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
992
[email protected]c70a2202009-06-17 12:55:10993
[email protected]4661e0c2009-06-04 00:45:26994def ListRelevantPresubmitFiles(files, root):
[email protected]fb2b8eb2009-04-23 21:03:42995 """Finds all presubmit files that apply to a given set of source files.
996
[email protected]b1901a62010-06-16 00:18:47997 If inherit-review-settings-ok is present right under root, looks for
998 PRESUBMIT.py in directories enclosing root.
999
[email protected]fb2b8eb2009-04-23 21:03:421000 Args:
1001 files: An iterable container containing file paths.
[email protected]4661e0c2009-06-04 00:45:261002 root: Path where to stop searching.
[email protected]fb2b8eb2009-04-23 21:03:421003
1004 Return:
[email protected]4661e0c2009-06-04 00:45:261005 List of absolute paths of the existing PRESUBMIT.py scripts.
[email protected]fb2b8eb2009-04-23 21:03:421006 """
[email protected]b1901a62010-06-16 00:18:471007 files = [normpath(os.path.join(root, f)) for f in files]
1008
1009 # List all the individual directories containing files.
1010 directories = set([os.path.dirname(f) for f in files])
1011
1012 # Ignore root if inherit-review-settings-ok is present.
1013 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1014 root = None
1015
1016 # Collect all unique directories that may contain PRESUBMIT.py.
1017 candidates = set()
1018 for directory in directories:
1019 while True:
1020 if directory in candidates:
[email protected]fb2b8eb2009-04-23 21:03:421021 break
[email protected]b1901a62010-06-16 00:18:471022 candidates.add(directory)
1023 if directory == root:
[email protected]4661e0c2009-06-04 00:45:261024 break
[email protected]b1901a62010-06-16 00:18:471025 parent_dir = os.path.dirname(directory)
1026 if parent_dir == directory:
1027 # We hit the system root directory.
1028 break
1029 directory = parent_dir
1030
1031 # Look for PRESUBMIT.py in all candidate directories.
1032 results = []
1033 for directory in sorted(list(candidates)):
1034 p = os.path.join(directory, 'PRESUBMIT.py')
1035 if os.path.isfile(p):
1036 results.append(p)
1037
[email protected]5d0dc432011-01-03 02:40:371038 logging.debug('Presubmit files: %s' % ','.join(results))
[email protected]b1901a62010-06-16 00:18:471039 return results
[email protected]fb2b8eb2009-04-23 21:03:421040
1041
[email protected]de243452009-10-06 21:02:561042class GetTrySlavesExecuter(object):
[email protected]cb2985f2010-11-03 14:08:311043 @staticmethod
[email protected]15169952011-09-27 14:30:531044 def ExecPresubmitScript(script_text, presubmit_path, project, change):
[email protected]de243452009-10-06 21:02:561045 """Executes GetPreferredTrySlaves() from a single presubmit script.
[email protected]92c30092014-04-15 00:30:371046
[email protected]58a69cb2014-03-01 02:08:291047 This will soon be deprecated and replaced by GetPreferredTryMasters().
[email protected]de243452009-10-06 21:02:561048
1049 Args:
1050 script_text: The text of the presubmit script.
[email protected]78230022011-05-24 18:55:191051 presubmit_path: Project script to run.
1052 project: Project name to pass to presubmit script for bot selection.
[email protected]de243452009-10-06 21:02:561053
1054 Return:
1055 A list of try slaves.
1056 """
1057 context = {}
[email protected]f6349642014-03-04 00:52:181058 main_path = os.getcwd()
[email protected]899e1c12011-04-07 17:03:181059 try:
[email protected]f6349642014-03-04 00:52:181060 os.chdir(os.path.dirname(presubmit_path))
[email protected]899e1c12011-04-07 17:03:181061 exec script_text in context
1062 except Exception, e:
1063 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
[email protected]f6349642014-03-04 00:52:181064 finally:
1065 os.chdir(main_path)
[email protected]de243452009-10-06 21:02:561066
1067 function_name = 'GetPreferredTrySlaves'
1068 if function_name in context:
[email protected]15169952011-09-27 14:30:531069 get_preferred_try_slaves = context[function_name]
1070 function_info = inspect.getargspec(get_preferred_try_slaves)
1071 if len(function_info[0]) == 1:
1072 result = get_preferred_try_slaves(project)
1073 elif len(function_info[0]) == 2:
1074 result = get_preferred_try_slaves(project, change)
1075 else:
1076 result = get_preferred_try_slaves()
[email protected]de243452009-10-06 21:02:561077 if not isinstance(result, types.ListType):
[email protected]899e1c12011-04-07 17:03:181078 raise PresubmitFailure(
[email protected]de243452009-10-06 21:02:561079 'Presubmit functions must return a list, got a %s instead: %s' %
1080 (type(result), str(result)))
1081 for item in result:
[email protected]68e04192013-11-04 22:14:381082 if isinstance(item, basestring):
1083 # Old-style ['bot'] format.
1084 botname = item
1085 elif isinstance(item, tuple):
1086 # New-style [('bot', set(['tests']))] format.
1087 botname = item[0]
1088 else:
1089 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1090 ' format.')
1091
1092 if botname != botname.strip():
[email protected]899e1c12011-04-07 17:03:181093 raise PresubmitFailure(
1094 'Try slave names cannot start/end with whitespace')
[email protected]68e04192013-11-04 22:14:381095 if ',' in botname:
[email protected]3ecc8ea2012-03-10 01:47:461096 raise PresubmitFailure(
[email protected]68e04192013-11-04 22:14:381097 'Do not use \',\' separated builder or test names: %s' % botname)
[email protected]de243452009-10-06 21:02:561098 else:
1099 result = []
[email protected]5ca27622013-12-18 17:44:581100
1101 def valid_oldstyle(result):
1102 return all(isinstance(i, basestring) for i in result)
1103
1104 def valid_newstyle(result):
1105 return (all(isinstance(i, tuple) for i in result) and
1106 all(len(i) == 2 for i in result) and
1107 all(isinstance(i[0], basestring) for i in result) and
1108 all(isinstance(i[1], set) for i in result)
1109 )
1110
1111 # Ensure it's either all old-style or all new-style.
1112 if not valid_oldstyle(result) and not valid_newstyle(result):
1113 raise PresubmitFailure(
1114 'PRESUBMIT.py returned invalid trybot specification!')
1115
[email protected]de243452009-10-06 21:02:561116 return result
1117
1118
[email protected]58a69cb2014-03-01 02:08:291119class GetTryMastersExecuter(object):
1120 @staticmethod
1121 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1122 """Executes GetPreferredTryMasters() from a single presubmit script.
1123
1124 Args:
1125 script_text: The text of the presubmit script.
1126 presubmit_path: Project script to run.
1127 project: Project name to pass to presubmit script for bot selection.
1128
1129 Return:
1130 A map of try masters to map of builders to set of tests.
1131 """
1132 context = {}
1133 try:
1134 exec script_text in context
1135 except Exception, e:
1136 raise PresubmitFailure('"%s" had an exception.\n%s'
1137 % (presubmit_path, e))
1138
1139 function_name = 'GetPreferredTryMasters'
1140 if function_name not in context:
1141 return {}
1142 get_preferred_try_masters = context[function_name]
1143 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1144 raise PresubmitFailure(
1145 'Expected function "GetPreferredTryMasters" to take two arguments.')
1146 return get_preferred_try_masters(project, change)
1147
1148
[email protected]5626a922015-02-26 14:03:301149class GetPostUploadExecuter(object):
1150 @staticmethod
1151 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1152 """Executes PostUploadHook() from a single presubmit script.
1153
1154 Args:
1155 script_text: The text of the presubmit script.
1156 presubmit_path: Project script to run.
1157 cl: The Changelist object.
1158 change: The Change object.
1159
1160 Return:
1161 A list of results objects.
1162 """
1163 context = {}
1164 try:
1165 exec script_text in context
1166 except Exception, e:
1167 raise PresubmitFailure('"%s" had an exception.\n%s'
1168 % (presubmit_path, e))
1169
1170 function_name = 'PostUploadHook'
1171 if function_name not in context:
1172 return {}
1173 post_upload_hook = context[function_name]
1174 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1175 raise PresubmitFailure(
1176 'Expected function "PostUploadHook" to take three arguments.')
1177 return post_upload_hook(cl, change, OutputApi(False))
1178
1179
[email protected]15169952011-09-27 14:30:531180def DoGetTrySlaves(change,
1181 changed_files,
[email protected]de243452009-10-06 21:02:561182 repository_root,
1183 default_presubmit,
[email protected]78230022011-05-24 18:55:191184 project,
[email protected]de243452009-10-06 21:02:561185 verbose,
1186 output_stream):
[email protected]58a69cb2014-03-01 02:08:291187 """Get the list of try servers from the presubmit scripts (deprecated).
[email protected]de243452009-10-06 21:02:561188
1189 Args:
1190 changed_files: List of modified files.
1191 repository_root: The repository root.
1192 default_presubmit: A default presubmit script to execute in any case.
[email protected]78230022011-05-24 18:55:191193 project: Optional name of a project used in selecting trybots.
[email protected]de243452009-10-06 21:02:561194 verbose: Prints debug info.
1195 output_stream: A stream to write debug output to.
1196
1197 Return:
1198 List of try slaves
1199 """
1200 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1201 if not presubmit_files and verbose:
[email protected]d59e7612014-03-05 19:55:561202 output_stream.write("Warning, no PRESUBMIT.py found.\n")
[email protected]de243452009-10-06 21:02:561203 results = []
1204 executer = GetTrySlavesExecuter()
[email protected]5ca27622013-12-18 17:44:581205
[email protected]de243452009-10-06 21:02:561206 if default_presubmit:
1207 if verbose:
1208 output_stream.write("Running default presubmit script.\n")
[email protected]899e1c12011-04-07 17:03:181209 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
[email protected]5ca27622013-12-18 17:44:581210 results.extend(executer.ExecPresubmitScript(
1211 default_presubmit, fake_path, project, change))
[email protected]de243452009-10-06 21:02:561212 for filename in presubmit_files:
1213 filename = os.path.abspath(filename)
1214 if verbose:
1215 output_stream.write("Running %s\n" % filename)
1216 # Accept CRLF presubmit script.
[email protected]5aeb7dd2009-11-17 18:09:011217 presubmit_script = gclient_utils.FileRead(filename, 'rU')
[email protected]5ca27622013-12-18 17:44:581218 results.extend(executer.ExecPresubmitScript(
1219 presubmit_script, filename, project, change))
[email protected]de243452009-10-06 21:02:561220
[email protected]5ca27622013-12-18 17:44:581221
1222 slave_dict = {}
1223 old_style = filter(lambda x: isinstance(x, basestring), results)
1224 new_style = filter(lambda x: isinstance(x, tuple), results)
1225
1226 for result in new_style:
1227 slave_dict.setdefault(result[0], set()).update(result[1])
1228 slaves = list(slave_dict.items())
1229
1230 slaves.extend(set(old_style))
[email protected]68e04192013-11-04 22:14:381231
[email protected]de243452009-10-06 21:02:561232 if slaves and verbose:
[email protected]5ca27622013-12-18 17:44:581233 output_stream.write(', '.join((str(x) for x in slaves)))
[email protected]de243452009-10-06 21:02:561234 output_stream.write('\n')
1235 return slaves
1236
1237
[email protected]58a69cb2014-03-01 02:08:291238def _MergeMasters(masters1, masters2):
1239 """Merges two master maps. Merges also the tests of each builder."""
1240 result = {}
1241 for (master, builders) in itertools.chain(masters1.iteritems(),
1242 masters2.iteritems()):
1243 new_builders = result.setdefault(master, {})
1244 for (builder, tests) in builders.iteritems():
1245 new_builders.setdefault(builder, set([])).update(tests)
1246 return result
1247
1248
1249def DoGetTryMasters(change,
1250 changed_files,
1251 repository_root,
1252 default_presubmit,
1253 project,
1254 verbose,
1255 output_stream):
1256 """Get the list of try masters from the presubmit scripts.
1257
1258 Args:
1259 changed_files: List of modified files.
1260 repository_root: The repository root.
1261 default_presubmit: A default presubmit script to execute in any case.
1262 project: Optional name of a project used in selecting trybots.
1263 verbose: Prints debug info.
1264 output_stream: A stream to write debug output to.
1265
1266 Return:
1267 Map of try masters to map of builders to set of tests.
1268 """
1269 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1270 if not presubmit_files and verbose:
1271 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1272 results = {}
1273 executer = GetTryMastersExecuter()
1274
1275 if default_presubmit:
1276 if verbose:
1277 output_stream.write("Running default presubmit script.\n")
1278 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1279 results = _MergeMasters(results, executer.ExecPresubmitScript(
1280 default_presubmit, fake_path, project, change))
1281 for filename in presubmit_files:
1282 filename = os.path.abspath(filename)
1283 if verbose:
1284 output_stream.write("Running %s\n" % filename)
1285 # Accept CRLF presubmit script.
1286 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1287 results = _MergeMasters(results, executer.ExecPresubmitScript(
1288 presubmit_script, filename, project, change))
1289
1290 # Make sets to lists again for later JSON serialization.
1291 for builders in results.itervalues():
1292 for builder in builders:
1293 builders[builder] = list(builders[builder])
1294
1295 if results and verbose:
1296 output_stream.write('%s\n' % str(results))
1297 return results
1298
1299
[email protected]5626a922015-02-26 14:03:301300def DoPostUploadExecuter(change,
1301 cl,
1302 repository_root,
1303 verbose,
1304 output_stream):
1305 """Execute the post upload hook.
1306
1307 Args:
1308 change: The Change object.
1309 cl: The Changelist object.
1310 repository_root: The repository root.
1311 verbose: Prints debug info.
1312 output_stream: A stream to write debug output to.
1313 """
1314 presubmit_files = ListRelevantPresubmitFiles(
1315 change.LocalPaths(), repository_root)
1316 if not presubmit_files and verbose:
1317 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1318 results = []
1319 executer = GetPostUploadExecuter()
1320 # The root presubmit file should be executed after the ones in subdirectories.
1321 # i.e. the specific post upload hooks should run before the general ones.
1322 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1323 presubmit_files.reverse()
1324
1325 for filename in presubmit_files:
1326 filename = os.path.abspath(filename)
1327 if verbose:
1328 output_stream.write("Running %s\n" % filename)
1329 # Accept CRLF presubmit script.
1330 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1331 results.extend(executer.ExecPresubmitScript(
1332 presubmit_script, filename, cl, change))
1333 output_stream.write('\n')
1334 if results:
1335 output_stream.write('** Post Upload Hook Messages **\n')
1336 for result in results:
1337 result.handle(output_stream)
1338 output_stream.write('\n')
1339
1340 return results
1341
1342
[email protected]fb2b8eb2009-04-23 21:03:421343class PresubmitExecuter(object):
[email protected]cc73ad62011-07-06 17:39:261344 def __init__(self, change, committing, rietveld_obj, verbose):
[email protected]fb2b8eb2009-04-23 21:03:421345 """
1346 Args:
[email protected]4ff922a2009-06-12 20:20:191347 change: The Change object.
[email protected]fb2b8eb2009-04-23 21:03:421348 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
[email protected]239f4112011-06-03 20:08:231349 rietveld_obj: rietveld.Rietveld client object.
[email protected]fb2b8eb2009-04-23 21:03:421350 """
[email protected]4ff922a2009-06-12 20:20:191351 self.change = change
[email protected]fb2b8eb2009-04-23 21:03:421352 self.committing = committing
[email protected]239f4112011-06-03 20:08:231353 self.rietveld = rietveld_obj
[email protected]899e1c12011-04-07 17:03:181354 self.verbose = verbose
[email protected]fb2b8eb2009-04-23 21:03:421355
1356 def ExecPresubmitScript(self, script_text, presubmit_path):
1357 """Executes a single presubmit script.
1358
1359 Args:
1360 script_text: The text of the presubmit script.
1361 presubmit_path: The path to the presubmit file (this will be reported via
1362 input_api.PresubmitLocalPath()).
1363
1364 Return:
1365 A list of result objects, empty if no problems.
1366 """
[email protected]c6ef53a2014-11-04 00:13:541367
[email protected]8e416c82009-10-06 04:30:441368 # Change to the presubmit file's directory to support local imports.
1369 main_path = os.getcwd()
1370 os.chdir(os.path.dirname(presubmit_path))
1371
1372 # Load the presubmit script into context.
[email protected]970c5222011-03-12 00:32:241373 input_api = InputApi(self.change, presubmit_path, self.committing,
[email protected]cc73ad62011-07-06 17:39:261374 self.rietveld, self.verbose)
[email protected]fb2b8eb2009-04-23 21:03:421375 context = {}
[email protected]899e1c12011-04-07 17:03:181376 try:
1377 exec script_text in context
1378 except Exception, e:
1379 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
[email protected]fb2b8eb2009-04-23 21:03:421380
1381 # These function names must change if we make substantial changes to
1382 # the presubmit API that are not backwards compatible.
1383 if self.committing:
1384 function_name = 'CheckChangeOnCommit'
1385 else:
1386 function_name = 'CheckChangeOnUpload'
1387 if function_name in context:
[email protected]a6d011e2013-03-26 17:31:491388 context['__args'] = (input_api, OutputApi(self.committing))
[email protected]5d0dc432011-01-03 02:40:371389 logging.debug('Running %s in %s' % (function_name, presubmit_path))
[email protected]fb2b8eb2009-04-23 21:03:421390 result = eval(function_name + '(*__args)', context)
[email protected]5d0dc432011-01-03 02:40:371391 logging.debug('Running %s done.' % function_name)
[email protected]fb2b8eb2009-04-23 21:03:421392 if not (isinstance(result, types.TupleType) or
1393 isinstance(result, types.ListType)):
[email protected]899e1c12011-04-07 17:03:181394 raise PresubmitFailure(
[email protected]fb2b8eb2009-04-23 21:03:421395 'Presubmit functions must return a tuple or list')
1396 for item in result:
1397 if not isinstance(item, OutputApi.PresubmitResult):
[email protected]899e1c12011-04-07 17:03:181398 raise PresubmitFailure(
[email protected]fb2b8eb2009-04-23 21:03:421399 'All presubmit results must be of types derived from '
1400 'output_api.PresubmitResult')
1401 else:
1402 result = () # no error since the script doesn't care about current event.
1403
[email protected]8e416c82009-10-06 04:30:441404 # Return the process to the original working directory.
1405 os.chdir(main_path)
[email protected]fb2b8eb2009-04-23 21:03:421406 return result
1407
[email protected]5ac21012011-03-16 02:58:251408
[email protected]4ff922a2009-06-12 20:20:191409def DoPresubmitChecks(change,
[email protected]fb2b8eb2009-04-23 21:03:421410 committing,
1411 verbose,
1412 output_stream,
[email protected]0ff1fab2009-05-22 13:08:151413 input_stream,
[email protected]b0dfd352009-06-10 14:12:541414 default_presubmit,
[email protected]970c5222011-03-12 00:32:241415 may_prompt,
[email protected]c6ef53a2014-11-04 00:13:541416 rietveld_obj):
[email protected]fb2b8eb2009-04-23 21:03:421417 """Runs all presubmit checks that apply to the files in the change.
1418
1419 This finds all PRESUBMIT.py files in directories enclosing the files in the
1420 change (up to the repository root) and calls the relevant entrypoint function
1421 depending on whether the change is being committed or uploaded.
1422
1423 Prints errors, warnings and notifications. Prompts the user for warnings
1424 when needed.
1425
1426 Args:
[email protected]4ff922a2009-06-12 20:20:191427 change: The Change object.
[email protected]fb2b8eb2009-04-23 21:03:421428 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1429 verbose: Prints debug info.
1430 output_stream: A stream to write output from presubmit tests to.
1431 input_stream: A stream to read input from the user.
[email protected]0ff1fab2009-05-22 13:08:151432 default_presubmit: A default presubmit script to execute in any case.
[email protected]b0dfd352009-06-10 14:12:541433 may_prompt: Enable (y/n) questions on warning or error.
[email protected]239f4112011-06-03 20:08:231434 rietveld_obj: rietveld.Rietveld object.
[email protected]fb2b8eb2009-04-23 21:03:421435
[email protected]ce8e46b2009-06-26 22:31:511436 Warning:
1437 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1438 SHOULD be sys.stdin.
1439
[email protected]fb2b8eb2009-04-23 21:03:421440 Return:
[email protected]5ac21012011-03-16 02:58:251441 A PresubmitOutput object. Use output.should_continue() to figure out
1442 if there were errors or warnings and the caller should abort.
[email protected]fb2b8eb2009-04-23 21:03:421443 """
[email protected]ea7c8552011-04-18 14:12:071444 old_environ = os.environ
1445 try:
1446 # Make sure python subprocesses won't generate .pyc files.
1447 os.environ = os.environ.copy()
1448 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
[email protected]fb2b8eb2009-04-23 21:03:421449
[email protected]ea7c8552011-04-18 14:12:071450 output = PresubmitOutput(input_stream, output_stream)
1451 if committing:
1452 output.write("Running presubmit commit checks ...\n")
[email protected]fb2b8eb2009-04-23 21:03:421453 else:
[email protected]ea7c8552011-04-18 14:12:071454 output.write("Running presubmit upload checks ...\n")
1455 start_time = time.time()
1456 presubmit_files = ListRelevantPresubmitFiles(
1457 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1458 if not presubmit_files and verbose:
[email protected]fae707b2011-09-15 18:57:581459 output.write("Warning, no PRESUBMIT.py found.\n")
[email protected]ea7c8552011-04-18 14:12:071460 results = []
[email protected]cc73ad62011-07-06 17:39:261461 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
[email protected]ea7c8552011-04-18 14:12:071462 if default_presubmit:
1463 if verbose:
1464 output.write("Running default presubmit script.\n")
1465 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1466 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1467 for filename in presubmit_files:
1468 filename = os.path.abspath(filename)
1469 if verbose:
1470 output.write("Running %s\n" % filename)
1471 # Accept CRLF presubmit script.
1472 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1473 results += executer.ExecPresubmitScript(presubmit_script, filename)
[email protected]fb2b8eb2009-04-23 21:03:421474
[email protected]ea7c8552011-04-18 14:12:071475 errors = []
1476 notifications = []
1477 warnings = []
1478 for result in results:
1479 if result.fatal:
1480 errors.append(result)
1481 elif result.should_prompt:
1482 warnings.append(result)
1483 else:
1484 notifications.append(result)
[email protected]ed9a0832009-09-09 22:48:551485
[email protected]ea7c8552011-04-18 14:12:071486 output.write('\n')
1487 for name, items in (('Messages', notifications),
1488 ('Warnings', warnings),
1489 ('ERRORS', errors)):
1490 if items:
1491 output.write('** Presubmit %s **\n' % name)
1492 for item in items:
1493 item.handle(output)
1494 output.write('\n')
[email protected]ed9a0832009-09-09 22:48:551495
[email protected]ea7c8552011-04-18 14:12:071496 total_time = time.time() - start_time
1497 if total_time > 1.0:
1498 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
[email protected]ce8e46b2009-06-26 22:31:511499
[email protected]ea7c8552011-04-18 14:12:071500 if not errors:
1501 if not warnings:
1502 output.write('Presubmit checks passed.\n')
1503 elif may_prompt:
1504 output.prompt_yes_no('There were presubmit warnings. '
1505 'Are you sure you wish to continue? (y/N): ')
1506 else:
1507 output.fail()
1508
1509 global _ASKED_FOR_FEEDBACK
1510 # Ask for feedback one time out of 5.
1511 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
[email protected]1ce8e662014-01-14 15:23:001512 output.write(
1513 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1514 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1515 'on the file to figure out who to ask for help.\n')
[email protected]ea7c8552011-04-18 14:12:071516 _ASKED_FOR_FEEDBACK = True
1517 return output
1518 finally:
1519 os.environ = old_environ
[email protected]fb2b8eb2009-04-23 21:03:421520
1521
1522def ScanSubDirs(mask, recursive):
1523 if not recursive:
[email protected]e57b09d2014-05-07 00:58:131524 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
[email protected]fb2b8eb2009-04-23 21:03:421525 else:
1526 results = []
1527 for root, dirs, files in os.walk('.'):
1528 if '.svn' in dirs:
1529 dirs.remove('.svn')
[email protected]c70a2202009-06-17 12:55:101530 if '.git' in dirs:
1531 dirs.remove('.git')
[email protected]fb2b8eb2009-04-23 21:03:421532 for name in files:
1533 if fnmatch.fnmatch(name, mask):
1534 results.append(os.path.join(root, name))
1535 return results
1536
1537
1538def ParseFiles(args, recursive):
[email protected]7444c502011-02-09 14:02:111539 logging.debug('Searching for %s' % args)
[email protected]fb2b8eb2009-04-23 21:03:421540 files = []
1541 for arg in args:
[email protected]e3608df2009-11-10 20:22:571542 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
[email protected]fb2b8eb2009-04-23 21:03:421543 return files
1544
1545
[email protected]5c8c6de2011-03-18 16:20:181546def load_files(options, args):
1547 """Tries to determine the SCM."""
1548 change_scm = scm.determine_scm(options.root)
1549 files = []
[email protected]5c8c6de2011-03-18 16:20:181550 if args:
1551 files = ParseFiles(args, options.recursive)
[email protected]9b31f162012-01-26 19:02:311552 if change_scm == 'svn':
1553 change_class = SvnChange
1554 if not files:
1555 files = scm.SVN.CaptureStatus([], options.root)
1556 elif change_scm == 'git':
1557 change_class = GitChange
[email protected]2da1ade2014-04-30 17:40:451558 upstream = options.upstream or None
[email protected]9b31f162012-01-26 19:02:311559 if not files:
[email protected]2da1ade2014-04-30 17:40:451560 files = scm.GIT.CaptureStatus([], options.root, upstream)
[email protected]5c8c6de2011-03-18 16:20:181561 else:
[email protected]9b31f162012-01-26 19:02:311562 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1563 if not files:
1564 return None, None
1565 change_class = Change
[email protected]5c8c6de2011-03-18 16:20:181566 return change_class, files
1567
1568
[email protected]8a4a2bc2013-03-08 08:13:201569class NonexistantCannedCheckFilter(Exception):
1570 pass
1571
1572
1573@contextlib.contextmanager
1574def canned_check_filter(method_names):
1575 filtered = {}
1576 try:
1577 for method_name in method_names:
1578 if not hasattr(presubmit_canned_checks, method_name):
1579 raise NonexistantCannedCheckFilter(method_name)
1580 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1581 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1582 yield
1583 finally:
1584 for name, method in filtered.iteritems():
1585 setattr(presubmit_canned_checks, name, method)
1586
[email protected]ffeb2f32013-12-03 13:55:221587
[email protected]bc117312013-04-20 03:57:561588def CallCommand(cmd_data):
[email protected]ffeb2f32013-12-03 13:55:221589 """Runs an external program, potentially from a child process created by the
1590 multiprocessing module.
1591
1592 multiprocessing needs a top level function with a single argument.
1593 """
[email protected]bc117312013-04-20 03:57:561594 cmd_data.kwargs['stdout'] = subprocess.PIPE
1595 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1596 try:
[email protected]ffeb2f32013-12-03 13:55:221597 start = time.time()
[email protected]bc117312013-04-20 03:57:561598 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
[email protected]ffeb2f32013-12-03 13:55:221599 duration = time.time() - start
[email protected]bc117312013-04-20 03:57:561600 except OSError as e:
[email protected]ffeb2f32013-12-03 13:55:221601 duration = time.time() - start
[email protected]bc117312013-04-20 03:57:561602 return cmd_data.message(
[email protected]ffeb2f32013-12-03 13:55:221603 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1604 if code != 0:
1605 return cmd_data.message(
1606 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1607 if cmd_data.info:
1608 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
[email protected]bc117312013-04-20 03:57:561609
[email protected]8a4a2bc2013-03-08 08:13:201610
[email protected]013731e2015-02-26 18:28:431611def main(argv=None):
[email protected]5c8c6de2011-03-18 16:20:181612 parser = optparse.OptionParser(usage="%prog [options] <files...>",
[email protected]fb2b8eb2009-04-23 21:03:421613 version="%prog " + str(__version__))
[email protected]c70a2202009-06-17 12:55:101614 parser.add_option("-c", "--commit", action="store_true", default=False,
[email protected]fb2b8eb2009-04-23 21:03:421615 help="Use commit instead of upload checks")
[email protected]c70a2202009-06-17 12:55:101616 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1617 help="Use upload instead of commit checks")
[email protected]fb2b8eb2009-04-23 21:03:421618 parser.add_option("-r", "--recursive", action="store_true",
1619 help="Act recursively")
[email protected]899e1c12011-04-07 17:03:181620 parser.add_option("-v", "--verbose", action="count", default=0,
1621 help="Use 2 times for more debug info")
[email protected]4ff922a2009-06-12 20:20:191622 parser.add_option("--name", default='no name')
[email protected]58407af2011-04-12 23:15:571623 parser.add_option("--author")
[email protected]4ff922a2009-06-12 20:20:191624 parser.add_option("--description", default='')
1625 parser.add_option("--issue", type='int', default=0)
1626 parser.add_option("--patchset", type='int', default=0)
[email protected]b1901a62010-06-16 00:18:471627 parser.add_option("--root", default=os.getcwd(),
1628 help="Search for PRESUBMIT.py up to this directory. "
1629 "If inherit-review-settings-ok is present in this "
1630 "directory, parent directories up to the root file "
1631 "system directories will also be searched.")
[email protected]2da1ade2014-04-30 17:40:451632 parser.add_option("--upstream",
1633 help="Git only: the base ref or upstream branch against "
1634 "which the diff should be computed.")
[email protected]c70a2202009-06-17 12:55:101635 parser.add_option("--default_presubmit")
1636 parser.add_option("--may_prompt", action='store_true', default=False)
[email protected]8a4a2bc2013-03-08 08:13:201637 parser.add_option("--skip_canned", action='append', default=[],
1638 help="A list of checks to skip which appear in "
1639 "presubmit_canned_checks. Can be provided multiple times "
1640 "to skip multiple canned checks.")
[email protected]239f4112011-06-03 20:08:231641 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1642 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
[email protected]720fd7a2013-04-24 04:13:501643 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1644 help=optparse.SUPPRESS_HELP)
[email protected]92c30092014-04-15 00:30:371645 # These are for OAuth2 authentication for bots. See also apply_issue.py
1646 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1647 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1648
[email protected]f7d31f52014-01-03 20:14:461649 parser.add_option("--trybot-json",
1650 help="Output trybot information to the file specified.")
[email protected]cf6a5d22015-04-09 22:02:001651 auth.add_auth_options(parser)
[email protected]82e5f282011-03-17 14:08:551652 options, args = parser.parse_args(argv)
[email protected]cf6a5d22015-04-09 22:02:001653 auth_config = auth.extract_auth_config_from_options(options)
[email protected]92c30092014-04-15 00:30:371654
[email protected]899e1c12011-04-07 17:03:181655 if options.verbose >= 2:
[email protected]7444c502011-02-09 14:02:111656 logging.basicConfig(level=logging.DEBUG)
[email protected]899e1c12011-04-07 17:03:181657 elif options.verbose:
1658 logging.basicConfig(level=logging.INFO)
1659 else:
1660 logging.basicConfig(level=logging.ERROR)
[email protected]92c30092014-04-15 00:30:371661
1662 if options.rietveld_email and options.rietveld_email_file:
1663 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1664 "can be passed to this program.")
[email protected]7b654f52014-04-15 04:02:321665
[email protected]92c30092014-04-15 00:30:371666 if options.rietveld_email_file:
1667 with open(options.rietveld_email_file, "rb") as f:
1668 options.rietveld_email = f.read().strip()
1669
[email protected]5c8c6de2011-03-18 16:20:181670 change_class, files = load_files(options, args)
1671 if not change_class:
1672 parser.error('For unversioned directory, <files> is not optional.')
[email protected]899e1c12011-04-07 17:03:181673 logging.info('Found %d file(s).' % len(files))
[email protected]92c30092014-04-15 00:30:371674
[email protected]239f4112011-06-03 20:08:231675 rietveld_obj = None
1676 if options.rietveld_url:
[email protected]92c30092014-04-15 00:30:371677 # The empty password is permitted: '' is not None.
[email protected]7b654f52014-04-15 04:02:321678 if options.rietveld_private_key_file:
[email protected]92c30092014-04-15 00:30:371679 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1680 options.rietveld_url,
1681 options.rietveld_email,
1682 options.rietveld_private_key_file)
1683 else:
[email protected]7b654f52014-04-15 04:02:321684 rietveld_obj = rietveld.CachingRietveld(
1685 options.rietveld_url,
[email protected]cf6a5d22015-04-09 22:02:001686 auth_config,
1687 options.rietveld_email)
[email protected]720fd7a2013-04-24 04:13:501688 if options.rietveld_fetch:
1689 assert options.issue
1690 props = rietveld_obj.get_issue_properties(options.issue, False)
1691 options.author = props['owner_email']
1692 options.description = props['description']
1693 logging.info('Got author: "%s"', options.author)
1694 logging.info('Got description: """\n%s\n"""', options.description)
[email protected]f7d31f52014-01-03 20:14:461695 if options.trybot_json:
1696 with open(options.trybot_json, 'w') as f:
1697 # Python's sets aren't JSON-encodable, so we convert them to lists here.
1698 class SetEncoder(json.JSONEncoder):
1699 # pylint: disable=E0202
1700 def default(self, obj):
1701 if isinstance(obj, set):
1702 return sorted(obj)
1703 return json.JSONEncoder.default(self, obj)
1704 change = change_class(options.name,
1705 options.description,
1706 options.root,
1707 files,
1708 options.issue,
1709 options.patchset,
[email protected]ea84ef12014-04-30 19:55:121710 options.author,
1711 upstream=options.upstream)
[email protected]f7d31f52014-01-03 20:14:461712 trybots = DoGetTrySlaves(
1713 change,
1714 change.LocalPaths(),
1715 change.RepositoryRoot(),
1716 None,
1717 None,
1718 options.verbose,
1719 sys.stdout)
1720 json.dump(trybots, f, cls=SetEncoder)
[email protected]899e1c12011-04-07 17:03:181721 try:
[email protected]8a4a2bc2013-03-08 08:13:201722 with canned_check_filter(options.skip_canned):
1723 results = DoPresubmitChecks(
1724 change_class(options.name,
1725 options.description,
1726 options.root,
1727 files,
1728 options.issue,
1729 options.patchset,
[email protected]ea84ef12014-04-30 19:55:121730 options.author,
1731 upstream=options.upstream),
[email protected]8a4a2bc2013-03-08 08:13:201732 options.commit,
1733 options.verbose,
1734 sys.stdout,
1735 sys.stdin,
1736 options.default_presubmit,
1737 options.may_prompt,
1738 rietveld_obj)
[email protected]899e1c12011-04-07 17:03:181739 return not results.should_continue()
[email protected]8a4a2bc2013-03-08 08:13:201740 except NonexistantCannedCheckFilter, e:
1741 print >> sys.stderr, (
1742 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1743 return 2
[email protected]899e1c12011-04-07 17:03:181744 except PresubmitFailure, e:
1745 print >> sys.stderr, e
1746 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1747 print >> sys.stderr, 'If all fails, contact maruel@'
1748 return 2
[email protected]fb2b8eb2009-04-23 21:03:421749
1750
1751if __name__ == '__main__':
[email protected]35625c72011-03-23 17:34:021752 fix_encoding.fix_encoding()
[email protected]013731e2015-02-26 18:28:431753 try:
1754 sys.exit(main())
1755 except KeyboardInterrupt:
1756 sys.stderr.write('interrupted\n')
1757 sys.exit(1)