blob: 2b01bc356401cea34d7634b6ab87d3ba120a893b [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[\\\/].*",
[email protected]b9658c32015-10-06 10:50:13248 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
249 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
[email protected]3410d912009-06-09 20:56:16250 # Output directories (just in case)
251 r".*\bDebug[\\\/].*",
252 r".*\bRelease[\\\/].*",
253 r".*\bxcodebuild[\\\/].*",
[email protected]c1c96352013-10-09 19:50:27254 r".*\bout[\\\/].*",
[email protected]3410d912009-06-09 20:56:16255 # All caps files like README and LICENCE.
[email protected]ab05d582011-02-09 23:41:22256 r".*\b[A-Z0-9_]{2,}$",
[email protected]df1595a2009-06-11 02:00:13257 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
[email protected]5d0dc432011-01-03 02:40:37258 r"(|.*[\\\/])\.git[\\\/].*",
259 r"(|.*[\\\/])\.svn[\\\/].*",
[email protected]7ccb4bb2011-11-07 20:26:20260 # There is no point in processing a patch file.
261 r".+\.diff$",
262 r".+\.patch$",
[email protected]3410d912009-06-09 20:56:16263 )
264
[email protected]cc73ad62011-07-06 17:39:26265 def __init__(self, change, presubmit_path, is_committing,
[email protected]239f4112011-06-03 20:08:23266 rietveld_obj, verbose):
[email protected]fb2b8eb2009-04-23 21:03:42267 """Builds an InputApi object.
268
269 Args:
[email protected]4ff922a2009-06-12 20:20:19270 change: A presubmit.Change object.
[email protected]fb2b8eb2009-04-23 21:03:42271 presubmit_path: The path to the presubmit script being processed.
[email protected]d7dccf52009-06-06 18:51:58272 is_committing: True if the change is about to be committed.
[email protected]239f4112011-06-03 20:08:23273 rietveld_obj: rietveld.Rietveld client object
[email protected]fb2b8eb2009-04-23 21:03:42274 """
[email protected]9711bba2009-05-22 23:51:39275 # Version number of the presubmit_support script.
276 self.version = [int(x) for x in __version__.split('.')]
[email protected]fb2b8eb2009-04-23 21:03:42277 self.change = change
[email protected]d7dccf52009-06-06 18:51:58278 self.is_committing = is_committing
[email protected]239f4112011-06-03 20:08:23279 self.rietveld = rietveld_obj
[email protected]cab38e92011-04-09 00:30:51280 # TBD
281 self.host_url = 'http://codereview.chromium.org'
282 if self.rietveld:
[email protected]239f4112011-06-03 20:08:23283 self.host_url = self.rietveld.url
[email protected]fb2b8eb2009-04-23 21:03:42284
285 # We expose various modules and functions as attributes of the input_api
286 # so that presubmit scripts don't have to import them.
287 self.basename = os.path.basename
288 self.cPickle = cPickle
[email protected]e72c5f52013-04-16 00:36:40289 self.cpplint = cpplint
[email protected]fb2b8eb2009-04-23 21:03:42290 self.cStringIO = cStringIO
[email protected]17cc2442012-10-17 21:12:09291 self.glob = glob.glob
[email protected]fb11c7b2010-03-18 18:22:14292 self.json = json
[email protected]6fba34d2011-06-02 13:45:12293 self.logging = logging.getLogger('PRESUBMIT')
[email protected]2b5ce562011-03-31 16:15:44294 self.os_listdir = os.listdir
295 self.os_walk = os.walk
[email protected]fb2b8eb2009-04-23 21:03:42296 self.os_path = os.path
[email protected]bd0cace2014-10-02 23:23:46297 self.os_stat = os.stat
[email protected]fb2b8eb2009-04-23 21:03:42298 self.pickle = pickle
299 self.marshal = marshal
300 self.re = re
301 self.subprocess = subprocess
302 self.tempfile = tempfile
[email protected]0d1bdea2011-03-24 22:54:38303 self.time = time
[email protected]d7dccf52009-06-06 18:51:58304 self.traceback = traceback
[email protected]1487d532009-06-06 00:22:57305 self.unittest = unittest
[email protected]fb2b8eb2009-04-23 21:03:42306 self.urllib2 = urllib2
307
[email protected]c0b22972009-06-25 16:19:14308 # To easily fork python.
309 self.python_executable = sys.executable
310 self.environ = os.environ
311
[email protected]fb2b8eb2009-04-23 21:03:42312 # InputApi.platform is the platform you're currently running on.
313 self.platform = sys.platform
314
[email protected]0af3bb32015-06-12 20:44:35315 self.cpu_count = multiprocessing.cpu_count()
316
[email protected]d61a4952015-07-01 23:21:26317 # this is done here because in RunTests, the current working directory has
318 # changed, which causes Pool() to explode fantastically when run on windows
319 # (because it tries to load the __main__ module, which imports lots of
320 # things relative to the current working directory).
321 self._run_tests_pool = multiprocessing.Pool(self.cpu_count)
322
[email protected]fb2b8eb2009-04-23 21:03:42323 # The local path of the currently-being-processed presubmit script.
[email protected]3d235242009-05-15 12:40:48324 self._current_presubmit_path = os.path.dirname(presubmit_path)
[email protected]fb2b8eb2009-04-23 21:03:42325
326 # We carry the canned checks so presubmit scripts can easily use them.
327 self.canned_checks = presubmit_canned_checks
328
[email protected]2a009622011-03-01 02:43:31329 # TODO(dpranke): figure out a list of all approved owners for a repo
330 # in order to be able to handle wildcard OWNERS files?
331 self.owners_db = owners.Database(change.RepositoryRoot(),
[email protected]17cc2442012-10-17 21:12:09332 fopen=file, os_path=self.os_path, glob=self.glob)
[email protected]899e1c12011-04-07 17:03:18333 self.verbose = verbose
[email protected]bc117312013-04-20 03:57:56334 self.Command = CommandData
[email protected]2a009622011-03-01 02:43:31335
[email protected]e72c5f52013-04-16 00:36:40336 # Replace <hash_map> and <hash_set> as headers that need to be included
[email protected]18278522013-06-11 22:42:32337 # with "base/containers/hash_tables.h" instead.
[email protected]e72c5f52013-04-16 00:36:40338 # Access to a protected member _XX of a client class
339 # pylint: disable=W0212
340 self.cpplint._re_pattern_templates = [
[email protected]18278522013-06-11 22:42:32341 (a, b, 'base/containers/hash_tables.h')
[email protected]e72c5f52013-04-16 00:36:40342 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
343 for (a, b, header) in cpplint._re_pattern_templates
344 ]
345
[email protected]fb2b8eb2009-04-23 21:03:42346 def PresubmitLocalPath(self):
347 """Returns the local path of the presubmit script currently being run.
348
349 This is useful if you don't want to hard-code absolute paths in the
350 presubmit script. For example, It can be used to find another file
351 relative to the PRESUBMIT.py script, so the whole tree can be branched and
352 the presubmit script still works, without editing its content.
353 """
[email protected]3d235242009-05-15 12:40:48354 return self._current_presubmit_path
[email protected]fb2b8eb2009-04-23 21:03:42355
[email protected]1e08c002009-05-28 19:09:33356 def DepotToLocalPath(self, depot_path):
[email protected]fb2b8eb2009-04-23 21:03:42357 """Translate a depot path to a local path (relative to client root).
358
359 Args:
360 Depot path as a string.
361
362 Returns:
363 The local path of the depot path under the user's current client, or None
364 if the file is not mapped.
365
366 Remember to check for the None case and show an appropriate error!
367 """
[email protected]d579fcf2011-12-13 20:36:03368 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
369 ).get('Path')
[email protected]fb2b8eb2009-04-23 21:03:42370
[email protected]1e08c002009-05-28 19:09:33371 def LocalToDepotPath(self, local_path):
[email protected]fb2b8eb2009-04-23 21:03:42372 """Translate a local path to a depot path.
373
374 Args:
375 Local path (relative to current directory, or absolute) as a string.
376
377 Returns:
378 The depot path (SVN URL) of the file if mapped, otherwise None.
379 """
[email protected]d579fcf2011-12-13 20:36:03380 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
381 ).get('URL')
[email protected]fb2b8eb2009-04-23 21:03:42382
[email protected]5538e022011-05-12 17:53:16383 def AffectedFiles(self, include_dirs=False, include_deletes=True,
384 file_filter=None):
[email protected]fb2b8eb2009-04-23 21:03:42385 """Same as input_api.change.AffectedFiles() except only lists files
386 (and optionally directories) in the same directory as the current presubmit
387 script, or subdirectories thereof.
388 """
[email protected]3d235242009-05-15 12:40:48389 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
[email protected]fb2b8eb2009-04-23 21:03:42390 if len(dir_with_slash) == 1:
391 dir_with_slash = ''
[email protected]5538e022011-05-12 17:53:16392
[email protected]4661e0c2009-06-04 00:45:26393 return filter(
394 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
[email protected]5538e022011-05-12 17:53:16395 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
[email protected]fb2b8eb2009-04-23 21:03:42396
397 def LocalPaths(self, include_dirs=False):
398 """Returns local paths of input_api.AffectedFiles()."""
[email protected]2f64f782014-04-25 00:12:33399 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
400 logging.debug("LocalPaths: %s", paths)
401 return paths
[email protected]fb2b8eb2009-04-23 21:03:42402
403 def AbsoluteLocalPaths(self, include_dirs=False):
404 """Returns absolute local paths of input_api.AffectedFiles()."""
405 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
406
407 def ServerPaths(self, include_dirs=False):
408 """Returns server paths of input_api.AffectedFiles()."""
409 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
410
[email protected]77c4f0f2009-05-29 18:53:04411 def AffectedTextFiles(self, include_deletes=None):
[email protected]fb2b8eb2009-04-23 21:03:42412 """Same as input_api.change.AffectedTextFiles() except only lists files
413 in the same directory as the current presubmit script, or subdirectories
414 thereof.
[email protected]fb2b8eb2009-04-23 21:03:42415 """
[email protected]77c4f0f2009-05-29 18:53:04416 if include_deletes is not None:
[email protected]cb2985f2010-11-03 14:08:31417 warn("AffectedTextFiles(include_deletes=%s)"
418 " is deprecated and ignored" % str(include_deletes),
419 category=DeprecationWarning,
420 stacklevel=2)
[email protected]77c4f0f2009-05-29 18:53:04421 return filter(lambda x: x.IsTextFile(),
422 self.AffectedFiles(include_dirs=False, include_deletes=False))
[email protected]fb2b8eb2009-04-23 21:03:42423
[email protected]3410d912009-06-09 20:56:16424 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
425 """Filters out files that aren't considered "source file".
426
427 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
428 and InputApi.DEFAULT_BLACK_LIST is used respectively.
429
430 The lists will be compiled as regular expression and
431 AffectedFile.LocalPath() needs to pass both list.
432
433 Note: Copy-paste this function to suit your needs or use a lambda function.
434 """
[email protected]cb2985f2010-11-03 14:08:31435 def Find(affected_file, items):
[email protected]ab05d582011-02-09 23:41:22436 local_path = affected_file.LocalPath()
[email protected]cb2985f2010-11-03 14:08:31437 for item in items:
[email protected]df1595a2009-06-11 02:00:13438 if self.re.match(item, local_path):
439 logging.debug("%s matched %s" % (item, local_path))
[email protected]3410d912009-06-09 20:56:16440 return True
441 return False
442 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
443 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
444
445 def AffectedSourceFiles(self, source_file):
446 """Filter the list of AffectedTextFiles by the function source_file.
447
448 If source_file is None, InputApi.FilterSourceFile() is used.
449 """
450 if not source_file:
451 source_file = self.FilterSourceFile
452 return filter(source_file, self.AffectedTextFiles())
453
454 def RightHandSideLines(self, source_file_filter=None):
[email protected]fb2b8eb2009-04-23 21:03:42455 """An iterator over all text lines in "new" version of changed files.
456
457 Only lists lines from new or modified text files in the change that are
458 contained by the directory of the currently executing presubmit script.
459
460 This is useful for doing line-by-line regex checks, like checking for
461 trailing whitespace.
462
463 Yields:
464 a 3 tuple:
465 the AffectedFile instance of the current file;
466 integer line number (1-based); and
467 the contents of the line as a string.
[email protected]1487d532009-06-06 00:22:57468
[email protected]2a3ab7e2011-04-27 22:06:27469 Note: The carriage return (LF or CR) is stripped off.
[email protected]fb2b8eb2009-04-23 21:03:42470 """
[email protected]3410d912009-06-09 20:56:16471 files = self.AffectedSourceFiles(source_file_filter)
[email protected]cb2985f2010-11-03 14:08:31472 return _RightHandSideLinesImpl(files)
[email protected]fb2b8eb2009-04-23 21:03:42473
[email protected]e3608df2009-11-10 20:22:57474 def ReadFile(self, file_item, mode='r'):
[email protected]44a17ad2009-06-08 14:14:35475 """Reads an arbitrary file.
[email protected]da8cddd2009-08-13 00:25:55476
[email protected]44a17ad2009-06-08 14:14:35477 Deny reading anything outside the repository.
478 """
[email protected]e3608df2009-11-10 20:22:57479 if isinstance(file_item, AffectedFile):
480 file_item = file_item.AbsoluteLocalPath()
481 if not file_item.startswith(self.change.RepositoryRoot()):
[email protected]44a17ad2009-06-08 14:14:35482 raise IOError('Access outside the repository root is denied.')
[email protected]5aeb7dd2009-11-17 18:09:01483 return gclient_utils.FileRead(file_item, mode)
[email protected]44a17ad2009-06-08 14:14:35484
[email protected]cc73ad62011-07-06 17:39:26485 @property
486 def tbr(self):
487 """Returns if a change is TBR'ed."""
488 return 'TBR' in self.change.tags
489
[email protected]ffeb2f32013-12-03 13:55:22490 def RunTests(self, tests_mix, parallel=True):
[email protected]bc117312013-04-20 03:57:56491 tests = []
492 msgs = []
493 for t in tests_mix:
494 if isinstance(t, OutputApi.PresubmitResult):
495 msgs.append(t)
496 else:
497 assert issubclass(t.message, _PresubmitResult)
498 tests.append(t)
[email protected]ffeb2f32013-12-03 13:55:22499 if self.verbose:
500 t.info = _PresubmitNotifyResult
[email protected]5678d332013-05-18 01:34:14501 if len(tests) > 1 and parallel:
[email protected]bc117312013-04-20 03:57:56502 # async recipe works around multiprocessing bug handling Ctrl-C
[email protected]d61a4952015-07-01 23:21:26503 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
[email protected]bc117312013-04-20 03:57:56504 else:
505 msgs.extend(map(CallCommand, tests))
506 return [m for m in msgs if m]
507
[email protected]fb2b8eb2009-04-23 21:03:42508
[email protected]ff526192013-06-10 19:30:26509class _DiffCache(object):
510 """Caches diffs retrieved from a particular SCM."""
[email protected]ea84ef12014-04-30 19:55:12511 def __init__(self, upstream=None):
512 """Stores the upstream revision against which all diffs will be computed."""
513 self._upstream = upstream
[email protected]ff526192013-06-10 19:30:26514
515 def GetDiff(self, path, local_root):
516 """Get the diff for a particular path."""
517 raise NotImplementedError()
518
519
520class _SvnDiffCache(_DiffCache):
521 """DiffCache implementation for subversion."""
[email protected]ea84ef12014-04-30 19:55:12522 def __init__(self, *args, **kwargs):
523 super(_SvnDiffCache, self).__init__(*args, **kwargs)
[email protected]ff526192013-06-10 19:30:26524 self._diffs_by_file = {}
525
526 def GetDiff(self, path, local_root):
527 if path not in self._diffs_by_file:
528 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
529 False, None)
530 return self._diffs_by_file[path]
531
532
533class _GitDiffCache(_DiffCache):
534 """DiffCache implementation for git; gets all file diffs at once."""
[email protected]ea84ef12014-04-30 19:55:12535 def __init__(self, upstream):
536 super(_GitDiffCache, self).__init__(upstream=upstream)
[email protected]ff526192013-06-10 19:30:26537 self._diffs_by_file = None
538
539 def GetDiff(self, path, local_root):
540 if not self._diffs_by_file:
541 # Compute a single diff for all files and parse the output; should
542 # with git this is much faster than computing one diff for each file.
543 diffs = {}
544
545 # Don't specify any filenames below, because there are command line length
546 # limits on some platforms and GenerateDiff would fail.
[email protected]ea84ef12014-04-30 19:55:12547 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
548 branch=self._upstream)
[email protected]ff526192013-06-10 19:30:26549
550 # This regex matches the path twice, separated by a space. Note that
551 # filename itself may contain spaces.
552 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
553 current_diff = []
554 keep_line_endings = True
555 for x in unified_diff.splitlines(keep_line_endings):
556 match = file_marker.match(x)
557 if match:
558 # Marks the start of a new per-file section.
559 diffs[match.group('filename')] = current_diff = [x]
560 elif x.startswith('diff --git'):
561 raise PresubmitFailure('Unexpected diff line: %s' % x)
562 else:
563 current_diff.append(x)
564
565 self._diffs_by_file = dict(
566 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
567
568 if path not in self._diffs_by_file:
569 raise PresubmitFailure(
570 'Unified diff did not contain entry for file %s' % path)
571
572 return self._diffs_by_file[path]
573
574
[email protected]fb2b8eb2009-04-23 21:03:42575class AffectedFile(object):
576 """Representation of a file in a change."""
[email protected]ff526192013-06-10 19:30:26577
578 DIFF_CACHE = _DiffCache
579
[email protected]b17b55b2010-11-03 14:42:37580 # Method could be a function
581 # pylint: disable=R0201
[email protected]ea84ef12014-04-30 19:55:12582 def __init__(self, path, action, repository_root, diff_cache):
[email protected]15bdffa2009-05-29 11:16:29583 self._path = path
584 self._action = action
[email protected]4ff922a2009-06-12 20:20:19585 self._local_root = repository_root
[email protected]15bdffa2009-05-29 11:16:29586 self._is_directory = None
587 self._properties = {}
[email protected]2a3ab7e2011-04-27 22:06:27588 self._cached_changed_contents = None
589 self._cached_new_contents = None
[email protected]ea84ef12014-04-30 19:55:12590 self._diff_cache = diff_cache
[email protected]5d0dc432011-01-03 02:40:37591 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
[email protected]fb2b8eb2009-04-23 21:03:42592
593 def ServerPath(self):
594 """Returns a path string that identifies the file in the SCM system.
595
596 Returns the empty string if the file does not exist in SCM.
597 """
[email protected]ff526192013-06-10 19:30:26598 return ''
[email protected]fb2b8eb2009-04-23 21:03:42599
600 def LocalPath(self):
601 """Returns the path of this file on the local disk relative to client root.
602 """
[email protected]15bdffa2009-05-29 11:16:29603 return normpath(self._path)
[email protected]fb2b8eb2009-04-23 21:03:42604
605 def AbsoluteLocalPath(self):
606 """Returns the absolute path of this file on the local disk.
607 """
[email protected]8e416c82009-10-06 04:30:44608 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
[email protected]fb2b8eb2009-04-23 21:03:42609
610 def IsDirectory(self):
611 """Returns true if this object is a directory."""
[email protected]15bdffa2009-05-29 11:16:29612 if self._is_directory is None:
613 path = self.AbsoluteLocalPath()
614 self._is_directory = (os.path.exists(path) and
615 os.path.isdir(path))
616 return self._is_directory
[email protected]fb2b8eb2009-04-23 21:03:42617
618 def Action(self):
619 """Returns the action on this opened file, e.g. A, M, D, etc."""
[email protected]dbbeedc2009-05-22 20:26:17620 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
621 # different for other SCM.
[email protected]15bdffa2009-05-29 11:16:29622 return self._action
[email protected]fb2b8eb2009-04-23 21:03:42623
[email protected]dbbeedc2009-05-22 20:26:17624 def Property(self, property_name):
625 """Returns the specified SCM property of this file, or None if no such
626 property.
627 """
[email protected]15bdffa2009-05-29 11:16:29628 return self._properties.get(property_name, None)
[email protected]dbbeedc2009-05-22 20:26:17629
[email protected]1e08c002009-05-28 19:09:33630 def IsTextFile(self):
[email protected]77c4f0f2009-05-29 18:53:04631 """Returns True if the file is a text file and not a binary file.
[email protected]da8cddd2009-08-13 00:25:55632
[email protected]77c4f0f2009-05-29 18:53:04633 Deleted files are not text file."""
[email protected]1e08c002009-05-28 19:09:33634 raise NotImplementedError() # Implement when needed
635
[email protected]fb2b8eb2009-04-23 21:03:42636 def NewContents(self):
637 """Returns an iterator over the lines in the new version of file.
638
639 The new version is the file in the user's workspace, i.e. the "right hand
640 side".
641
642 Contents will be empty if the file is a directory or does not exist.
[email protected]2a3ab7e2011-04-27 22:06:27643 Note: The carriage returns (LF or CR) are stripped off.
[email protected]fb2b8eb2009-04-23 21:03:42644 """
[email protected]2a3ab7e2011-04-27 22:06:27645 if self._cached_new_contents is None:
646 self._cached_new_contents = []
647 if not self.IsDirectory():
648 try:
649 self._cached_new_contents = gclient_utils.FileRead(
650 self.AbsoluteLocalPath(), 'rU').splitlines()
651 except IOError:
652 pass # File not found? That's fine; maybe it was deleted.
653 return self._cached_new_contents[:]
[email protected]fb2b8eb2009-04-23 21:03:42654
[email protected]ab05d582011-02-09 23:41:22655 def ChangedContents(self):
656 """Returns a list of tuples (line number, line text) of all new lines.
657
658 This relies on the scm diff output describing each changed code section
659 with a line of the form
660
661 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
662 """
[email protected]2a3ab7e2011-04-27 22:06:27663 if self._cached_changed_contents is not None:
664 return self._cached_changed_contents[:]
665 self._cached_changed_contents = []
[email protected]ab05d582011-02-09 23:41:22666 line_num = 0
667
668 if self.IsDirectory():
669 return []
670
671 for line in self.GenerateScmDiff().splitlines():
672 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
673 if m:
674 line_num = int(m.groups(1)[0])
675 continue
676 if line.startswith('+') and not line.startswith('++'):
[email protected]2a3ab7e2011-04-27 22:06:27677 self._cached_changed_contents.append((line_num, line[1:]))
[email protected]ab05d582011-02-09 23:41:22678 if not line.startswith('-'):
679 line_num += 1
[email protected]2a3ab7e2011-04-27 22:06:27680 return self._cached_changed_contents[:]
[email protected]ab05d582011-02-09 23:41:22681
[email protected]5de13972009-06-10 18:16:06682 def __str__(self):
683 return self.LocalPath()
684
[email protected]ab05d582011-02-09 23:41:22685 def GenerateScmDiff(self):
[email protected]ff526192013-06-10 19:30:26686 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
[email protected]fb2b8eb2009-04-23 21:03:42687
[email protected]58407af2011-04-12 23:15:57688
[email protected]dbbeedc2009-05-22 20:26:17689class SvnAffectedFile(AffectedFile):
690 """Representation of a file in a change out of a Subversion checkout."""
[email protected]b17b55b2010-11-03 14:42:37691 # Method 'NNN' is abstract in class 'NNN' but is not overridden
692 # pylint: disable=W0223
[email protected]dbbeedc2009-05-22 20:26:17693
[email protected]ff526192013-06-10 19:30:26694 DIFF_CACHE = _SvnDiffCache
695
[email protected]15bdffa2009-05-29 11:16:29696 def __init__(self, *args, **kwargs):
697 AffectedFile.__init__(self, *args, **kwargs)
698 self._server_path = None
699 self._is_text_file = None
[email protected]15bdffa2009-05-29 11:16:29700
[email protected]dbbeedc2009-05-22 20:26:17701 def ServerPath(self):
[email protected]15bdffa2009-05-29 11:16:29702 if self._server_path is None:
[email protected]d579fcf2011-12-13 20:36:03703 self._server_path = scm.SVN.CaptureLocalInfo(
704 [self.LocalPath()], self._local_root).get('URL', '')
[email protected]15bdffa2009-05-29 11:16:29705 return self._server_path
[email protected]dbbeedc2009-05-22 20:26:17706
707 def IsDirectory(self):
[email protected]15bdffa2009-05-29 11:16:29708 if self._is_directory is None:
709 path = self.AbsoluteLocalPath()
[email protected]dbbeedc2009-05-22 20:26:17710 if os.path.exists(path):
711 # Retrieve directly from the file system; it is much faster than
712 # querying subversion, especially on Windows.
[email protected]15bdffa2009-05-29 11:16:29713 self._is_directory = os.path.isdir(path)
[email protected]dbbeedc2009-05-22 20:26:17714 else:
[email protected]d579fcf2011-12-13 20:36:03715 self._is_directory = scm.SVN.CaptureLocalInfo(
716 [self.LocalPath()], self._local_root
717 ).get('Node Kind') in ('dir', 'directory')
[email protected]15bdffa2009-05-29 11:16:29718 return self._is_directory
[email protected]dbbeedc2009-05-22 20:26:17719
720 def Property(self, property_name):
[email protected]15bdffa2009-05-29 11:16:29721 if not property_name in self._properties:
[email protected]5aeb7dd2009-11-17 18:09:01722 self._properties[property_name] = scm.SVN.GetFileProperty(
[email protected]d579fcf2011-12-13 20:36:03723 self.LocalPath(), property_name, self._local_root).rstrip()
[email protected]15bdffa2009-05-29 11:16:29724 return self._properties[property_name]
[email protected]dbbeedc2009-05-22 20:26:17725
[email protected]1e08c002009-05-28 19:09:33726 def IsTextFile(self):
[email protected]15bdffa2009-05-29 11:16:29727 if self._is_text_file is None:
728 if self.Action() == 'D':
729 # A deleted file is not a text file.
730 self._is_text_file = False
731 elif self.IsDirectory():
732 self._is_text_file = False
733 else:
[email protected]d579fcf2011-12-13 20:36:03734 mime_type = scm.SVN.GetFileProperty(
735 self.LocalPath(), 'svn:mime-type', self._local_root)
[email protected]15bdffa2009-05-29 11:16:29736 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
737 return self._is_text_file
[email protected]1e08c002009-05-28 19:09:33738
[email protected]dbbeedc2009-05-22 20:26:17739
[email protected]c70a2202009-06-17 12:55:10740class GitAffectedFile(AffectedFile):
741 """Representation of a file in a change out of a git checkout."""
[email protected]b17b55b2010-11-03 14:42:37742 # Method 'NNN' is abstract in class 'NNN' but is not overridden
743 # pylint: disable=W0223
[email protected]c70a2202009-06-17 12:55:10744
[email protected]ff526192013-06-10 19:30:26745 DIFF_CACHE = _GitDiffCache
746
[email protected]c70a2202009-06-17 12:55:10747 def __init__(self, *args, **kwargs):
748 AffectedFile.__init__(self, *args, **kwargs)
749 self._server_path = None
750 self._is_text_file = None
[email protected]c70a2202009-06-17 12:55:10751
752 def ServerPath(self):
753 if self._server_path is None:
[email protected]899e1c12011-04-07 17:03:18754 raise NotImplementedError('TODO(maruel) Implement.')
[email protected]c70a2202009-06-17 12:55:10755 return self._server_path
756
757 def IsDirectory(self):
758 if self._is_directory is None:
759 path = self.AbsoluteLocalPath()
760 if os.path.exists(path):
761 # Retrieve directly from the file system; it is much faster than
762 # querying subversion, especially on Windows.
763 self._is_directory = os.path.isdir(path)
764 else:
[email protected]c70a2202009-06-17 12:55:10765 self._is_directory = False
766 return self._is_directory
767
768 def Property(self, property_name):
769 if not property_name in self._properties:
[email protected]899e1c12011-04-07 17:03:18770 raise NotImplementedError('TODO(maruel) Implement.')
[email protected]c70a2202009-06-17 12:55:10771 return self._properties[property_name]
772
773 def IsTextFile(self):
774 if self._is_text_file is None:
775 if self.Action() == 'D':
776 # A deleted file is not a text file.
777 self._is_text_file = False
778 elif self.IsDirectory():
779 self._is_text_file = False
780 else:
[email protected]c70a2202009-06-17 12:55:10781 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
782 return self._is_text_file
783
[email protected]c1938752011-04-12 23:11:13784
[email protected]4ff922a2009-06-12 20:20:19785class Change(object):
[email protected]6ebe68a2009-05-27 23:43:40786 """Describe a change.
787
788 Used directly by the presubmit scripts to query the current change being
789 tested.
[email protected]da8cddd2009-08-13 00:25:55790
[email protected]6ebe68a2009-05-27 23:43:40791 Instance members:
[email protected]ff526192013-06-10 19:30:26792 tags: Dictionary of KEY=VALUE pairs found in the change description.
[email protected]6ebe68a2009-05-27 23:43:40793 self.KEY: equivalent to tags['KEY']
794 """
795
[email protected]4ff922a2009-06-12 20:20:19796 _AFFECTED_FILES = AffectedFile
797
[email protected]6ebe68a2009-05-27 23:43:40798 # Matches key/value (or "tag") lines in changelist descriptions.
[email protected]428342a2011-11-10 15:46:33799 TAG_LINE_RE = re.compile(
[email protected]c6f60e82013-04-19 17:01:57800 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
[email protected]c1938752011-04-12 23:11:13801 scm = ''
[email protected]fb2b8eb2009-04-23 21:03:42802
[email protected]58407af2011-04-12 23:15:57803 def __init__(
[email protected]ea84ef12014-04-30 19:55:12804 self, name, description, local_root, files, issue, patchset, author,
805 upstream=None):
[email protected]4ff922a2009-06-12 20:20:19806 if files is None:
807 files = []
808 self._name = name
[email protected]8e416c82009-10-06 04:30:44809 # Convert root into an absolute path.
810 self._local_root = os.path.abspath(local_root)
[email protected]ea84ef12014-04-30 19:55:12811 self._upstream = upstream
[email protected]4ff922a2009-06-12 20:20:19812 self.issue = issue
813 self.patchset = patchset
[email protected]58407af2011-04-12 23:15:57814 self.author_email = author
[email protected]fb2b8eb2009-04-23 21:03:42815
[email protected]b5cded62014-03-25 17:47:57816 self._full_description = ''
[email protected]fb2b8eb2009-04-23 21:03:42817 self.tags = {}
[email protected]b5cded62014-03-25 17:47:57818 self._description_without_tags = ''
819 self.SetDescriptionText(description)
[email protected]fb2b8eb2009-04-23 21:03:42820
[email protected]e085d812011-10-10 19:49:15821 assert all(
822 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
823
[email protected]ea84ef12014-04-30 19:55:12824 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
[email protected]6ebe68a2009-05-27 23:43:40825 self._affected_files = [
[email protected]ff526192013-06-10 19:30:26826 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
827 for action, path in files
[email protected]dbbeedc2009-05-22 20:26:17828 ]
[email protected]fb2b8eb2009-04-23 21:03:42829
[email protected]92022ec2009-06-11 01:59:28830 def Name(self):
[email protected]fb2b8eb2009-04-23 21:03:42831 """Returns the change name."""
[email protected]6ebe68a2009-05-27 23:43:40832 return self._name
[email protected]fb2b8eb2009-04-23 21:03:42833
[email protected]fb2b8eb2009-04-23 21:03:42834 def DescriptionText(self):
835 """Returns the user-entered changelist description, minus tags.
836
837 Any line in the user-provided description starting with e.g. "FOO="
838 (whitespace permitted before and around) is considered a tag line. Such
839 lines are stripped out of the description this function returns.
840 """
[email protected]6ebe68a2009-05-27 23:43:40841 return self._description_without_tags
[email protected]fb2b8eb2009-04-23 21:03:42842
843 def FullDescriptionText(self):
844 """Returns the complete changelist description including tags."""
[email protected]6ebe68a2009-05-27 23:43:40845 return self._full_description
[email protected]fb2b8eb2009-04-23 21:03:42846
[email protected]b5cded62014-03-25 17:47:57847 def SetDescriptionText(self, description):
848 """Sets the full description text (including tags) to |description|.
[email protected]92c30092014-04-15 00:30:37849
[email protected]b5cded62014-03-25 17:47:57850 Also updates the list of tags."""
851 self._full_description = description
852
853 # From the description text, build up a dictionary of key/value pairs
854 # plus the description minus all key/value or "tag" lines.
855 description_without_tags = []
856 self.tags = {}
857 for line in self._full_description.splitlines():
858 m = self.TAG_LINE_RE.match(line)
859 if m:
860 self.tags[m.group('key')] = m.group('value')
861 else:
862 description_without_tags.append(line)
863
864 # Change back to text and remove whitespace at end.
865 self._description_without_tags = (
866 '\n'.join(description_without_tags).rstrip())
867
[email protected]fb2b8eb2009-04-23 21:03:42868 def RepositoryRoot(self):
[email protected]92022ec2009-06-11 01:59:28869 """Returns the repository (checkout) root directory for this change,
870 as an absolute path.
871 """
[email protected]4ff922a2009-06-12 20:20:19872 return self._local_root
[email protected]fb2b8eb2009-04-23 21:03:42873
874 def __getattr__(self, attr):
[email protected]92022ec2009-06-11 01:59:28875 """Return tags directly as attributes on the object."""
876 if not re.match(r"^[A-Z_]*$", attr):
877 raise AttributeError(self, attr)
[email protected]e1a524f2009-05-27 14:43:46878 return self.tags.get(attr)
[email protected]fb2b8eb2009-04-23 21:03:42879
[email protected]40a3d0b2014-05-15 01:59:16880 def AllFiles(self, root=None):
881 """List all files under source control in the repo."""
882 raise NotImplementedError()
883
[email protected]5538e022011-05-12 17:53:16884 def AffectedFiles(self, include_dirs=False, include_deletes=True,
885 file_filter=None):
[email protected]fb2b8eb2009-04-23 21:03:42886 """Returns a list of AffectedFile instances for all files in the change.
887
888 Args:
889 include_deletes: If false, deleted files will be filtered out.
890 include_dirs: True to include directories in the list
[email protected]5538e022011-05-12 17:53:16891 file_filter: An additional filter to apply.
[email protected]fb2b8eb2009-04-23 21:03:42892
893 Returns:
894 [AffectedFile(path, action), AffectedFile(path, action)]
895 """
896 if include_dirs:
[email protected]6ebe68a2009-05-27 23:43:40897 affected = self._affected_files
[email protected]fb2b8eb2009-04-23 21:03:42898 else:
[email protected]6ebe68a2009-05-27 23:43:40899 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
[email protected]fb2b8eb2009-04-23 21:03:42900
[email protected]5538e022011-05-12 17:53:16901 affected = filter(file_filter, affected)
902
[email protected]fb2b8eb2009-04-23 21:03:42903 if include_deletes:
904 return affected
905 else:
906 return filter(lambda x: x.Action() != 'D', affected)
907
[email protected]77c4f0f2009-05-29 18:53:04908 def AffectedTextFiles(self, include_deletes=None):
909 """Return a list of the existing text files in a change."""
910 if include_deletes is not None:
[email protected]cb2985f2010-11-03 14:08:31911 warn("AffectedTextFiles(include_deletes=%s)"
912 " is deprecated and ignored" % str(include_deletes),
913 category=DeprecationWarning,
914 stacklevel=2)
[email protected]77c4f0f2009-05-29 18:53:04915 return filter(lambda x: x.IsTextFile(),
916 self.AffectedFiles(include_dirs=False, include_deletes=False))
[email protected]fb2b8eb2009-04-23 21:03:42917
918 def LocalPaths(self, include_dirs=False):
919 """Convenience function."""
920 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
921
922 def AbsoluteLocalPaths(self, include_dirs=False):
923 """Convenience function."""
924 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
925
926 def ServerPaths(self, include_dirs=False):
927 """Convenience function."""
928 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
929
930 def RightHandSideLines(self):
931 """An iterator over all text lines in "new" version of changed files.
932
933 Lists lines from new or modified text files in the change.
934
935 This is useful for doing line-by-line regex checks, like checking for
936 trailing whitespace.
937
938 Yields:
939 a 3 tuple:
940 the AffectedFile instance of the current file;
941 integer line number (1-based); and
942 the contents of the line as a string.
943 """
[email protected]cb2985f2010-11-03 14:08:31944 return _RightHandSideLinesImpl(
945 x for x in self.AffectedFiles(include_deletes=False)
946 if x.IsTextFile())
[email protected]fb2b8eb2009-04-23 21:03:42947
948
[email protected]4ff922a2009-06-12 20:20:19949class SvnChange(Change):
950 _AFFECTED_FILES = SvnAffectedFile
[email protected]c1938752011-04-12 23:11:13951 scm = 'svn'
952 _changelists = None
[email protected]6bd31702009-09-02 23:29:07953
954 def _GetChangeLists(self):
955 """Get all change lists."""
956 if self._changelists == None:
957 previous_cwd = os.getcwd()
958 os.chdir(self.RepositoryRoot())
[email protected]cb2985f2010-11-03 14:08:31959 # Need to import here to avoid circular dependency.
960 import gcl
[email protected]6bd31702009-09-02 23:29:07961 self._changelists = gcl.GetModifiedFiles()
962 os.chdir(previous_cwd)
963 return self._changelists
[email protected]da8cddd2009-08-13 00:25:55964
965 def GetAllModifiedFiles(self):
966 """Get all modified files."""
[email protected]6bd31702009-09-02 23:29:07967 changelists = self._GetChangeLists()
[email protected]da8cddd2009-08-13 00:25:55968 all_modified_files = []
969 for cl in changelists.values():
[email protected]6bd31702009-09-02 23:29:07970 all_modified_files.extend(
971 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
[email protected]da8cddd2009-08-13 00:25:55972 return all_modified_files
973
974 def GetModifiedFiles(self):
975 """Get modified files in the current CL."""
[email protected]6bd31702009-09-02 23:29:07976 changelists = self._GetChangeLists()
977 return [os.path.join(self.RepositoryRoot(), f[1])
978 for f in changelists[self.Name()]]
[email protected]da8cddd2009-08-13 00:25:55979
[email protected]40a3d0b2014-05-15 01:59:16980 def AllFiles(self, root=None):
981 """List all files under source control in the repo."""
982 root = root or self.RepositoryRoot()
983 return subprocess.check_output(
984 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
985
[email protected]4ff922a2009-06-12 20:20:19986
[email protected]c70a2202009-06-17 12:55:10987class GitChange(Change):
988 _AFFECTED_FILES = GitAffectedFile
[email protected]c1938752011-04-12 23:11:13989 scm = 'git'
[email protected]da8cddd2009-08-13 00:25:55990
[email protected]40a3d0b2014-05-15 01:59:16991 def AllFiles(self, root=None):
992 """List all files under source control in the repo."""
993 root = root or self.RepositoryRoot()
994 return subprocess.check_output(
995 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
996
[email protected]c70a2202009-06-17 12:55:10997
[email protected]4661e0c2009-06-04 00:45:26998def ListRelevantPresubmitFiles(files, root):
[email protected]fb2b8eb2009-04-23 21:03:42999 """Finds all presubmit files that apply to a given set of source files.
1000
[email protected]b1901a62010-06-16 00:18:471001 If inherit-review-settings-ok is present right under root, looks for
1002 PRESUBMIT.py in directories enclosing root.
1003
[email protected]fb2b8eb2009-04-23 21:03:421004 Args:
1005 files: An iterable container containing file paths.
[email protected]4661e0c2009-06-04 00:45:261006 root: Path where to stop searching.
[email protected]fb2b8eb2009-04-23 21:03:421007
1008 Return:
[email protected]4661e0c2009-06-04 00:45:261009 List of absolute paths of the existing PRESUBMIT.py scripts.
[email protected]fb2b8eb2009-04-23 21:03:421010 """
[email protected]b1901a62010-06-16 00:18:471011 files = [normpath(os.path.join(root, f)) for f in files]
1012
1013 # List all the individual directories containing files.
1014 directories = set([os.path.dirname(f) for f in files])
1015
1016 # Ignore root if inherit-review-settings-ok is present.
1017 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1018 root = None
1019
1020 # Collect all unique directories that may contain PRESUBMIT.py.
1021 candidates = set()
1022 for directory in directories:
1023 while True:
1024 if directory in candidates:
[email protected]fb2b8eb2009-04-23 21:03:421025 break
[email protected]b1901a62010-06-16 00:18:471026 candidates.add(directory)
1027 if directory == root:
[email protected]4661e0c2009-06-04 00:45:261028 break
[email protected]b1901a62010-06-16 00:18:471029 parent_dir = os.path.dirname(directory)
1030 if parent_dir == directory:
1031 # We hit the system root directory.
1032 break
1033 directory = parent_dir
1034
1035 # Look for PRESUBMIT.py in all candidate directories.
1036 results = []
1037 for directory in sorted(list(candidates)):
1038 p = os.path.join(directory, 'PRESUBMIT.py')
1039 if os.path.isfile(p):
1040 results.append(p)
1041
[email protected]5d0dc432011-01-03 02:40:371042 logging.debug('Presubmit files: %s' % ','.join(results))
[email protected]b1901a62010-06-16 00:18:471043 return results
[email protected]fb2b8eb2009-04-23 21:03:421044
1045
[email protected]de243452009-10-06 21:02:561046class GetTrySlavesExecuter(object):
[email protected]cb2985f2010-11-03 14:08:311047 @staticmethod
[email protected]15169952011-09-27 14:30:531048 def ExecPresubmitScript(script_text, presubmit_path, project, change):
[email protected]de243452009-10-06 21:02:561049 """Executes GetPreferredTrySlaves() from a single presubmit script.
[email protected]92c30092014-04-15 00:30:371050
[email protected]58a69cb2014-03-01 02:08:291051 This will soon be deprecated and replaced by GetPreferredTryMasters().
[email protected]de243452009-10-06 21:02:561052
1053 Args:
1054 script_text: The text of the presubmit script.
[email protected]78230022011-05-24 18:55:191055 presubmit_path: Project script to run.
1056 project: Project name to pass to presubmit script for bot selection.
[email protected]de243452009-10-06 21:02:561057
1058 Return:
1059 A list of try slaves.
1060 """
1061 context = {}
[email protected]f6349642014-03-04 00:52:181062 main_path = os.getcwd()
[email protected]899e1c12011-04-07 17:03:181063 try:
[email protected]f6349642014-03-04 00:52:181064 os.chdir(os.path.dirname(presubmit_path))
[email protected]899e1c12011-04-07 17:03:181065 exec script_text in context
1066 except Exception, e:
1067 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
[email protected]f6349642014-03-04 00:52:181068 finally:
1069 os.chdir(main_path)
[email protected]de243452009-10-06 21:02:561070
1071 function_name = 'GetPreferredTrySlaves'
1072 if function_name in context:
[email protected]15169952011-09-27 14:30:531073 get_preferred_try_slaves = context[function_name]
1074 function_info = inspect.getargspec(get_preferred_try_slaves)
1075 if len(function_info[0]) == 1:
1076 result = get_preferred_try_slaves(project)
1077 elif len(function_info[0]) == 2:
1078 result = get_preferred_try_slaves(project, change)
1079 else:
1080 result = get_preferred_try_slaves()
[email protected]de243452009-10-06 21:02:561081 if not isinstance(result, types.ListType):
[email protected]899e1c12011-04-07 17:03:181082 raise PresubmitFailure(
[email protected]de243452009-10-06 21:02:561083 'Presubmit functions must return a list, got a %s instead: %s' %
1084 (type(result), str(result)))
1085 for item in result:
[email protected]68e04192013-11-04 22:14:381086 if isinstance(item, basestring):
1087 # Old-style ['bot'] format.
1088 botname = item
1089 elif isinstance(item, tuple):
1090 # New-style [('bot', set(['tests']))] format.
1091 botname = item[0]
1092 else:
1093 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1094 ' format.')
1095
1096 if botname != botname.strip():
[email protected]899e1c12011-04-07 17:03:181097 raise PresubmitFailure(
1098 'Try slave names cannot start/end with whitespace')
[email protected]68e04192013-11-04 22:14:381099 if ',' in botname:
[email protected]3ecc8ea2012-03-10 01:47:461100 raise PresubmitFailure(
[email protected]68e04192013-11-04 22:14:381101 'Do not use \',\' separated builder or test names: %s' % botname)
[email protected]de243452009-10-06 21:02:561102 else:
1103 result = []
[email protected]5ca27622013-12-18 17:44:581104
1105 def valid_oldstyle(result):
1106 return all(isinstance(i, basestring) for i in result)
1107
1108 def valid_newstyle(result):
1109 return (all(isinstance(i, tuple) for i in result) and
1110 all(len(i) == 2 for i in result) and
1111 all(isinstance(i[0], basestring) for i in result) and
1112 all(isinstance(i[1], set) for i in result)
1113 )
1114
1115 # Ensure it's either all old-style or all new-style.
1116 if not valid_oldstyle(result) and not valid_newstyle(result):
1117 raise PresubmitFailure(
1118 'PRESUBMIT.py returned invalid trybot specification!')
1119
[email protected]de243452009-10-06 21:02:561120 return result
1121
1122
[email protected]58a69cb2014-03-01 02:08:291123class GetTryMastersExecuter(object):
1124 @staticmethod
1125 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1126 """Executes GetPreferredTryMasters() from a single presubmit script.
1127
1128 Args:
1129 script_text: The text of the presubmit script.
1130 presubmit_path: Project script to run.
1131 project: Project name to pass to presubmit script for bot selection.
1132
1133 Return:
1134 A map of try masters to map of builders to set of tests.
1135 """
1136 context = {}
1137 try:
1138 exec script_text in context
1139 except Exception, e:
1140 raise PresubmitFailure('"%s" had an exception.\n%s'
1141 % (presubmit_path, e))
1142
1143 function_name = 'GetPreferredTryMasters'
1144 if function_name not in context:
1145 return {}
1146 get_preferred_try_masters = context[function_name]
1147 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1148 raise PresubmitFailure(
1149 'Expected function "GetPreferredTryMasters" to take two arguments.')
1150 return get_preferred_try_masters(project, change)
1151
1152
[email protected]5626a922015-02-26 14:03:301153class GetPostUploadExecuter(object):
1154 @staticmethod
1155 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1156 """Executes PostUploadHook() from a single presubmit script.
1157
1158 Args:
1159 script_text: The text of the presubmit script.
1160 presubmit_path: Project script to run.
1161 cl: The Changelist object.
1162 change: The Change object.
1163
1164 Return:
1165 A list of results objects.
1166 """
1167 context = {}
1168 try:
1169 exec script_text in context
1170 except Exception, e:
1171 raise PresubmitFailure('"%s" had an exception.\n%s'
1172 % (presubmit_path, e))
1173
1174 function_name = 'PostUploadHook'
1175 if function_name not in context:
1176 return {}
1177 post_upload_hook = context[function_name]
1178 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1179 raise PresubmitFailure(
1180 'Expected function "PostUploadHook" to take three arguments.')
1181 return post_upload_hook(cl, change, OutputApi(False))
1182
1183
[email protected]15169952011-09-27 14:30:531184def DoGetTrySlaves(change,
1185 changed_files,
[email protected]de243452009-10-06 21:02:561186 repository_root,
1187 default_presubmit,
[email protected]78230022011-05-24 18:55:191188 project,
[email protected]de243452009-10-06 21:02:561189 verbose,
1190 output_stream):
[email protected]58a69cb2014-03-01 02:08:291191 """Get the list of try servers from the presubmit scripts (deprecated).
[email protected]de243452009-10-06 21:02:561192
1193 Args:
1194 changed_files: List of modified files.
1195 repository_root: The repository root.
1196 default_presubmit: A default presubmit script to execute in any case.
[email protected]78230022011-05-24 18:55:191197 project: Optional name of a project used in selecting trybots.
[email protected]de243452009-10-06 21:02:561198 verbose: Prints debug info.
1199 output_stream: A stream to write debug output to.
1200
1201 Return:
1202 List of try slaves
1203 """
1204 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1205 if not presubmit_files and verbose:
[email protected]d59e7612014-03-05 19:55:561206 output_stream.write("Warning, no PRESUBMIT.py found.\n")
[email protected]de243452009-10-06 21:02:561207 results = []
1208 executer = GetTrySlavesExecuter()
[email protected]5ca27622013-12-18 17:44:581209
[email protected]de243452009-10-06 21:02:561210 if default_presubmit:
1211 if verbose:
1212 output_stream.write("Running default presubmit script.\n")
[email protected]899e1c12011-04-07 17:03:181213 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
[email protected]5ca27622013-12-18 17:44:581214 results.extend(executer.ExecPresubmitScript(
1215 default_presubmit, fake_path, project, change))
[email protected]de243452009-10-06 21:02:561216 for filename in presubmit_files:
1217 filename = os.path.abspath(filename)
1218 if verbose:
1219 output_stream.write("Running %s\n" % filename)
1220 # Accept CRLF presubmit script.
[email protected]5aeb7dd2009-11-17 18:09:011221 presubmit_script = gclient_utils.FileRead(filename, 'rU')
[email protected]5ca27622013-12-18 17:44:581222 results.extend(executer.ExecPresubmitScript(
1223 presubmit_script, filename, project, change))
[email protected]de243452009-10-06 21:02:561224
[email protected]5ca27622013-12-18 17:44:581225
1226 slave_dict = {}
1227 old_style = filter(lambda x: isinstance(x, basestring), results)
1228 new_style = filter(lambda x: isinstance(x, tuple), results)
1229
1230 for result in new_style:
1231 slave_dict.setdefault(result[0], set()).update(result[1])
1232 slaves = list(slave_dict.items())
1233
1234 slaves.extend(set(old_style))
[email protected]68e04192013-11-04 22:14:381235
[email protected]de243452009-10-06 21:02:561236 if slaves and verbose:
[email protected]5ca27622013-12-18 17:44:581237 output_stream.write(', '.join((str(x) for x in slaves)))
[email protected]de243452009-10-06 21:02:561238 output_stream.write('\n')
1239 return slaves
1240
1241
[email protected]58a69cb2014-03-01 02:08:291242def _MergeMasters(masters1, masters2):
1243 """Merges two master maps. Merges also the tests of each builder."""
1244 result = {}
1245 for (master, builders) in itertools.chain(masters1.iteritems(),
1246 masters2.iteritems()):
1247 new_builders = result.setdefault(master, {})
1248 for (builder, tests) in builders.iteritems():
1249 new_builders.setdefault(builder, set([])).update(tests)
1250 return result
1251
1252
1253def DoGetTryMasters(change,
1254 changed_files,
1255 repository_root,
1256 default_presubmit,
1257 project,
1258 verbose,
1259 output_stream):
1260 """Get the list of try masters from the presubmit scripts.
1261
1262 Args:
1263 changed_files: List of modified files.
1264 repository_root: The repository root.
1265 default_presubmit: A default presubmit script to execute in any case.
1266 project: Optional name of a project used in selecting trybots.
1267 verbose: Prints debug info.
1268 output_stream: A stream to write debug output to.
1269
1270 Return:
1271 Map of try masters to map of builders to set of tests.
1272 """
1273 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1274 if not presubmit_files and verbose:
1275 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1276 results = {}
1277 executer = GetTryMastersExecuter()
1278
1279 if default_presubmit:
1280 if verbose:
1281 output_stream.write("Running default presubmit script.\n")
1282 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1283 results = _MergeMasters(results, executer.ExecPresubmitScript(
1284 default_presubmit, fake_path, project, change))
1285 for filename in presubmit_files:
1286 filename = os.path.abspath(filename)
1287 if verbose:
1288 output_stream.write("Running %s\n" % filename)
1289 # Accept CRLF presubmit script.
1290 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1291 results = _MergeMasters(results, executer.ExecPresubmitScript(
1292 presubmit_script, filename, project, change))
1293
1294 # Make sets to lists again for later JSON serialization.
1295 for builders in results.itervalues():
1296 for builder in builders:
1297 builders[builder] = list(builders[builder])
1298
1299 if results and verbose:
1300 output_stream.write('%s\n' % str(results))
1301 return results
1302
1303
[email protected]5626a922015-02-26 14:03:301304def DoPostUploadExecuter(change,
1305 cl,
1306 repository_root,
1307 verbose,
1308 output_stream):
1309 """Execute the post upload hook.
1310
1311 Args:
1312 change: The Change object.
1313 cl: The Changelist object.
1314 repository_root: The repository root.
1315 verbose: Prints debug info.
1316 output_stream: A stream to write debug output to.
1317 """
1318 presubmit_files = ListRelevantPresubmitFiles(
1319 change.LocalPaths(), repository_root)
1320 if not presubmit_files and verbose:
1321 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1322 results = []
1323 executer = GetPostUploadExecuter()
1324 # The root presubmit file should be executed after the ones in subdirectories.
1325 # i.e. the specific post upload hooks should run before the general ones.
1326 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1327 presubmit_files.reverse()
1328
1329 for filename in presubmit_files:
1330 filename = os.path.abspath(filename)
1331 if verbose:
1332 output_stream.write("Running %s\n" % filename)
1333 # Accept CRLF presubmit script.
1334 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1335 results.extend(executer.ExecPresubmitScript(
1336 presubmit_script, filename, cl, change))
1337 output_stream.write('\n')
1338 if results:
1339 output_stream.write('** Post Upload Hook Messages **\n')
1340 for result in results:
1341 result.handle(output_stream)
1342 output_stream.write('\n')
1343
1344 return results
1345
1346
[email protected]fb2b8eb2009-04-23 21:03:421347class PresubmitExecuter(object):
[email protected]cc73ad62011-07-06 17:39:261348 def __init__(self, change, committing, rietveld_obj, verbose):
[email protected]fb2b8eb2009-04-23 21:03:421349 """
1350 Args:
[email protected]4ff922a2009-06-12 20:20:191351 change: The Change object.
[email protected]fb2b8eb2009-04-23 21:03:421352 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
[email protected]239f4112011-06-03 20:08:231353 rietveld_obj: rietveld.Rietveld client object.
[email protected]fb2b8eb2009-04-23 21:03:421354 """
[email protected]4ff922a2009-06-12 20:20:191355 self.change = change
[email protected]fb2b8eb2009-04-23 21:03:421356 self.committing = committing
[email protected]239f4112011-06-03 20:08:231357 self.rietveld = rietveld_obj
[email protected]899e1c12011-04-07 17:03:181358 self.verbose = verbose
[email protected]fb2b8eb2009-04-23 21:03:421359
1360 def ExecPresubmitScript(self, script_text, presubmit_path):
1361 """Executes a single presubmit script.
1362
1363 Args:
1364 script_text: The text of the presubmit script.
1365 presubmit_path: The path to the presubmit file (this will be reported via
1366 input_api.PresubmitLocalPath()).
1367
1368 Return:
1369 A list of result objects, empty if no problems.
1370 """
[email protected]c6ef53a2014-11-04 00:13:541371
[email protected]8e416c82009-10-06 04:30:441372 # Change to the presubmit file's directory to support local imports.
1373 main_path = os.getcwd()
1374 os.chdir(os.path.dirname(presubmit_path))
1375
1376 # Load the presubmit script into context.
[email protected]970c5222011-03-12 00:32:241377 input_api = InputApi(self.change, presubmit_path, self.committing,
[email protected]cc73ad62011-07-06 17:39:261378 self.rietveld, self.verbose)
[email protected]fb2b8eb2009-04-23 21:03:421379 context = {}
[email protected]899e1c12011-04-07 17:03:181380 try:
1381 exec script_text in context
1382 except Exception, e:
1383 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
[email protected]fb2b8eb2009-04-23 21:03:421384
1385 # These function names must change if we make substantial changes to
1386 # the presubmit API that are not backwards compatible.
1387 if self.committing:
1388 function_name = 'CheckChangeOnCommit'
1389 else:
1390 function_name = 'CheckChangeOnUpload'
1391 if function_name in context:
[email protected]a6d011e2013-03-26 17:31:491392 context['__args'] = (input_api, OutputApi(self.committing))
[email protected]5d0dc432011-01-03 02:40:371393 logging.debug('Running %s in %s' % (function_name, presubmit_path))
[email protected]fb2b8eb2009-04-23 21:03:421394 result = eval(function_name + '(*__args)', context)
[email protected]5d0dc432011-01-03 02:40:371395 logging.debug('Running %s done.' % function_name)
[email protected]fb2b8eb2009-04-23 21:03:421396 if not (isinstance(result, types.TupleType) or
1397 isinstance(result, types.ListType)):
[email protected]899e1c12011-04-07 17:03:181398 raise PresubmitFailure(
[email protected]fb2b8eb2009-04-23 21:03:421399 'Presubmit functions must return a tuple or list')
1400 for item in result:
1401 if not isinstance(item, OutputApi.PresubmitResult):
[email protected]899e1c12011-04-07 17:03:181402 raise PresubmitFailure(
[email protected]fb2b8eb2009-04-23 21:03:421403 'All presubmit results must be of types derived from '
1404 'output_api.PresubmitResult')
1405 else:
1406 result = () # no error since the script doesn't care about current event.
1407
[email protected]8e416c82009-10-06 04:30:441408 # Return the process to the original working directory.
1409 os.chdir(main_path)
[email protected]fb2b8eb2009-04-23 21:03:421410 return result
1411
[email protected]5ac21012011-03-16 02:58:251412
[email protected]4ff922a2009-06-12 20:20:191413def DoPresubmitChecks(change,
[email protected]fb2b8eb2009-04-23 21:03:421414 committing,
1415 verbose,
1416 output_stream,
[email protected]0ff1fab2009-05-22 13:08:151417 input_stream,
[email protected]b0dfd352009-06-10 14:12:541418 default_presubmit,
[email protected]970c5222011-03-12 00:32:241419 may_prompt,
[email protected]c6ef53a2014-11-04 00:13:541420 rietveld_obj):
[email protected]fb2b8eb2009-04-23 21:03:421421 """Runs all presubmit checks that apply to the files in the change.
1422
1423 This finds all PRESUBMIT.py files in directories enclosing the files in the
1424 change (up to the repository root) and calls the relevant entrypoint function
1425 depending on whether the change is being committed or uploaded.
1426
1427 Prints errors, warnings and notifications. Prompts the user for warnings
1428 when needed.
1429
1430 Args:
[email protected]4ff922a2009-06-12 20:20:191431 change: The Change object.
[email protected]fb2b8eb2009-04-23 21:03:421432 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1433 verbose: Prints debug info.
1434 output_stream: A stream to write output from presubmit tests to.
1435 input_stream: A stream to read input from the user.
[email protected]0ff1fab2009-05-22 13:08:151436 default_presubmit: A default presubmit script to execute in any case.
[email protected]b0dfd352009-06-10 14:12:541437 may_prompt: Enable (y/n) questions on warning or error.
[email protected]239f4112011-06-03 20:08:231438 rietveld_obj: rietveld.Rietveld object.
[email protected]fb2b8eb2009-04-23 21:03:421439
[email protected]ce8e46b2009-06-26 22:31:511440 Warning:
1441 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1442 SHOULD be sys.stdin.
1443
[email protected]fb2b8eb2009-04-23 21:03:421444 Return:
[email protected]5ac21012011-03-16 02:58:251445 A PresubmitOutput object. Use output.should_continue() to figure out
1446 if there were errors or warnings and the caller should abort.
[email protected]fb2b8eb2009-04-23 21:03:421447 """
[email protected]ea7c8552011-04-18 14:12:071448 old_environ = os.environ
1449 try:
1450 # Make sure python subprocesses won't generate .pyc files.
1451 os.environ = os.environ.copy()
1452 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
[email protected]fb2b8eb2009-04-23 21:03:421453
[email protected]ea7c8552011-04-18 14:12:071454 output = PresubmitOutput(input_stream, output_stream)
1455 if committing:
1456 output.write("Running presubmit commit checks ...\n")
[email protected]fb2b8eb2009-04-23 21:03:421457 else:
[email protected]ea7c8552011-04-18 14:12:071458 output.write("Running presubmit upload checks ...\n")
1459 start_time = time.time()
1460 presubmit_files = ListRelevantPresubmitFiles(
1461 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1462 if not presubmit_files and verbose:
[email protected]fae707b2011-09-15 18:57:581463 output.write("Warning, no PRESUBMIT.py found.\n")
[email protected]ea7c8552011-04-18 14:12:071464 results = []
[email protected]cc73ad62011-07-06 17:39:261465 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
[email protected]ea7c8552011-04-18 14:12:071466 if default_presubmit:
1467 if verbose:
1468 output.write("Running default presubmit script.\n")
1469 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1470 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1471 for filename in presubmit_files:
1472 filename = os.path.abspath(filename)
1473 if verbose:
1474 output.write("Running %s\n" % filename)
1475 # Accept CRLF presubmit script.
1476 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1477 results += executer.ExecPresubmitScript(presubmit_script, filename)
[email protected]fb2b8eb2009-04-23 21:03:421478
[email protected]ea7c8552011-04-18 14:12:071479 errors = []
1480 notifications = []
1481 warnings = []
1482 for result in results:
1483 if result.fatal:
1484 errors.append(result)
1485 elif result.should_prompt:
1486 warnings.append(result)
1487 else:
1488 notifications.append(result)
[email protected]ed9a0832009-09-09 22:48:551489
[email protected]ea7c8552011-04-18 14:12:071490 output.write('\n')
1491 for name, items in (('Messages', notifications),
1492 ('Warnings', warnings),
1493 ('ERRORS', errors)):
1494 if items:
1495 output.write('** Presubmit %s **\n' % name)
1496 for item in items:
1497 item.handle(output)
1498 output.write('\n')
[email protected]ed9a0832009-09-09 22:48:551499
[email protected]ea7c8552011-04-18 14:12:071500 total_time = time.time() - start_time
1501 if total_time > 1.0:
1502 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
[email protected]ce8e46b2009-06-26 22:31:511503
[email protected]ea7c8552011-04-18 14:12:071504 if not errors:
1505 if not warnings:
1506 output.write('Presubmit checks passed.\n')
1507 elif may_prompt:
1508 output.prompt_yes_no('There were presubmit warnings. '
1509 'Are you sure you wish to continue? (y/N): ')
1510 else:
1511 output.fail()
1512
1513 global _ASKED_FOR_FEEDBACK
1514 # Ask for feedback one time out of 5.
1515 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
[email protected]1ce8e662014-01-14 15:23:001516 output.write(
1517 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1518 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1519 'on the file to figure out who to ask for help.\n')
[email protected]ea7c8552011-04-18 14:12:071520 _ASKED_FOR_FEEDBACK = True
1521 return output
1522 finally:
1523 os.environ = old_environ
[email protected]fb2b8eb2009-04-23 21:03:421524
1525
1526def ScanSubDirs(mask, recursive):
1527 if not recursive:
[email protected]e57b09d2014-05-07 00:58:131528 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
[email protected]fb2b8eb2009-04-23 21:03:421529 else:
1530 results = []
1531 for root, dirs, files in os.walk('.'):
1532 if '.svn' in dirs:
1533 dirs.remove('.svn')
[email protected]c70a2202009-06-17 12:55:101534 if '.git' in dirs:
1535 dirs.remove('.git')
[email protected]fb2b8eb2009-04-23 21:03:421536 for name in files:
1537 if fnmatch.fnmatch(name, mask):
1538 results.append(os.path.join(root, name))
1539 return results
1540
1541
1542def ParseFiles(args, recursive):
[email protected]7444c502011-02-09 14:02:111543 logging.debug('Searching for %s' % args)
[email protected]fb2b8eb2009-04-23 21:03:421544 files = []
1545 for arg in args:
[email protected]e3608df2009-11-10 20:22:571546 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
[email protected]fb2b8eb2009-04-23 21:03:421547 return files
1548
1549
[email protected]5c8c6de2011-03-18 16:20:181550def load_files(options, args):
1551 """Tries to determine the SCM."""
1552 change_scm = scm.determine_scm(options.root)
1553 files = []
[email protected]5c8c6de2011-03-18 16:20:181554 if args:
1555 files = ParseFiles(args, options.recursive)
[email protected]9b31f162012-01-26 19:02:311556 if change_scm == 'svn':
1557 change_class = SvnChange
1558 if not files:
1559 files = scm.SVN.CaptureStatus([], options.root)
1560 elif change_scm == 'git':
1561 change_class = GitChange
[email protected]2da1ade2014-04-30 17:40:451562 upstream = options.upstream or None
[email protected]9b31f162012-01-26 19:02:311563 if not files:
[email protected]2da1ade2014-04-30 17:40:451564 files = scm.GIT.CaptureStatus([], options.root, upstream)
[email protected]5c8c6de2011-03-18 16:20:181565 else:
[email protected]9b31f162012-01-26 19:02:311566 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1567 if not files:
1568 return None, None
1569 change_class = Change
[email protected]5c8c6de2011-03-18 16:20:181570 return change_class, files
1571
1572
[email protected]8a4a2bc2013-03-08 08:13:201573class NonexistantCannedCheckFilter(Exception):
1574 pass
1575
1576
1577@contextlib.contextmanager
1578def canned_check_filter(method_names):
1579 filtered = {}
1580 try:
1581 for method_name in method_names:
1582 if not hasattr(presubmit_canned_checks, method_name):
1583 raise NonexistantCannedCheckFilter(method_name)
1584 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1585 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1586 yield
1587 finally:
1588 for name, method in filtered.iteritems():
1589 setattr(presubmit_canned_checks, name, method)
1590
[email protected]ffeb2f32013-12-03 13:55:221591
[email protected]bc117312013-04-20 03:57:561592def CallCommand(cmd_data):
[email protected]ffeb2f32013-12-03 13:55:221593 """Runs an external program, potentially from a child process created by the
1594 multiprocessing module.
1595
1596 multiprocessing needs a top level function with a single argument.
1597 """
[email protected]bc117312013-04-20 03:57:561598 cmd_data.kwargs['stdout'] = subprocess.PIPE
1599 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1600 try:
[email protected]ffeb2f32013-12-03 13:55:221601 start = time.time()
[email protected]bc117312013-04-20 03:57:561602 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
[email protected]ffeb2f32013-12-03 13:55:221603 duration = time.time() - start
[email protected]bc117312013-04-20 03:57:561604 except OSError as e:
[email protected]ffeb2f32013-12-03 13:55:221605 duration = time.time() - start
[email protected]bc117312013-04-20 03:57:561606 return cmd_data.message(
[email protected]ffeb2f32013-12-03 13:55:221607 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1608 if code != 0:
1609 return cmd_data.message(
1610 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1611 if cmd_data.info:
1612 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
[email protected]bc117312013-04-20 03:57:561613
[email protected]8a4a2bc2013-03-08 08:13:201614
[email protected]013731e2015-02-26 18:28:431615def main(argv=None):
[email protected]5c8c6de2011-03-18 16:20:181616 parser = optparse.OptionParser(usage="%prog [options] <files...>",
[email protected]fb2b8eb2009-04-23 21:03:421617 version="%prog " + str(__version__))
[email protected]c70a2202009-06-17 12:55:101618 parser.add_option("-c", "--commit", action="store_true", default=False,
[email protected]fb2b8eb2009-04-23 21:03:421619 help="Use commit instead of upload checks")
[email protected]c70a2202009-06-17 12:55:101620 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1621 help="Use upload instead of commit checks")
[email protected]fb2b8eb2009-04-23 21:03:421622 parser.add_option("-r", "--recursive", action="store_true",
1623 help="Act recursively")
[email protected]899e1c12011-04-07 17:03:181624 parser.add_option("-v", "--verbose", action="count", default=0,
1625 help="Use 2 times for more debug info")
[email protected]4ff922a2009-06-12 20:20:191626 parser.add_option("--name", default='no name')
[email protected]58407af2011-04-12 23:15:571627 parser.add_option("--author")
[email protected]4ff922a2009-06-12 20:20:191628 parser.add_option("--description", default='')
1629 parser.add_option("--issue", type='int', default=0)
1630 parser.add_option("--patchset", type='int', default=0)
[email protected]b1901a62010-06-16 00:18:471631 parser.add_option("--root", default=os.getcwd(),
1632 help="Search for PRESUBMIT.py up to this directory. "
1633 "If inherit-review-settings-ok is present in this "
1634 "directory, parent directories up to the root file "
1635 "system directories will also be searched.")
[email protected]2da1ade2014-04-30 17:40:451636 parser.add_option("--upstream",
1637 help="Git only: the base ref or upstream branch against "
1638 "which the diff should be computed.")
[email protected]c70a2202009-06-17 12:55:101639 parser.add_option("--default_presubmit")
1640 parser.add_option("--may_prompt", action='store_true', default=False)
[email protected]8a4a2bc2013-03-08 08:13:201641 parser.add_option("--skip_canned", action='append', default=[],
1642 help="A list of checks to skip which appear in "
1643 "presubmit_canned_checks. Can be provided multiple times "
1644 "to skip multiple canned checks.")
[email protected]239f4112011-06-03 20:08:231645 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1646 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
[email protected]720fd7a2013-04-24 04:13:501647 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1648 help=optparse.SUPPRESS_HELP)
[email protected]92c30092014-04-15 00:30:371649 # These are for OAuth2 authentication for bots. See also apply_issue.py
1650 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1651 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1652
[email protected]2ff30182016-03-23 09:52:511653 # TODO(phajdan.jr): Update callers and remove obsolete --trybot-json .
[email protected]f7d31f52014-01-03 20:14:461654 parser.add_option("--trybot-json",
1655 help="Output trybot information to the file specified.")
[email protected]cf6a5d22015-04-09 22:02:001656 auth.add_auth_options(parser)
[email protected]82e5f282011-03-17 14:08:551657 options, args = parser.parse_args(argv)
[email protected]cf6a5d22015-04-09 22:02:001658 auth_config = auth.extract_auth_config_from_options(options)
[email protected]92c30092014-04-15 00:30:371659
[email protected]899e1c12011-04-07 17:03:181660 if options.verbose >= 2:
[email protected]7444c502011-02-09 14:02:111661 logging.basicConfig(level=logging.DEBUG)
[email protected]899e1c12011-04-07 17:03:181662 elif options.verbose:
1663 logging.basicConfig(level=logging.INFO)
1664 else:
1665 logging.basicConfig(level=logging.ERROR)
[email protected]92c30092014-04-15 00:30:371666
1667 if options.rietveld_email and options.rietveld_email_file:
1668 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1669 "can be passed to this program.")
[email protected]7b654f52014-04-15 04:02:321670
[email protected]92c30092014-04-15 00:30:371671 if options.rietveld_email_file:
1672 with open(options.rietveld_email_file, "rb") as f:
1673 options.rietveld_email = f.read().strip()
1674
[email protected]5c8c6de2011-03-18 16:20:181675 change_class, files = load_files(options, args)
1676 if not change_class:
1677 parser.error('For unversioned directory, <files> is not optional.')
[email protected]899e1c12011-04-07 17:03:181678 logging.info('Found %d file(s).' % len(files))
[email protected]92c30092014-04-15 00:30:371679
[email protected]239f4112011-06-03 20:08:231680 rietveld_obj = None
1681 if options.rietveld_url:
[email protected]92c30092014-04-15 00:30:371682 # The empty password is permitted: '' is not None.
[email protected]7b654f52014-04-15 04:02:321683 if options.rietveld_private_key_file:
[email protected]92c30092014-04-15 00:30:371684 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1685 options.rietveld_url,
1686 options.rietveld_email,
1687 options.rietveld_private_key_file)
1688 else:
[email protected]7b654f52014-04-15 04:02:321689 rietveld_obj = rietveld.CachingRietveld(
1690 options.rietveld_url,
[email protected]cf6a5d22015-04-09 22:02:001691 auth_config,
1692 options.rietveld_email)
[email protected]720fd7a2013-04-24 04:13:501693 if options.rietveld_fetch:
1694 assert options.issue
1695 props = rietveld_obj.get_issue_properties(options.issue, False)
1696 options.author = props['owner_email']
1697 options.description = props['description']
1698 logging.info('Got author: "%s"', options.author)
1699 logging.info('Got description: """\n%s\n"""', options.description)
[email protected]899e1c12011-04-07 17:03:181700 try:
[email protected]8a4a2bc2013-03-08 08:13:201701 with canned_check_filter(options.skip_canned):
1702 results = DoPresubmitChecks(
1703 change_class(options.name,
1704 options.description,
1705 options.root,
1706 files,
1707 options.issue,
1708 options.patchset,
[email protected]ea84ef12014-04-30 19:55:121709 options.author,
1710 upstream=options.upstream),
[email protected]8a4a2bc2013-03-08 08:13:201711 options.commit,
1712 options.verbose,
1713 sys.stdout,
1714 sys.stdin,
1715 options.default_presubmit,
1716 options.may_prompt,
1717 rietveld_obj)
[email protected]899e1c12011-04-07 17:03:181718 return not results.should_continue()
[email protected]8a4a2bc2013-03-08 08:13:201719 except NonexistantCannedCheckFilter, e:
1720 print >> sys.stderr, (
1721 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1722 return 2
[email protected]899e1c12011-04-07 17:03:181723 except PresubmitFailure, e:
1724 print >> sys.stderr, e
1725 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1726 print >> sys.stderr, 'If all fails, contact maruel@'
1727 return 2
[email protected]fb2b8eb2009-04-23 21:03:421728
1729
1730if __name__ == '__main__':
[email protected]35625c72011-03-23 17:34:021731 fix_encoding.fix_encoding()
[email protected]013731e2015-02-26 18:28:431732 try:
1733 sys.exit(main())
1734 except KeyboardInterrupt:
1735 sys.stderr.write('interrupted\n')
1736 sys.exit(1)