blob: a95b9adf6efa5efad34d68360845545cb8b34ddc [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
maruel@chromium.org3bbf2942012-01-10 16:52:06 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00009__version__ = '1.8.0'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
enne@chromium.orge72c5f52013-04-16 00:36:40 +000015import cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000016import cPickle # Exposed through the API.
17import cStringIO # Exposed through the API.
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +000018import contextlib
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000019import fnmatch
20import glob
asvitkine@chromium.org15169952011-09-27 14:30:53 +000021import inspect
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +000022import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000023import json # Exposed through the API.
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000024import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import marshal # Exposed through the API.
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000026import multiprocessing
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000027import optparse
28import os # Somewhat exposed through the API.
29import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000030import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000031import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import sys # Parts exposed through API.
33import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000034import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000035import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000036import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000037import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000038import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000039from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000040
41# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000042import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000043import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000044import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000046import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000048import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049
50
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000051# Ask for feedback only once in program lifetime.
52_ASKED_FOR_FEEDBACK = False
53
54
maruel@chromium.org899e1c12011-04-07 17:03:18 +000055class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000056 pass
57
58
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000059class CommandData(object):
60 def __init__(self, name, cmd, kwargs, message):
61 self.name = name
62 self.cmd = cmd
63 self.kwargs = kwargs
64 self.message = message
65 self.info = None
66
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000067
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000068def normpath(path):
69 '''Version of os.path.normpath that also changes backward slashes to
70 forward slashes when not running on Windows.
71 '''
72 # This is safe to always do because the Windows version of os.path.normpath
73 # will replace forward slashes with backward slashes.
74 path = path.replace(os.sep, '/')
75 return os.path.normpath(path)
76
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000077
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078def _RightHandSideLinesImpl(affected_files):
79 """Implements RightHandSideLines for InputApi and GclChange."""
80 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000081 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000082 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000083 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000084
85
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000086class PresubmitOutput(object):
87 def __init__(self, input_stream=None, output_stream=None):
88 self.input_stream = input_stream
89 self.output_stream = output_stream
90 self.reviewers = []
91 self.written_output = []
92 self.error_count = 0
93
94 def prompt_yes_no(self, prompt_string):
95 self.write(prompt_string)
96 if self.input_stream:
97 response = self.input_stream.readline().strip().lower()
98 if response not in ('y', 'yes'):
99 self.fail()
100 else:
101 self.fail()
102
103 def fail(self):
104 self.error_count += 1
105
106 def should_continue(self):
107 return not self.error_count
108
109 def write(self, s):
110 self.written_output.append(s)
111 if self.output_stream:
112 self.output_stream.write(s)
113
114 def getvalue(self):
115 return ''.join(self.written_output)
116
117
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000118# Top level object so multiprocessing can pickle
119# Public access through OutputApi object.
120class _PresubmitResult(object):
121 """Base class for result objects."""
122 fatal = False
123 should_prompt = False
124
125 def __init__(self, message, items=None, long_text=''):
126 """
127 message: A short one-line message to indicate errors.
128 items: A list of short strings to indicate where errors occurred.
129 long_text: multi-line text output, e.g. from another tool
130 """
131 self._message = message
132 self._items = items or []
133 if items:
134 self._items = items
135 self._long_text = long_text.rstrip()
136
137 def handle(self, output):
138 output.write(self._message)
139 output.write('\n')
140 for index, item in enumerate(self._items):
141 output.write(' ')
142 # Write separately in case it's unicode.
143 output.write(str(item))
144 if index < len(self._items) - 1:
145 output.write(' \\')
146 output.write('\n')
147 if self._long_text:
148 output.write('\n***************\n')
149 # Write separately in case it's unicode.
150 output.write(self._long_text)
151 output.write('\n***************\n')
152 if self.fatal:
153 output.fail()
154
155
156# Top level object so multiprocessing can pickle
157# Public access through OutputApi object.
158class _PresubmitAddReviewers(_PresubmitResult):
159 """Add some suggested reviewers to the change."""
160 def __init__(self, reviewers):
161 super(_PresubmitAddReviewers, self).__init__('')
162 self.reviewers = reviewers
163
164 def handle(self, output):
165 output.reviewers.extend(self.reviewers)
166
167
168# Top level object so multiprocessing can pickle
169# Public access through OutputApi object.
170class _PresubmitError(_PresubmitResult):
171 """A hard presubmit error."""
172 fatal = True
173
174
175# Top level object so multiprocessing can pickle
176# Public access through OutputApi object.
177class _PresubmitPromptWarning(_PresubmitResult):
178 """An warning that prompts the user if they want to continue."""
179 should_prompt = True
180
181
182# Top level object so multiprocessing can pickle
183# Public access through OutputApi object.
184class _PresubmitNotifyResult(_PresubmitResult):
185 """Just print something to the screen -- but it's not even a warning."""
186 pass
187
188
189# Top level object so multiprocessing can pickle
190# Public access through OutputApi object.
191class _MailTextResult(_PresubmitResult):
192 """A warning that should be included in the review request email."""
193 def __init__(self, *args, **kwargs):
194 super(_MailTextResult, self).__init__()
195 raise NotImplementedError()
196
197
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000198class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000199 """An instance of OutputApi gets passed to presubmit scripts so that they
200 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000201 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000202 PresubmitResult = _PresubmitResult
203 PresubmitAddReviewers = _PresubmitAddReviewers
204 PresubmitError = _PresubmitError
205 PresubmitPromptWarning = _PresubmitPromptWarning
206 PresubmitNotifyResult = _PresubmitNotifyResult
207 MailTextResult = _MailTextResult
208
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000209 def __init__(self, is_committing):
210 self.is_committing = is_committing
211
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000212 def PresubmitPromptOrNotify(self, *args, **kwargs):
213 """Warn the user when uploading, but only notify if committing."""
214 if self.is_committing:
215 return self.PresubmitNotifyResult(*args, **kwargs)
216 return self.PresubmitPromptWarning(*args, **kwargs)
217
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000218
219class InputApi(object):
220 """An instance of this object is passed to presubmit scripts so they can
221 know stuff about the change they're looking at.
222 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000223 # Method could be a function
224 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000225
maruel@chromium.org3410d912009-06-09 20:56:16 +0000226 # File extensions that are considered source files from a style guide
227 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000228 #
229 # Files without an extension aren't included in the list. If you want to
230 # filter them as source files, add r"(^|.*?[\\\/])[^.]+$" to the white list.
231 # Note that ALL CAPS files are black listed in DEFAULT_BLACK_LIST below.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000232 DEFAULT_WHITE_LIST = (
233 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000234 r".+\.c$", r".+\.cc$", r".+\.cpp$", r".+\.h$", r".+\.m$", r".+\.mm$",
235 r".+\.inl$", r".+\.asm$", r".+\.hxx$", r".+\.hpp$", r".+\.s$", r".+\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000236 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000237 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000238 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000239 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000240 )
241
242 # Path regexp that should be excluded from being considered containing source
243 # files. Don't modify this list from a presubmit script!
244 DEFAULT_BLACK_LIST = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000245 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000246 r".*\bexperimental[\\\/].*",
247 r".*\bthird_party[\\\/].*",
248 # Output directories (just in case)
249 r".*\bDebug[\\\/].*",
250 r".*\bRelease[\\\/].*",
251 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000252 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000253 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000254 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000255 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000256 r"(|.*[\\\/])\.git[\\\/].*",
257 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000258 # There is no point in processing a patch file.
259 r".+\.diff$",
260 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000261 )
262
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000263 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000264 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000265 """Builds an InputApi object.
266
267 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000268 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000269 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000270 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000271 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000272 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000273 # Version number of the presubmit_support script.
274 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000275 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000276 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000277 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000278 # TBD
279 self.host_url = 'http://codereview.chromium.org'
280 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000281 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000282
283 # We expose various modules and functions as attributes of the input_api
284 # so that presubmit scripts don't have to import them.
285 self.basename = os.path.basename
286 self.cPickle = cPickle
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000287 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000288 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000289 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000290 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000291 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000292 self.os_listdir = os.listdir
293 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000294 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000295 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000296 self.pickle = pickle
297 self.marshal = marshal
298 self.re = re
299 self.subprocess = subprocess
300 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000301 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000302 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000303 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000304 self.urllib2 = urllib2
305
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000306 # To easily fork python.
307 self.python_executable = sys.executable
308 self.environ = os.environ
309
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000310 # InputApi.platform is the platform you're currently running on.
311 self.platform = sys.platform
312
313 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000314 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315
316 # We carry the canned checks so presubmit scripts can easily use them.
317 self.canned_checks = presubmit_canned_checks
318
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000319 # TODO(dpranke): figure out a list of all approved owners for a repo
320 # in order to be able to handle wildcard OWNERS files?
321 self.owners_db = owners.Database(change.RepositoryRoot(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000322 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000323 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000324 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000325
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000326 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000327 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000328 # Access to a protected member _XX of a client class
329 # pylint: disable=W0212
330 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000331 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000332 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
333 for (a, b, header) in cpplint._re_pattern_templates
334 ]
335
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000336 def PresubmitLocalPath(self):
337 """Returns the local path of the presubmit script currently being run.
338
339 This is useful if you don't want to hard-code absolute paths in the
340 presubmit script. For example, It can be used to find another file
341 relative to the PRESUBMIT.py script, so the whole tree can be branched and
342 the presubmit script still works, without editing its content.
343 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000344 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000346 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000347 """Translate a depot path to a local path (relative to client root).
348
349 Args:
350 Depot path as a string.
351
352 Returns:
353 The local path of the depot path under the user's current client, or None
354 if the file is not mapped.
355
356 Remember to check for the None case and show an appropriate error!
357 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000358 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
359 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000360
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000361 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000362 """Translate a local path to a depot path.
363
364 Args:
365 Local path (relative to current directory, or absolute) as a string.
366
367 Returns:
368 The depot path (SVN URL) of the file if mapped, otherwise None.
369 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000370 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
371 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000372
sail@chromium.org5538e022011-05-12 17:53:16 +0000373 def AffectedFiles(self, include_dirs=False, include_deletes=True,
374 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000375 """Same as input_api.change.AffectedFiles() except only lists files
376 (and optionally directories) in the same directory as the current presubmit
377 script, or subdirectories thereof.
378 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000379 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000380 if len(dir_with_slash) == 1:
381 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000382
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000383 return filter(
384 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000385 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000386
387 def LocalPaths(self, include_dirs=False):
388 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000389 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
390 logging.debug("LocalPaths: %s", paths)
391 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000392
393 def AbsoluteLocalPaths(self, include_dirs=False):
394 """Returns absolute local paths of input_api.AffectedFiles()."""
395 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
396
397 def ServerPaths(self, include_dirs=False):
398 """Returns server paths of input_api.AffectedFiles()."""
399 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
400
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000401 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402 """Same as input_api.change.AffectedTextFiles() except only lists files
403 in the same directory as the current presubmit script, or subdirectories
404 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000405 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000406 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000407 warn("AffectedTextFiles(include_deletes=%s)"
408 " is deprecated and ignored" % str(include_deletes),
409 category=DeprecationWarning,
410 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000411 return filter(lambda x: x.IsTextFile(),
412 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000413
maruel@chromium.org3410d912009-06-09 20:56:16 +0000414 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
415 """Filters out files that aren't considered "source file".
416
417 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
418 and InputApi.DEFAULT_BLACK_LIST is used respectively.
419
420 The lists will be compiled as regular expression and
421 AffectedFile.LocalPath() needs to pass both list.
422
423 Note: Copy-paste this function to suit your needs or use a lambda function.
424 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000425 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000426 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000427 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000428 if self.re.match(item, local_path):
429 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000430 return True
431 return False
432 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
433 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
434
435 def AffectedSourceFiles(self, source_file):
436 """Filter the list of AffectedTextFiles by the function source_file.
437
438 If source_file is None, InputApi.FilterSourceFile() is used.
439 """
440 if not source_file:
441 source_file = self.FilterSourceFile
442 return filter(source_file, self.AffectedTextFiles())
443
444 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000445 """An iterator over all text lines in "new" version of changed files.
446
447 Only lists lines from new or modified text files in the change that are
448 contained by the directory of the currently executing presubmit script.
449
450 This is useful for doing line-by-line regex checks, like checking for
451 trailing whitespace.
452
453 Yields:
454 a 3 tuple:
455 the AffectedFile instance of the current file;
456 integer line number (1-based); and
457 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000458
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000459 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000460 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000461 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000462 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000463
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000464 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000465 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000466
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000467 Deny reading anything outside the repository.
468 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000469 if isinstance(file_item, AffectedFile):
470 file_item = file_item.AbsoluteLocalPath()
471 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000472 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000473 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000474
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000475 @property
476 def tbr(self):
477 """Returns if a change is TBR'ed."""
478 return 'TBR' in self.change.tags
479
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000480 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000481 tests = []
482 msgs = []
483 for t in tests_mix:
484 if isinstance(t, OutputApi.PresubmitResult):
485 msgs.append(t)
486 else:
487 assert issubclass(t.message, _PresubmitResult)
488 tests.append(t)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000489 if self.verbose:
490 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000491 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000492 pool = multiprocessing.Pool()
493 # async recipe works around multiprocessing bug handling Ctrl-C
494 msgs.extend(pool.map_async(CallCommand, tests).get(99999))
495 pool.close()
496 pool.join()
497 else:
498 msgs.extend(map(CallCommand, tests))
499 return [m for m in msgs if m]
500
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000501
nick@chromium.orgff526192013-06-10 19:30:26 +0000502class _DiffCache(object):
503 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000504 def __init__(self, upstream=None):
505 """Stores the upstream revision against which all diffs will be computed."""
506 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000507
508 def GetDiff(self, path, local_root):
509 """Get the diff for a particular path."""
510 raise NotImplementedError()
511
512
513class _SvnDiffCache(_DiffCache):
514 """DiffCache implementation for subversion."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000515 def __init__(self, *args, **kwargs):
516 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000517 self._diffs_by_file = {}
518
519 def GetDiff(self, path, local_root):
520 if path not in self._diffs_by_file:
521 self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
522 False, None)
523 return self._diffs_by_file[path]
524
525
526class _GitDiffCache(_DiffCache):
527 """DiffCache implementation for git; gets all file diffs at once."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000528 def __init__(self, upstream):
529 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000530 self._diffs_by_file = None
531
532 def GetDiff(self, path, local_root):
533 if not self._diffs_by_file:
534 # Compute a single diff for all files and parse the output; should
535 # with git this is much faster than computing one diff for each file.
536 diffs = {}
537
538 # Don't specify any filenames below, because there are command line length
539 # limits on some platforms and GenerateDiff would fail.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000540 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
541 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000542
543 # This regex matches the path twice, separated by a space. Note that
544 # filename itself may contain spaces.
545 file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
546 current_diff = []
547 keep_line_endings = True
548 for x in unified_diff.splitlines(keep_line_endings):
549 match = file_marker.match(x)
550 if match:
551 # Marks the start of a new per-file section.
552 diffs[match.group('filename')] = current_diff = [x]
553 elif x.startswith('diff --git'):
554 raise PresubmitFailure('Unexpected diff line: %s' % x)
555 else:
556 current_diff.append(x)
557
558 self._diffs_by_file = dict(
559 (normpath(path), ''.join(diff)) for path, diff in diffs.items())
560
561 if path not in self._diffs_by_file:
562 raise PresubmitFailure(
563 'Unified diff did not contain entry for file %s' % path)
564
565 return self._diffs_by_file[path]
566
567
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000568class AffectedFile(object):
569 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000570
571 DIFF_CACHE = _DiffCache
572
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000573 # Method could be a function
574 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000575 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000576 self._path = path
577 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000578 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000579 self._is_directory = None
580 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000581 self._cached_changed_contents = None
582 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000583 self._diff_cache = diff_cache
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000584 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000585
586 def ServerPath(self):
587 """Returns a path string that identifies the file in the SCM system.
588
589 Returns the empty string if the file does not exist in SCM.
590 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000591 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592
593 def LocalPath(self):
594 """Returns the path of this file on the local disk relative to client root.
595 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000596 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000597
598 def AbsoluteLocalPath(self):
599 """Returns the absolute path of this file on the local disk.
600 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000601 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000602
603 def IsDirectory(self):
604 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000605 if self._is_directory is None:
606 path = self.AbsoluteLocalPath()
607 self._is_directory = (os.path.exists(path) and
608 os.path.isdir(path))
609 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000610
611 def Action(self):
612 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000613 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
614 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000615 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000616
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000617 def Property(self, property_name):
618 """Returns the specified SCM property of this file, or None if no such
619 property.
620 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000621 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000622
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000623 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000624 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000625
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000626 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000627 raise NotImplementedError() # Implement when needed
628
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000629 def NewContents(self):
630 """Returns an iterator over the lines in the new version of file.
631
632 The new version is the file in the user's workspace, i.e. the "right hand
633 side".
634
635 Contents will be empty if the file is a directory or does not exist.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000636 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000637 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000638 if self._cached_new_contents is None:
639 self._cached_new_contents = []
640 if not self.IsDirectory():
641 try:
642 self._cached_new_contents = gclient_utils.FileRead(
643 self.AbsoluteLocalPath(), 'rU').splitlines()
644 except IOError:
645 pass # File not found? That's fine; maybe it was deleted.
646 return self._cached_new_contents[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000647
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000648 def ChangedContents(self):
649 """Returns a list of tuples (line number, line text) of all new lines.
650
651 This relies on the scm diff output describing each changed code section
652 with a line of the form
653
654 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
655 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000656 if self._cached_changed_contents is not None:
657 return self._cached_changed_contents[:]
658 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000659 line_num = 0
660
661 if self.IsDirectory():
662 return []
663
664 for line in self.GenerateScmDiff().splitlines():
665 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
666 if m:
667 line_num = int(m.groups(1)[0])
668 continue
669 if line.startswith('+') and not line.startswith('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000670 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000671 if not line.startswith('-'):
672 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000673 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000674
maruel@chromium.org5de13972009-06-10 18:16:06 +0000675 def __str__(self):
676 return self.LocalPath()
677
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000678 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000679 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
maruel@chromium.org58407af2011-04-12 23:15:57 +0000681
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000682class SvnAffectedFile(AffectedFile):
683 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000684 # Method 'NNN' is abstract in class 'NNN' but is not overridden
685 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000686
nick@chromium.orgff526192013-06-10 19:30:26 +0000687 DIFF_CACHE = _SvnDiffCache
688
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000689 def __init__(self, *args, **kwargs):
690 AffectedFile.__init__(self, *args, **kwargs)
691 self._server_path = None
692 self._is_text_file = None
693
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000694 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000695 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000696 self._server_path = scm.SVN.CaptureLocalInfo(
697 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000698 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000699
700 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000701 if self._is_directory is None:
702 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000703 if os.path.exists(path):
704 # Retrieve directly from the file system; it is much faster than
705 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000706 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000707 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000708 self._is_directory = scm.SVN.CaptureLocalInfo(
709 [self.LocalPath()], self._local_root
710 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000711 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000712
713 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000714 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000715 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000716 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000717 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000718
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000719 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000720 if self._is_text_file is None:
721 if self.Action() == 'D':
722 # A deleted file is not a text file.
723 self._is_text_file = False
724 elif self.IsDirectory():
725 self._is_text_file = False
726 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000727 mime_type = scm.SVN.GetFileProperty(
728 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000729 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
730 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000731
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000732
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000733class GitAffectedFile(AffectedFile):
734 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000735 # Method 'NNN' is abstract in class 'NNN' but is not overridden
736 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000737
nick@chromium.orgff526192013-06-10 19:30:26 +0000738 DIFF_CACHE = _GitDiffCache
739
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000740 def __init__(self, *args, **kwargs):
741 AffectedFile.__init__(self, *args, **kwargs)
742 self._server_path = None
743 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000744
745 def ServerPath(self):
746 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000747 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000748 return self._server_path
749
750 def IsDirectory(self):
751 if self._is_directory is None:
752 path = self.AbsoluteLocalPath()
753 if os.path.exists(path):
754 # Retrieve directly from the file system; it is much faster than
755 # querying subversion, especially on Windows.
756 self._is_directory = os.path.isdir(path)
757 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000758 self._is_directory = False
759 return self._is_directory
760
761 def Property(self, property_name):
762 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000763 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000764 return self._properties[property_name]
765
766 def IsTextFile(self):
767 if self._is_text_file is None:
768 if self.Action() == 'D':
769 # A deleted file is not a text file.
770 self._is_text_file = False
771 elif self.IsDirectory():
772 self._is_text_file = False
773 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000774 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
775 return self._is_text_file
776
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000777
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000778class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000779 """Describe a change.
780
781 Used directly by the presubmit scripts to query the current change being
782 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000783
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000784 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000785 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000786 self.KEY: equivalent to tags['KEY']
787 """
788
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000789 _AFFECTED_FILES = AffectedFile
790
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000791 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000792 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000793 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000794 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000795
maruel@chromium.org58407af2011-04-12 23:15:57 +0000796 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000797 self, name, description, local_root, files, issue, patchset, author,
798 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000799 if files is None:
800 files = []
801 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000802 # Convert root into an absolute path.
803 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000804 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000805 self.issue = issue
806 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000807 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000808
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000809 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000810 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000811 self._description_without_tags = ''
812 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000813
maruel@chromium.orge085d812011-10-10 19:49:15 +0000814 assert all(
815 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
816
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000817 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000818 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000819 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
820 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000821 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000822
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000823 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000824 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000825 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000826
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000827 def DescriptionText(self):
828 """Returns the user-entered changelist description, minus tags.
829
830 Any line in the user-provided description starting with e.g. "FOO="
831 (whitespace permitted before and around) is considered a tag line. Such
832 lines are stripped out of the description this function returns.
833 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000834 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835
836 def FullDescriptionText(self):
837 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000838 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000839
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000840 def SetDescriptionText(self, description):
841 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000842
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000843 Also updates the list of tags."""
844 self._full_description = description
845
846 # From the description text, build up a dictionary of key/value pairs
847 # plus the description minus all key/value or "tag" lines.
848 description_without_tags = []
849 self.tags = {}
850 for line in self._full_description.splitlines():
851 m = self.TAG_LINE_RE.match(line)
852 if m:
853 self.tags[m.group('key')] = m.group('value')
854 else:
855 description_without_tags.append(line)
856
857 # Change back to text and remove whitespace at end.
858 self._description_without_tags = (
859 '\n'.join(description_without_tags).rstrip())
860
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000861 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000862 """Returns the repository (checkout) root directory for this change,
863 as an absolute path.
864 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000865 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000866
867 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000868 """Return tags directly as attributes on the object."""
869 if not re.match(r"^[A-Z_]*$", attr):
870 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000871 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000872
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000873 def AllFiles(self, root=None):
874 """List all files under source control in the repo."""
875 raise NotImplementedError()
876
sail@chromium.org5538e022011-05-12 17:53:16 +0000877 def AffectedFiles(self, include_dirs=False, include_deletes=True,
878 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879 """Returns a list of AffectedFile instances for all files in the change.
880
881 Args:
882 include_deletes: If false, deleted files will be filtered out.
883 include_dirs: True to include directories in the list
sail@chromium.org5538e022011-05-12 17:53:16 +0000884 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000885
886 Returns:
887 [AffectedFile(path, action), AffectedFile(path, action)]
888 """
889 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000890 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000891 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000892 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000893
sail@chromium.org5538e022011-05-12 17:53:16 +0000894 affected = filter(file_filter, affected)
895
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000896 if include_deletes:
897 return affected
898 else:
899 return filter(lambda x: x.Action() != 'D', affected)
900
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000901 def AffectedTextFiles(self, include_deletes=None):
902 """Return a list of the existing text files in a change."""
903 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000904 warn("AffectedTextFiles(include_deletes=%s)"
905 " is deprecated and ignored" % str(include_deletes),
906 category=DeprecationWarning,
907 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000908 return filter(lambda x: x.IsTextFile(),
909 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000910
911 def LocalPaths(self, include_dirs=False):
912 """Convenience function."""
913 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
914
915 def AbsoluteLocalPaths(self, include_dirs=False):
916 """Convenience function."""
917 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
918
919 def ServerPaths(self, include_dirs=False):
920 """Convenience function."""
921 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
922
923 def RightHandSideLines(self):
924 """An iterator over all text lines in "new" version of changed files.
925
926 Lists lines from new or modified text files in the change.
927
928 This is useful for doing line-by-line regex checks, like checking for
929 trailing whitespace.
930
931 Yields:
932 a 3 tuple:
933 the AffectedFile instance of the current file;
934 integer line number (1-based); and
935 the contents of the line as a string.
936 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000937 return _RightHandSideLinesImpl(
938 x for x in self.AffectedFiles(include_deletes=False)
939 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000940
941
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000942class SvnChange(Change):
943 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000944 scm = 'svn'
945 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000946
947 def _GetChangeLists(self):
948 """Get all change lists."""
949 if self._changelists == None:
950 previous_cwd = os.getcwd()
951 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000952 # Need to import here to avoid circular dependency.
953 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000954 self._changelists = gcl.GetModifiedFiles()
955 os.chdir(previous_cwd)
956 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000957
958 def GetAllModifiedFiles(self):
959 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000960 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000961 all_modified_files = []
962 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000963 all_modified_files.extend(
964 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000965 return all_modified_files
966
967 def GetModifiedFiles(self):
968 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000969 changelists = self._GetChangeLists()
970 return [os.path.join(self.RepositoryRoot(), f[1])
971 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000972
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000973 def AllFiles(self, root=None):
974 """List all files under source control in the repo."""
975 root = root or self.RepositoryRoot()
976 return subprocess.check_output(
977 ['svn', 'ls', '-R', '.'], cwd=root).splitlines()
978
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000979
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000980class GitChange(Change):
981 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000982 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000983
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000984 def AllFiles(self, root=None):
985 """List all files under source control in the repo."""
986 root = root or self.RepositoryRoot()
987 return subprocess.check_output(
988 ['git', 'ls-files', '--', '.'], cwd=root).splitlines()
989
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000990
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000991def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000992 """Finds all presubmit files that apply to a given set of source files.
993
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000994 If inherit-review-settings-ok is present right under root, looks for
995 PRESUBMIT.py in directories enclosing root.
996
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000997 Args:
998 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000999 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001000
1001 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001002 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001003 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001004 files = [normpath(os.path.join(root, f)) for f in files]
1005
1006 # List all the individual directories containing files.
1007 directories = set([os.path.dirname(f) for f in files])
1008
1009 # Ignore root if inherit-review-settings-ok is present.
1010 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
1011 root = None
1012
1013 # Collect all unique directories that may contain PRESUBMIT.py.
1014 candidates = set()
1015 for directory in directories:
1016 while True:
1017 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001018 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001019 candidates.add(directory)
1020 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001021 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001022 parent_dir = os.path.dirname(directory)
1023 if parent_dir == directory:
1024 # We hit the system root directory.
1025 break
1026 directory = parent_dir
1027
1028 # Look for PRESUBMIT.py in all candidate directories.
1029 results = []
1030 for directory in sorted(list(candidates)):
1031 p = os.path.join(directory, 'PRESUBMIT.py')
1032 if os.path.isfile(p):
1033 results.append(p)
1034
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001035 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001036 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037
1038
thestig@chromium.orgde243452009-10-06 21:02:56 +00001039class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001040 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001041 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001042 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001043
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001044 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001045
1046 Args:
1047 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001048 presubmit_path: Project script to run.
1049 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001050
1051 Return:
1052 A list of try slaves.
1053 """
1054 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001055 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001056 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001057 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001058 exec script_text in context
1059 except Exception, e:
1060 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001061 finally:
1062 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001063
1064 function_name = 'GetPreferredTrySlaves'
1065 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001066 get_preferred_try_slaves = context[function_name]
1067 function_info = inspect.getargspec(get_preferred_try_slaves)
1068 if len(function_info[0]) == 1:
1069 result = get_preferred_try_slaves(project)
1070 elif len(function_info[0]) == 2:
1071 result = get_preferred_try_slaves(project, change)
1072 else:
1073 result = get_preferred_try_slaves()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001074 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001075 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001076 'Presubmit functions must return a list, got a %s instead: %s' %
1077 (type(result), str(result)))
1078 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001079 if isinstance(item, basestring):
1080 # Old-style ['bot'] format.
1081 botname = item
1082 elif isinstance(item, tuple):
1083 # New-style [('bot', set(['tests']))] format.
1084 botname = item[0]
1085 else:
1086 raise PresubmitFailure('PRESUBMIT.py returned invalid tryslave/test'
1087 ' format.')
1088
1089 if botname != botname.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001090 raise PresubmitFailure(
1091 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001092 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001093 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001094 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001095 else:
1096 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001097
1098 def valid_oldstyle(result):
1099 return all(isinstance(i, basestring) for i in result)
1100
1101 def valid_newstyle(result):
1102 return (all(isinstance(i, tuple) for i in result) and
1103 all(len(i) == 2 for i in result) and
1104 all(isinstance(i[0], basestring) for i in result) and
1105 all(isinstance(i[1], set) for i in result)
1106 )
1107
1108 # Ensure it's either all old-style or all new-style.
1109 if not valid_oldstyle(result) and not valid_newstyle(result):
1110 raise PresubmitFailure(
1111 'PRESUBMIT.py returned invalid trybot specification!')
1112
thestig@chromium.orgde243452009-10-06 21:02:56 +00001113 return result
1114
1115
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001116class GetTryMastersExecuter(object):
1117 @staticmethod
1118 def ExecPresubmitScript(script_text, presubmit_path, project, change):
1119 """Executes GetPreferredTryMasters() from a single presubmit script.
1120
1121 Args:
1122 script_text: The text of the presubmit script.
1123 presubmit_path: Project script to run.
1124 project: Project name to pass to presubmit script for bot selection.
1125
1126 Return:
1127 A map of try masters to map of builders to set of tests.
1128 """
1129 context = {}
1130 try:
1131 exec script_text in context
1132 except Exception, e:
1133 raise PresubmitFailure('"%s" had an exception.\n%s'
1134 % (presubmit_path, e))
1135
1136 function_name = 'GetPreferredTryMasters'
1137 if function_name not in context:
1138 return {}
1139 get_preferred_try_masters = context[function_name]
1140 if not len(inspect.getargspec(get_preferred_try_masters)[0]) == 2:
1141 raise PresubmitFailure(
1142 'Expected function "GetPreferredTryMasters" to take two arguments.')
1143 return get_preferred_try_masters(project, change)
1144
1145
rmistry@google.com5626a922015-02-26 14:03:30 +00001146class GetPostUploadExecuter(object):
1147 @staticmethod
1148 def ExecPresubmitScript(script_text, presubmit_path, cl, change):
1149 """Executes PostUploadHook() from a single presubmit script.
1150
1151 Args:
1152 script_text: The text of the presubmit script.
1153 presubmit_path: Project script to run.
1154 cl: The Changelist object.
1155 change: The Change object.
1156
1157 Return:
1158 A list of results objects.
1159 """
1160 context = {}
1161 try:
1162 exec script_text in context
1163 except Exception, e:
1164 raise PresubmitFailure('"%s" had an exception.\n%s'
1165 % (presubmit_path, e))
1166
1167 function_name = 'PostUploadHook'
1168 if function_name not in context:
1169 return {}
1170 post_upload_hook = context[function_name]
1171 if not len(inspect.getargspec(post_upload_hook)[0]) == 3:
1172 raise PresubmitFailure(
1173 'Expected function "PostUploadHook" to take three arguments.')
1174 return post_upload_hook(cl, change, OutputApi(False))
1175
1176
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001177def DoGetTrySlaves(change,
1178 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001179 repository_root,
1180 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001181 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001182 verbose,
1183 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001184 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001185
1186 Args:
1187 changed_files: List of modified files.
1188 repository_root: The repository root.
1189 default_presubmit: A default presubmit script to execute in any case.
bradnelson@google.com78230022011-05-24 18:55:19 +00001190 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001191 verbose: Prints debug info.
1192 output_stream: A stream to write debug output to.
1193
1194 Return:
1195 List of try slaves
1196 """
1197 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1198 if not presubmit_files and verbose:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001199 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001200 results = []
1201 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001202
thestig@chromium.orgde243452009-10-06 21:02:56 +00001203 if default_presubmit:
1204 if verbose:
1205 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001206 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001207 results.extend(executer.ExecPresubmitScript(
1208 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001209 for filename in presubmit_files:
1210 filename = os.path.abspath(filename)
1211 if verbose:
1212 output_stream.write("Running %s\n" % filename)
1213 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001214 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001215 results.extend(executer.ExecPresubmitScript(
1216 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001217
stip@chromium.org5ca27622013-12-18 17:44:58 +00001218
1219 slave_dict = {}
1220 old_style = filter(lambda x: isinstance(x, basestring), results)
1221 new_style = filter(lambda x: isinstance(x, tuple), results)
1222
1223 for result in new_style:
1224 slave_dict.setdefault(result[0], set()).update(result[1])
1225 slaves = list(slave_dict.items())
1226
1227 slaves.extend(set(old_style))
stip@chromium.org68e04192013-11-04 22:14:38 +00001228
thestig@chromium.orgde243452009-10-06 21:02:56 +00001229 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001230 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001231 output_stream.write('\n')
1232 return slaves
1233
1234
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001235def _MergeMasters(masters1, masters2):
1236 """Merges two master maps. Merges also the tests of each builder."""
1237 result = {}
1238 for (master, builders) in itertools.chain(masters1.iteritems(),
1239 masters2.iteritems()):
1240 new_builders = result.setdefault(master, {})
1241 for (builder, tests) in builders.iteritems():
1242 new_builders.setdefault(builder, set([])).update(tests)
1243 return result
1244
1245
1246def DoGetTryMasters(change,
1247 changed_files,
1248 repository_root,
1249 default_presubmit,
1250 project,
1251 verbose,
1252 output_stream):
1253 """Get the list of try masters from the presubmit scripts.
1254
1255 Args:
1256 changed_files: List of modified files.
1257 repository_root: The repository root.
1258 default_presubmit: A default presubmit script to execute in any case.
1259 project: Optional name of a project used in selecting trybots.
1260 verbose: Prints debug info.
1261 output_stream: A stream to write debug output to.
1262
1263 Return:
1264 Map of try masters to map of builders to set of tests.
1265 """
1266 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
1267 if not presubmit_files and verbose:
1268 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1269 results = {}
1270 executer = GetTryMastersExecuter()
1271
1272 if default_presubmit:
1273 if verbose:
1274 output_stream.write("Running default presubmit script.\n")
1275 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
1276 results = _MergeMasters(results, executer.ExecPresubmitScript(
1277 default_presubmit, fake_path, project, change))
1278 for filename in presubmit_files:
1279 filename = os.path.abspath(filename)
1280 if verbose:
1281 output_stream.write("Running %s\n" % filename)
1282 # Accept CRLF presubmit script.
1283 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1284 results = _MergeMasters(results, executer.ExecPresubmitScript(
1285 presubmit_script, filename, project, change))
1286
1287 # Make sets to lists again for later JSON serialization.
1288 for builders in results.itervalues():
1289 for builder in builders:
1290 builders[builder] = list(builders[builder])
1291
1292 if results and verbose:
1293 output_stream.write('%s\n' % str(results))
1294 return results
1295
1296
rmistry@google.com5626a922015-02-26 14:03:30 +00001297def DoPostUploadExecuter(change,
1298 cl,
1299 repository_root,
1300 verbose,
1301 output_stream):
1302 """Execute the post upload hook.
1303
1304 Args:
1305 change: The Change object.
1306 cl: The Changelist object.
1307 repository_root: The repository root.
1308 verbose: Prints debug info.
1309 output_stream: A stream to write debug output to.
1310 """
1311 presubmit_files = ListRelevantPresubmitFiles(
1312 change.LocalPaths(), repository_root)
1313 if not presubmit_files and verbose:
1314 output_stream.write("Warning, no PRESUBMIT.py found.\n")
1315 results = []
1316 executer = GetPostUploadExecuter()
1317 # The root presubmit file should be executed after the ones in subdirectories.
1318 # i.e. the specific post upload hooks should run before the general ones.
1319 # Thus, reverse the order provided by ListRelevantPresubmitFiles.
1320 presubmit_files.reverse()
1321
1322 for filename in presubmit_files:
1323 filename = os.path.abspath(filename)
1324 if verbose:
1325 output_stream.write("Running %s\n" % filename)
1326 # Accept CRLF presubmit script.
1327 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1328 results.extend(executer.ExecPresubmitScript(
1329 presubmit_script, filename, cl, change))
1330 output_stream.write('\n')
1331 if results:
1332 output_stream.write('** Post Upload Hook Messages **\n')
1333 for result in results:
1334 result.handle(output_stream)
1335 output_stream.write('\n')
1336
1337 return results
1338
1339
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001340class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001341 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001342 """
1343 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001344 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001345 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001346 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001347 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001348 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001349 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001350 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001351 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001352
1353 def ExecPresubmitScript(self, script_text, presubmit_path):
1354 """Executes a single presubmit script.
1355
1356 Args:
1357 script_text: The text of the presubmit script.
1358 presubmit_path: The path to the presubmit file (this will be reported via
1359 input_api.PresubmitLocalPath()).
1360
1361 Return:
1362 A list of result objects, empty if no problems.
1363 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001364
chase@chromium.org8e416c82009-10-06 04:30:44 +00001365 # Change to the presubmit file's directory to support local imports.
1366 main_path = os.getcwd()
1367 os.chdir(os.path.dirname(presubmit_path))
1368
1369 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001370 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001371 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001372 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001373 try:
1374 exec script_text in context
1375 except Exception, e:
1376 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001377
1378 # These function names must change if we make substantial changes to
1379 # the presubmit API that are not backwards compatible.
1380 if self.committing:
1381 function_name = 'CheckChangeOnCommit'
1382 else:
1383 function_name = 'CheckChangeOnUpload'
1384 if function_name in context:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001385 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001386 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001387 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001388 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001389 if not (isinstance(result, types.TupleType) or
1390 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001391 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001392 'Presubmit functions must return a tuple or list')
1393 for item in result:
1394 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001395 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001396 'All presubmit results must be of types derived from '
1397 'output_api.PresubmitResult')
1398 else:
1399 result = () # no error since the script doesn't care about current event.
1400
chase@chromium.org8e416c82009-10-06 04:30:44 +00001401 # Return the process to the original working directory.
1402 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001403 return result
1404
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001405
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001406def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001407 committing,
1408 verbose,
1409 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001410 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001411 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001412 may_prompt,
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001413 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001414 """Runs all presubmit checks that apply to the files in the change.
1415
1416 This finds all PRESUBMIT.py files in directories enclosing the files in the
1417 change (up to the repository root) and calls the relevant entrypoint function
1418 depending on whether the change is being committed or uploaded.
1419
1420 Prints errors, warnings and notifications. Prompts the user for warnings
1421 when needed.
1422
1423 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001424 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001425 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1426 verbose: Prints debug info.
1427 output_stream: A stream to write output from presubmit tests to.
1428 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001429 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001430 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001431 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001432
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001433 Warning:
1434 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1435 SHOULD be sys.stdin.
1436
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001437 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001438 A PresubmitOutput object. Use output.should_continue() to figure out
1439 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001440 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001441 old_environ = os.environ
1442 try:
1443 # Make sure python subprocesses won't generate .pyc files.
1444 os.environ = os.environ.copy()
1445 os.environ['PYTHONDONTWRITEBYTECODE'] = '1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001446
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001447 output = PresubmitOutput(input_stream, output_stream)
1448 if committing:
1449 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001450 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001451 output.write("Running presubmit upload checks ...\n")
1452 start_time = time.time()
1453 presubmit_files = ListRelevantPresubmitFiles(
1454 change.AbsoluteLocalPaths(True), change.RepositoryRoot())
1455 if not presubmit_files and verbose:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001456 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001457 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001458 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001459 if default_presubmit:
1460 if verbose:
1461 output.write("Running default presubmit script.\n")
1462 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
1463 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
1464 for filename in presubmit_files:
1465 filename = os.path.abspath(filename)
1466 if verbose:
1467 output.write("Running %s\n" % filename)
1468 # Accept CRLF presubmit script.
1469 presubmit_script = gclient_utils.FileRead(filename, 'rU')
1470 results += executer.ExecPresubmitScript(presubmit_script, filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001471
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001472 errors = []
1473 notifications = []
1474 warnings = []
1475 for result in results:
1476 if result.fatal:
1477 errors.append(result)
1478 elif result.should_prompt:
1479 warnings.append(result)
1480 else:
1481 notifications.append(result)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001482
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001483 output.write('\n')
1484 for name, items in (('Messages', notifications),
1485 ('Warnings', warnings),
1486 ('ERRORS', errors)):
1487 if items:
1488 output.write('** Presubmit %s **\n' % name)
1489 for item in items:
1490 item.handle(output)
1491 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001492
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001493 total_time = time.time() - start_time
1494 if total_time > 1.0:
1495 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001496
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001497 if not errors:
1498 if not warnings:
1499 output.write('Presubmit checks passed.\n')
1500 elif may_prompt:
1501 output.prompt_yes_no('There were presubmit warnings. '
1502 'Are you sure you wish to continue? (y/N): ')
1503 else:
1504 output.fail()
1505
1506 global _ASKED_FOR_FEEDBACK
1507 # Ask for feedback one time out of 5.
1508 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001509 output.write(
1510 'Was the presubmit check useful? If not, run "git cl presubmit -v"\n'
1511 'to figure out which PRESUBMIT.py was run, then run git blame\n'
1512 'on the file to figure out who to ask for help.\n')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001513 _ASKED_FOR_FEEDBACK = True
1514 return output
1515 finally:
1516 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001517
1518
1519def ScanSubDirs(mask, recursive):
1520 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001521 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001522 else:
1523 results = []
1524 for root, dirs, files in os.walk('.'):
1525 if '.svn' in dirs:
1526 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001527 if '.git' in dirs:
1528 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001529 for name in files:
1530 if fnmatch.fnmatch(name, mask):
1531 results.append(os.path.join(root, name))
1532 return results
1533
1534
1535def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001536 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001537 files = []
1538 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001539 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001540 return files
1541
1542
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001543def load_files(options, args):
1544 """Tries to determine the SCM."""
1545 change_scm = scm.determine_scm(options.root)
1546 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001547 if args:
1548 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001549 if change_scm == 'svn':
1550 change_class = SvnChange
1551 if not files:
1552 files = scm.SVN.CaptureStatus([], options.root)
1553 elif change_scm == 'git':
1554 change_class = GitChange
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001555 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001556 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001557 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001558 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001559 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1560 if not files:
1561 return None, None
1562 change_class = Change
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001563 return change_class, files
1564
1565
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001566class NonexistantCannedCheckFilter(Exception):
1567 pass
1568
1569
1570@contextlib.contextmanager
1571def canned_check_filter(method_names):
1572 filtered = {}
1573 try:
1574 for method_name in method_names:
1575 if not hasattr(presubmit_canned_checks, method_name):
1576 raise NonexistantCannedCheckFilter(method_name)
1577 filtered[method_name] = getattr(presubmit_canned_checks, method_name)
1578 setattr(presubmit_canned_checks, method_name, lambda *_a, **_kw: [])
1579 yield
1580 finally:
1581 for name, method in filtered.iteritems():
1582 setattr(presubmit_canned_checks, name, method)
1583
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001584
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001585def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001586 """Runs an external program, potentially from a child process created by the
1587 multiprocessing module.
1588
1589 multiprocessing needs a top level function with a single argument.
1590 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001591 cmd_data.kwargs['stdout'] = subprocess.PIPE
1592 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1593 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001594 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001595 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001596 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001597 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001598 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001599 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001600 '%s exec failure (%4.2fs)\n %s' % (cmd_data.name, duration, e))
1601 if code != 0:
1602 return cmd_data.message(
1603 '%s (%4.2fs) failed\n%s' % (cmd_data.name, duration, out))
1604 if cmd_data.info:
1605 return cmd_data.info('%s (%4.2fs)' % (cmd_data.name, duration))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001606
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001607
sbc@chromium.org013731e2015-02-26 18:28:43 +00001608def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001609 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001610 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001611 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001612 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001613 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1614 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001615 parser.add_option("-r", "--recursive", action="store_true",
1616 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001617 parser.add_option("-v", "--verbose", action="count", default=0,
1618 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001619 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001620 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001621 parser.add_option("--description", default='')
1622 parser.add_option("--issue", type='int', default=0)
1623 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001624 parser.add_option("--root", default=os.getcwd(),
1625 help="Search for PRESUBMIT.py up to this directory. "
1626 "If inherit-review-settings-ok is present in this "
1627 "directory, parent directories up to the root file "
1628 "system directories will also be searched.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001629 parser.add_option("--upstream",
1630 help="Git only: the base ref or upstream branch against "
1631 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001632 parser.add_option("--default_presubmit")
1633 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001634 parser.add_option("--skip_canned", action='append', default=[],
1635 help="A list of checks to skip which appear in "
1636 "presubmit_canned_checks. Can be provided multiple times "
1637 "to skip multiple canned checks.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001638 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1639 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
1640 parser.add_option("--rietveld_password", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001641 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1642 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001643 # These are for OAuth2 authentication for bots. See also apply_issue.py
1644 parser.add_option("--rietveld_email_file", help=optparse.SUPPRESS_HELP)
1645 parser.add_option("--rietveld_private_key_file", help=optparse.SUPPRESS_HELP)
1646
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001647 parser.add_option("--trybot-json",
1648 help="Output trybot information to the file specified.")
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001649 options, args = parser.parse_args(argv)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001650
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001651 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001652 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001653 elif options.verbose:
1654 logging.basicConfig(level=logging.INFO)
1655 else:
1656 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001657
1658 if options.rietveld_email and options.rietveld_email_file:
1659 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1660 "can be passed to this program.")
1661 if options.rietveld_private_key_file and options.rietveld_password:
1662 parser.error("Only one of --rietveld_private_key_file or "
1663 "--rietveld_password can be passed to this program.")
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001664
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001665 if options.rietveld_email_file:
1666 with open(options.rietveld_email_file, "rb") as f:
1667 options.rietveld_email = f.read().strip()
1668
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001669 change_class, files = load_files(options, args)
1670 if not change_class:
1671 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001672 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001673
maruel@chromium.org239f4112011-06-03 20:08:23 +00001674 rietveld_obj = None
1675 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001676 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001677 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001678 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1679 options.rietveld_url,
1680 options.rietveld_email,
1681 options.rietveld_private_key_file)
1682 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001683 rietveld_obj = rietveld.CachingRietveld(
1684 options.rietveld_url,
1685 options.rietveld_email,
1686 options.rietveld_password)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001687 if options.rietveld_fetch:
1688 assert options.issue
1689 props = rietveld_obj.get_issue_properties(options.issue, False)
1690 options.author = props['owner_email']
1691 options.description = props['description']
1692 logging.info('Got author: "%s"', options.author)
1693 logging.info('Got description: """\n%s\n"""', options.description)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001694 if options.trybot_json:
1695 with open(options.trybot_json, 'w') as f:
1696 # Python's sets aren't JSON-encodable, so we convert them to lists here.
1697 class SetEncoder(json.JSONEncoder):
1698 # pylint: disable=E0202
1699 def default(self, obj):
1700 if isinstance(obj, set):
1701 return sorted(obj)
1702 return json.JSONEncoder.default(self, obj)
1703 change = change_class(options.name,
1704 options.description,
1705 options.root,
1706 files,
1707 options.issue,
1708 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001709 options.author,
1710 upstream=options.upstream)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001711 trybots = DoGetTrySlaves(
1712 change,
1713 change.LocalPaths(),
1714 change.RepositoryRoot(),
1715 None,
1716 None,
1717 options.verbose,
1718 sys.stdout)
1719 json.dump(trybots, f, cls=SetEncoder)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001720 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001721 with canned_check_filter(options.skip_canned):
1722 results = DoPresubmitChecks(
1723 change_class(options.name,
1724 options.description,
1725 options.root,
1726 files,
1727 options.issue,
1728 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001729 options.author,
1730 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001731 options.commit,
1732 options.verbose,
1733 sys.stdout,
1734 sys.stdin,
1735 options.default_presubmit,
1736 options.may_prompt,
1737 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001738 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001739 except NonexistantCannedCheckFilter, e:
1740 print >> sys.stderr, (
1741 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1742 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001743 except PresubmitFailure, e:
1744 print >> sys.stderr, e
1745 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1746 print >> sys.stderr, 'If all fails, contact maruel@'
1747 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001748
1749
1750if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001751 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001752 try:
1753 sys.exit(main())
1754 except KeyboardInterrupt:
1755 sys.stderr.write('interrupted\n')
1756 sys.exit(1)