blob: 3f873c0d83af11246dba1c788b6ff6d24912b102 [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.
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
maruel@chromium.org35625c72011-03-23 17:34:02 +000043import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000044import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000045import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000046import presubmit_canned_checks
maruel@chromium.org239f4112011-06-03 20:08:23 +000047import rietveld
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000048import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000049import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000050
51
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000052# Ask for feedback only once in program lifetime.
53_ASKED_FOR_FEEDBACK = False
54
55
maruel@chromium.org899e1c12011-04-07 17:03:18 +000056class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000057 pass
58
59
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +000060class 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
ilevy@chromium.orgbc117312013-04-20 03:57:56 +000068
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000069def 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
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000079def _RightHandSideLinesImpl(affected_files):
80 """Implements RightHandSideLines for InputApi and GclChange."""
81 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000082 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000083 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000084 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000085
86
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000087class 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
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000119# 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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000199class OutputApi(object):
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000200 """An instance of OutputApi gets passed to presubmit scripts so that they
201 can output various types of results.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000202 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000203 PresubmitResult = _PresubmitResult
204 PresubmitAddReviewers = _PresubmitAddReviewers
205 PresubmitError = _PresubmitError
206 PresubmitPromptWarning = _PresubmitPromptWarning
207 PresubmitNotifyResult = _PresubmitNotifyResult
208 MailTextResult = _MailTextResult
209
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000210 def __init__(self, is_committing):
211 self.is_committing = is_committing
212
wez@chromium.orga6d011e2013-03-26 17:31:49 +0000213 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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000219
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 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000224 # Method could be a function
225 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000226
maruel@chromium.org3410d912009-06-09 20:56:16 +0000227 # File extensions that are considered source files from a style guide
228 # perspective. Don't modify this list from a presubmit script!
maruel@chromium.orgc33455a2011-06-24 19:14:18 +0000229 #
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.
maruel@chromium.org3410d912009-06-09 20:56:16 +0000233 DEFAULT_WHITE_LIST = (
234 # C++ and friends
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000235 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$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000237 # Scripts
maruel@chromium.orgfe1211a2011-05-28 18:54:17 +0000238 r".+\.js$", r".+\.py$", r".+\.sh$", r".+\.rb$", r".+\.pl$", r".+\.pm$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000239 # Other
estade@chromium.orgae7af922012-01-27 14:51:13 +0000240 r".+\.java$", r".+\.mk$", r".+\.am$", r".+\.css$"
maruel@chromium.org3410d912009-06-09 20:56:16 +0000241 )
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 = (
gavinp@chromium.org656326d2012-08-13 00:43:57 +0000246 r"testing_support[\\\/]google_appengine[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000247 r".*\bexperimental[\\\/].*",
primiano@chromium.orgb9658c32015-10-06 10:50:13 +0000248 # Exclude third_party/.* but NOT third_party/WebKit (crbug.com/539768).
249 r".*\bthird_party[\\\/](?!WebKit[\\\/]).*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000250 # Output directories (just in case)
251 r".*\bDebug[\\\/].*",
252 r".*\bRelease[\\\/].*",
253 r".*\bxcodebuild[\\\/].*",
thakis@chromium.orgc1c96352013-10-09 19:50:27 +0000254 r".*\bout[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000255 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000256 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000257 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000258 r"(|.*[\\\/])\.git[\\\/].*",
259 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org7ccb4bb2011-11-07 20:26:20 +0000260 # There is no point in processing a patch file.
261 r".+\.diff$",
262 r".+\.patch$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000263 )
264
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000265 def __init__(self, change, presubmit_path, is_committing,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000266 rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000267 """Builds an InputApi object.
268
269 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000270 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000271 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000272 is_committing: True if the change is about to be committed.
maruel@chromium.org239f4112011-06-03 20:08:23 +0000273 rietveld_obj: rietveld.Rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000274 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000275 # Version number of the presubmit_support script.
276 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000277 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000278 self.is_committing = is_committing
maruel@chromium.org239f4112011-06-03 20:08:23 +0000279 self.rietveld = rietveld_obj
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000280 # TBD
281 self.host_url = 'http://codereview.chromium.org'
282 if self.rietveld:
maruel@chromium.org239f4112011-06-03 20:08:23 +0000283 self.host_url = self.rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000284
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
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000289 self.cpplint = cpplint
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000290 self.cStringIO = cStringIO
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000291 self.glob = glob.glob
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000292 self.json = json
maruel@chromium.org6fba34d2011-06-02 13:45:12 +0000293 self.logging = logging.getLogger('PRESUBMIT')
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000294 self.os_listdir = os.listdir
295 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000296 self.os_path = os.path
pgervais@chromium.orgbd0cace2014-10-02 23:23:46 +0000297 self.os_stat = os.stat
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000298 self.pickle = pickle
299 self.marshal = marshal
300 self.re = re
301 self.subprocess = subprocess
302 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000303 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000304 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000305 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000306 self.urllib2 = urllib2
307
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000308 # To easily fork python.
309 self.python_executable = sys.executable
310 self.environ = os.environ
311
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000312 # InputApi.platform is the platform you're currently running on.
313 self.platform = sys.platform
314
iannucci@chromium.org0af3bb32015-06-12 20:44:35 +0000315 self.cpu_count = multiprocessing.cpu_count()
316
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000317 # 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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000323 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000324 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000325
326 # We carry the canned checks so presubmit scripts can easily use them.
327 self.canned_checks = presubmit_canned_checks
328
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000329 # 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(),
dpranke@chromium.org17cc2442012-10-17 21:12:09 +0000332 fopen=file, os_path=self.os_path, glob=self.glob)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000333 self.verbose = verbose
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000334 self.Command = CommandData
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000335
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000336 # Replace <hash_map> and <hash_set> as headers that need to be included
danakj@chromium.org18278522013-06-11 22:42:32 +0000337 # with "base/containers/hash_tables.h" instead.
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000338 # Access to a protected member _XX of a client class
339 # pylint: disable=W0212
340 self.cpplint._re_pattern_templates = [
danakj@chromium.org18278522013-06-11 22:42:32 +0000341 (a, b, 'base/containers/hash_tables.h')
enne@chromium.orge72c5f52013-04-16 00:36:40 +0000342 if header in ('<hash_map>', '<hash_set>') else (a, b, header)
343 for (a, b, header) in cpplint._re_pattern_templates
344 ]
345
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000346 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 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000354 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000355
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000356 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000357 """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 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000368 return scm.SVN.CaptureLocalInfo([depot_path], self.change.RepositoryRoot()
369 ).get('Path')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000370
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000371 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000372 """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 """
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000380 return scm.SVN.CaptureLocalInfo([local_path], self.change.RepositoryRoot()
381 ).get('URL')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000382
sail@chromium.org5538e022011-05-12 17:53:16 +0000383 def AffectedFiles(self, include_dirs=False, include_deletes=True,
384 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385 """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 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000389 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000390 if len(dir_with_slash) == 1:
391 dir_with_slash = ''
sail@chromium.org5538e022011-05-12 17:53:16 +0000392
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000393 return filter(
394 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
sail@chromium.org5538e022011-05-12 17:53:16 +0000395 self.change.AffectedFiles(include_dirs, include_deletes, file_filter))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000396
397 def LocalPaths(self, include_dirs=False):
398 """Returns local paths of input_api.AffectedFiles()."""
pgervais@chromium.org2f64f782014-04-25 00:12:33 +0000399 paths = [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
400 logging.debug("LocalPaths: %s", paths)
401 return paths
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000402
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
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000411 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000412 """Same as input_api.change.AffectedTextFiles() except only lists files
413 in the same directory as the current presubmit script, or subdirectories
414 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000416 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000417 warn("AffectedTextFiles(include_deletes=%s)"
418 " is deprecated and ignored" % str(include_deletes),
419 category=DeprecationWarning,
420 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000421 return filter(lambda x: x.IsTextFile(),
422 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000423
maruel@chromium.org3410d912009-06-09 20:56:16 +0000424 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 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000435 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000436 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000437 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000438 if self.re.match(item, local_path):
439 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000440 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):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000455 """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.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000468
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000469 Note: The carriage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000470 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000471 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000472 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000473
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000474 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000475 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000476
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000477 Deny reading anything outside the repository.
478 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000479 if isinstance(file_item, AffectedFile):
480 file_item = file_item.AbsoluteLocalPath()
481 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000482 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000483 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000484
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000485 @property
486 def tbr(self):
487 """Returns if a change is TBR'ed."""
488 return 'TBR' in self.change.tags
489
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000490 def RunTests(self, tests_mix, parallel=True):
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000491 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)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +0000499 if self.verbose:
500 t.info = _PresubmitNotifyResult
ilevy@chromium.org5678d332013-05-18 01:34:14 +0000501 if len(tests) > 1 and parallel:
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000502 # async recipe works around multiprocessing bug handling Ctrl-C
iannucci@chromium.orgd61a4952015-07-01 23:21:26 +0000503 msgs.extend(self._run_tests_pool.map_async(CallCommand, tests).get(99999))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +0000504 else:
505 msgs.extend(map(CallCommand, tests))
506 return [m for m in msgs if m]
507
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000508
nick@chromium.orgff526192013-06-10 19:30:26 +0000509class _DiffCache(object):
510 """Caches diffs retrieved from a particular SCM."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000511 def __init__(self, upstream=None):
512 """Stores the upstream revision against which all diffs will be computed."""
513 self._upstream = upstream
nick@chromium.orgff526192013-06-10 19:30:26 +0000514
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."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000522 def __init__(self, *args, **kwargs):
523 super(_SvnDiffCache, self).__init__(*args, **kwargs)
nick@chromium.orgff526192013-06-10 19:30:26 +0000524 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."""
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000535 def __init__(self, upstream):
536 super(_GitDiffCache, self).__init__(upstream=upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000537 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.
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000547 unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True,
548 branch=self._upstream)
nick@chromium.orgff526192013-06-10 19:30:26 +0000549
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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000575class AffectedFile(object):
576 """Representation of a file in a change."""
nick@chromium.orgff526192013-06-10 19:30:26 +0000577
578 DIFF_CACHE = _DiffCache
579
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000580 # Method could be a function
581 # pylint: disable=R0201
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000582 def __init__(self, path, action, repository_root, diff_cache):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000583 self._path = path
584 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000585 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000586 self._is_directory = None
587 self._properties = {}
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000588 self._cached_changed_contents = None
589 self._cached_new_contents = None
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000590 self._diff_cache = diff_cache
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000591 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000592
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 """
nick@chromium.orgff526192013-06-10 19:30:26 +0000598 return ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000599
600 def LocalPath(self):
601 """Returns the path of this file on the local disk relative to client root.
602 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000603 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000604
605 def AbsoluteLocalPath(self):
606 """Returns the absolute path of this file on the local disk.
607 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000608 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000609
610 def IsDirectory(self):
611 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000612 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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000617
618 def Action(self):
619 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000620 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
621 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000622 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000623
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000624 def Property(self, property_name):
625 """Returns the specified SCM property of this file, or None if no such
626 property.
627 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000628 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000629
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000630 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000631 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000632
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000633 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000634 raise NotImplementedError() # Implement when needed
635
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000636 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.
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000643 Note: The carriage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000644 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000645 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[:]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000654
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000655 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 """
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000663 if self._cached_changed_contents is not None:
664 return self._cached_changed_contents[:]
665 self._cached_changed_contents = []
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000666 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('++'):
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000677 self._cached_changed_contents.append((line_num, line[1:]))
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000678 if not line.startswith('-'):
679 line_num += 1
nick@chromium.org2a3ab7e2011-04-27 22:06:27 +0000680 return self._cached_changed_contents[:]
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000681
maruel@chromium.org5de13972009-06-10 18:16:06 +0000682 def __str__(self):
683 return self.LocalPath()
684
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000685 def GenerateScmDiff(self):
nick@chromium.orgff526192013-06-10 19:30:26 +0000686 return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000687
maruel@chromium.org58407af2011-04-12 23:15:57 +0000688
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000689class SvnAffectedFile(AffectedFile):
690 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000691 # Method 'NNN' is abstract in class 'NNN' but is not overridden
692 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000693
nick@chromium.orgff526192013-06-10 19:30:26 +0000694 DIFF_CACHE = _SvnDiffCache
695
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000696 def __init__(self, *args, **kwargs):
697 AffectedFile.__init__(self, *args, **kwargs)
698 self._server_path = None
699 self._is_text_file = None
700
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000701 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000702 if self._server_path is None:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000703 self._server_path = scm.SVN.CaptureLocalInfo(
704 [self.LocalPath()], self._local_root).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000705 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000706
707 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000708 if self._is_directory is None:
709 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000710 if os.path.exists(path):
711 # Retrieve directly from the file system; it is much faster than
712 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000713 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000714 else:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000715 self._is_directory = scm.SVN.CaptureLocalInfo(
716 [self.LocalPath()], self._local_root
717 ).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000718 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000719
720 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000721 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000722 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000723 self.LocalPath(), property_name, self._local_root).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000724 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000725
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000726 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000727 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:
maruel@chromium.orgd579fcf2011-12-13 20:36:03 +0000734 mime_type = scm.SVN.GetFileProperty(
735 self.LocalPath(), 'svn:mime-type', self._local_root)
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000736 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
737 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000738
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000739
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000740class GitAffectedFile(AffectedFile):
741 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000742 # Method 'NNN' is abstract in class 'NNN' but is not overridden
743 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000744
nick@chromium.orgff526192013-06-10 19:30:26 +0000745 DIFF_CACHE = _GitDiffCache
746
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000747 def __init__(self, *args, **kwargs):
748 AffectedFile.__init__(self, *args, **kwargs)
749 self._server_path = None
750 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000751
752 def ServerPath(self):
753 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000754 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000755 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:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000765 self._is_directory = False
766 return self._is_directory
767
768 def Property(self, property_name):
769 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000770 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000771 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:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000781 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
782 return self._is_text_file
783
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000784
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000785class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000786 """Describe a change.
787
788 Used directly by the presubmit scripts to query the current change being
789 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000790
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000791 Instance members:
nick@chromium.orgff526192013-06-10 19:30:26 +0000792 tags: Dictionary of KEY=VALUE pairs found in the change description.
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000793 self.KEY: equivalent to tags['KEY']
794 """
795
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000796 _AFFECTED_FILES = AffectedFile
797
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000798 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org428342a2011-11-10 15:46:33 +0000799 TAG_LINE_RE = re.compile(
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000800 '^[ \t]*(?P<key>[A-Z][A-Z_0-9]*)[ \t]*=[ \t]*(?P<value>.*?)[ \t]*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000801 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000802
maruel@chromium.org58407af2011-04-12 23:15:57 +0000803 def __init__(
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000804 self, name, description, local_root, files, issue, patchset, author,
805 upstream=None):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000806 if files is None:
807 files = []
808 self._name = name
chase@chromium.org8e416c82009-10-06 04:30:44 +0000809 # Convert root into an absolute path.
810 self._local_root = os.path.abspath(local_root)
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000811 self._upstream = upstream
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000812 self.issue = issue
813 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000814 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000815
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000816 self._full_description = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817 self.tags = {}
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000818 self._description_without_tags = ''
819 self.SetDescriptionText(description)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820
maruel@chromium.orge085d812011-10-10 19:49:15 +0000821 assert all(
822 (isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
823
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000824 diff_cache = self._AFFECTED_FILES.DIFF_CACHE(self._upstream)
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000825 self._affected_files = [
nick@chromium.orgff526192013-06-10 19:30:26 +0000826 self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
827 for action, path in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000828 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000829
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000830 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000831 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000832 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000833
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000834 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 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000841 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000842
843 def FullDescriptionText(self):
844 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000845 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000846
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000847 def SetDescriptionText(self, description):
848 """Sets the full description text (including tags) to |description|.
pgervais@chromium.org92c30092014-04-15 00:30:37 +0000849
isherman@chromium.orgb5cded62014-03-25 17:47:57 +0000850 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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000868 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000869 """Returns the repository (checkout) root directory for this change,
870 as an absolute path.
871 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000872 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000873
874 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000875 """Return tags directly as attributes on the object."""
876 if not re.match(r"^[A-Z_]*$", attr):
877 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000878 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000879
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000880 def AllFiles(self, root=None):
881 """List all files under source control in the repo."""
882 raise NotImplementedError()
883
sail@chromium.org5538e022011-05-12 17:53:16 +0000884 def AffectedFiles(self, include_dirs=False, include_deletes=True,
885 file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000886 """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
sail@chromium.org5538e022011-05-12 17:53:16 +0000891 file_filter: An additional filter to apply.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000892
893 Returns:
894 [AffectedFile(path, action), AffectedFile(path, action)]
895 """
896 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000897 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000898 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000899 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000900
sail@chromium.org5538e022011-05-12 17:53:16 +0000901 affected = filter(file_filter, affected)
902
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000903 if include_deletes:
904 return affected
905 else:
906 return filter(lambda x: x.Action() != 'D', affected)
907
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000908 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:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000911 warn("AffectedTextFiles(include_deletes=%s)"
912 " is deprecated and ignored" % str(include_deletes),
913 category=DeprecationWarning,
914 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000915 return filter(lambda x: x.IsTextFile(),
916 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000917
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 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000944 return _RightHandSideLinesImpl(
945 x for x in self.AffectedFiles(include_deletes=False)
946 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947
948
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000949class SvnChange(Change):
950 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000951 scm = 'svn'
952 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000953
954 def _GetChangeLists(self):
955 """Get all change lists."""
956 if self._changelists == None:
957 previous_cwd = os.getcwd()
958 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000959 # Need to import here to avoid circular dependency.
960 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000961 self._changelists = gcl.GetModifiedFiles()
962 os.chdir(previous_cwd)
963 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000964
965 def GetAllModifiedFiles(self):
966 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000967 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000968 all_modified_files = []
969 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000970 all_modified_files.extend(
971 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000972 return all_modified_files
973
974 def GetModifiedFiles(self):
975 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000976 changelists = self._GetChangeLists()
977 return [os.path.join(self.RepositoryRoot(), f[1])
978 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000979
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000980 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
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000986
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000987class GitChange(Change):
988 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000989 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000990
agable@chromium.org40a3d0b2014-05-15 01:59:16 +0000991 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
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000997
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000998def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000999 """Finds all presubmit files that apply to a given set of source files.
1000
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001001 If inherit-review-settings-ok is present right under root, looks for
1002 PRESUBMIT.py in directories enclosing root.
1003
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001004 Args:
1005 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001006 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001007
1008 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001009 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001011 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:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001025 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001026 candidates.add(directory)
1027 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001028 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001029 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
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001042 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001043 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001044
1045
thestig@chromium.orgde243452009-10-06 21:02:56 +00001046class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +00001047 @staticmethod
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001048 def ExecPresubmitScript(script_text, presubmit_path, project, change):
thestig@chromium.orgde243452009-10-06 21:02:56 +00001049 """Executes GetPreferredTrySlaves() from a single presubmit script.
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001050
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001051 This will soon be deprecated and replaced by GetPreferredTryMasters().
thestig@chromium.orgde243452009-10-06 21:02:56 +00001052
1053 Args:
1054 script_text: The text of the presubmit script.
bradnelson@google.com78230022011-05-24 18:55:19 +00001055 presubmit_path: Project script to run.
1056 project: Project name to pass to presubmit script for bot selection.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001057
1058 Return:
1059 A list of try slaves.
1060 """
1061 context = {}
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001062 main_path = os.getcwd()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001063 try:
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001064 os.chdir(os.path.dirname(presubmit_path))
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001065 exec script_text in context
1066 except Exception, e:
1067 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
alokp@chromium.orgf6349642014-03-04 00:52:18 +00001068 finally:
1069 os.chdir(main_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001070
1071 function_name = 'GetPreferredTrySlaves'
1072 if function_name in context:
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001073 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()
thestig@chromium.orgde243452009-10-06 21:02:56 +00001081 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001082 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +00001083 'Presubmit functions must return a list, got a %s instead: %s' %
1084 (type(result), str(result)))
1085 for item in result:
stip@chromium.org68e04192013-11-04 22:14:38 +00001086 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():
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001097 raise PresubmitFailure(
1098 'Try slave names cannot start/end with whitespace')
stip@chromium.org68e04192013-11-04 22:14:38 +00001099 if ',' in botname:
maruel@chromium.org3ecc8ea2012-03-10 01:47:46 +00001100 raise PresubmitFailure(
stip@chromium.org68e04192013-11-04 22:14:38 +00001101 'Do not use \',\' separated builder or test names: %s' % botname)
thestig@chromium.orgde243452009-10-06 21:02:56 +00001102 else:
1103 result = []
stip@chromium.org5ca27622013-12-18 17:44:58 +00001104
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
thestig@chromium.orgde243452009-10-06 21:02:56 +00001120 return result
1121
1122
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001123class 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
rmistry@google.com5626a922015-02-26 14:03:30 +00001153class 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
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001184def DoGetTrySlaves(change,
1185 changed_files,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001186 repository_root,
1187 default_presubmit,
bradnelson@google.com78230022011-05-24 18:55:19 +00001188 project,
thestig@chromium.orgde243452009-10-06 21:02:56 +00001189 verbose,
1190 output_stream):
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001191 """Get the list of try servers from the presubmit scripts (deprecated).
thestig@chromium.orgde243452009-10-06 21:02:56 +00001192
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.
bradnelson@google.com78230022011-05-24 18:55:19 +00001197 project: Optional name of a project used in selecting trybots.
thestig@chromium.orgde243452009-10-06 21:02:56 +00001198 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:
mdempsky@chromium.orgd59e7612014-03-05 19:55:56 +00001206 output_stream.write("Warning, no PRESUBMIT.py found.\n")
thestig@chromium.orgde243452009-10-06 21:02:56 +00001207 results = []
1208 executer = GetTrySlavesExecuter()
stip@chromium.org5ca27622013-12-18 17:44:58 +00001209
thestig@chromium.orgde243452009-10-06 21:02:56 +00001210 if default_presubmit:
1211 if verbose:
1212 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001213 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001214 results.extend(executer.ExecPresubmitScript(
1215 default_presubmit, fake_path, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001216 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.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001221 presubmit_script = gclient_utils.FileRead(filename, 'rU')
stip@chromium.org5ca27622013-12-18 17:44:58 +00001222 results.extend(executer.ExecPresubmitScript(
1223 presubmit_script, filename, project, change))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001224
stip@chromium.org5ca27622013-12-18 17:44:58 +00001225
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))
stip@chromium.org68e04192013-11-04 22:14:38 +00001235
thestig@chromium.orgde243452009-10-06 21:02:56 +00001236 if slaves and verbose:
stip@chromium.org5ca27622013-12-18 17:44:58 +00001237 output_stream.write(', '.join((str(x) for x in slaves)))
thestig@chromium.orgde243452009-10-06 21:02:56 +00001238 output_stream.write('\n')
1239 return slaves
1240
1241
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00001242def _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
rmistry@google.com5626a922015-02-26 14:03:30 +00001304def 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
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001347class PresubmitExecuter(object):
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001348 def __init__(self, change, committing, rietveld_obj, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001349 """
1350 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001351 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001352 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001353 rietveld_obj: rietveld.Rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001354 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001355 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001356 self.committing = committing
maruel@chromium.org239f4112011-06-03 20:08:23 +00001357 self.rietveld = rietveld_obj
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001358 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001359
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 """
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001371
chase@chromium.org8e416c82009-10-06 04:30:44 +00001372 # 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.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001377 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001378 self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001379 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001380 try:
1381 exec script_text in context
1382 except Exception, e:
1383 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001384
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:
wez@chromium.orga6d011e2013-03-26 17:31:49 +00001392 context['__args'] = (input_api, OutputApi(self.committing))
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001393 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001394 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +00001395 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001396 if not (isinstance(result, types.TupleType) or
1397 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001398 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001399 'Presubmit functions must return a tuple or list')
1400 for item in result:
1401 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001402 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001403 '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
chase@chromium.org8e416c82009-10-06 04:30:44 +00001408 # Return the process to the original working directory.
1409 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001410 return result
1411
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001412
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001413def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001414 committing,
1415 verbose,
1416 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001417 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001418 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001419 may_prompt,
thakis@chromium.orgc6ef53a2014-11-04 00:13:54 +00001420 rietveld_obj):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001421 """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:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001431 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001432 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.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001436 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001437 may_prompt: Enable (y/n) questions on warning or error.
maruel@chromium.org239f4112011-06-03 20:08:23 +00001438 rietveld_obj: rietveld.Rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001439
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001440 Warning:
1441 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1442 SHOULD be sys.stdin.
1443
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001444 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001445 A PresubmitOutput object. Use output.should_continue() to figure out
1446 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001447 """
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001448 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'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001453
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001454 output = PresubmitOutput(input_stream, output_stream)
1455 if committing:
1456 output.write("Running presubmit commit checks ...\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001457 else:
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001458 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:
maruel@chromium.orgfae707b2011-09-15 18:57:58 +00001463 output.write("Warning, no PRESUBMIT.py found.\n")
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001464 results = []
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001465 executer = PresubmitExecuter(change, committing, rietveld_obj, verbose)
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001466 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)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001478
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001479 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)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001489
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001490 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')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001499
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001500 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)
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001503
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001504 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):
maruel@chromium.org1ce8e662014-01-14 15:23:00 +00001516 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')
maruel@chromium.orgea7c8552011-04-18 14:12:07 +00001520 _ASKED_FOR_FEEDBACK = True
1521 return output
1522 finally:
1523 os.environ = old_environ
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001524
1525
1526def ScanSubDirs(mask, recursive):
1527 if not recursive:
pgervais@chromium.orge57b09d2014-05-07 00:58:13 +00001528 return [x for x in glob.glob(mask) if x not in ('.svn', '.git')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001529 else:
1530 results = []
1531 for root, dirs, files in os.walk('.'):
1532 if '.svn' in dirs:
1533 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001534 if '.git' in dirs:
1535 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001536 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):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001543 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001544 files = []
1545 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001546 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001547 return files
1548
1549
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001550def load_files(options, args):
1551 """Tries to determine the SCM."""
1552 change_scm = scm.determine_scm(options.root)
1553 files = []
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001554 if args:
1555 files = ParseFiles(args, options.recursive)
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001556 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
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001562 upstream = options.upstream or None
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001563 if not files:
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001564 files = scm.GIT.CaptureStatus([], options.root, upstream)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001565 else:
maruel@chromium.org9b31f162012-01-26 19:02:31 +00001566 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
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001570 return change_class, files
1571
1572
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001573class 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
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001591
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001592def CallCommand(cmd_data):
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001593 """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 """
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001598 cmd_data.kwargs['stdout'] = subprocess.PIPE
1599 cmd_data.kwargs['stderr'] = subprocess.STDOUT
1600 try:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001601 start = time.time()
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001602 (out, _), code = subprocess.communicate(cmd_data.cmd, **cmd_data.kwargs)
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001603 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001604 except OSError as e:
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001605 duration = time.time() - start
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001606 return cmd_data.message(
maruel@chromium.orgffeb2f32013-12-03 13:55:22 +00001607 '%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))
ilevy@chromium.orgbc117312013-04-20 03:57:56 +00001613
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001614
sbc@chromium.org013731e2015-02-26 18:28:43 +00001615def main(argv=None):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001616 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001617 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001618 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001619 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001620 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1621 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001622 parser.add_option("-r", "--recursive", action="store_true",
1623 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001624 parser.add_option("-v", "--verbose", action="count", default=0,
1625 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001626 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001627 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001628 parser.add_option("--description", default='')
1629 parser.add_option("--issue", type='int', default=0)
1630 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001631 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.")
agable@chromium.org2da1ade2014-04-30 17:40:45 +00001636 parser.add_option("--upstream",
1637 help="Git only: the base ref or upstream branch against "
1638 "which the diff should be computed.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001639 parser.add_option("--default_presubmit")
1640 parser.add_option("--may_prompt", action='store_true', default=False)
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001641 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.")
maruel@chromium.org239f4112011-06-03 20:08:23 +00001645 parser.add_option("--rietveld_url", help=optparse.SUPPRESS_HELP)
1646 parser.add_option("--rietveld_email", help=optparse.SUPPRESS_HELP)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001647 parser.add_option("--rietveld_fetch", action='store_true', default=False,
1648 help=optparse.SUPPRESS_HELP)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001649 # 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
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001653 parser.add_option("--trybot-json",
1654 help="Output trybot information to the file specified.")
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001655 auth.add_auth_options(parser)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001656 options, args = parser.parse_args(argv)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001657 auth_config = auth.extract_auth_config_from_options(options)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001658
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001659 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001660 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001661 elif options.verbose:
1662 logging.basicConfig(level=logging.INFO)
1663 else:
1664 logging.basicConfig(level=logging.ERROR)
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001665
1666 if options.rietveld_email and options.rietveld_email_file:
1667 parser.error("Only one of --rietveld_email or --rietveld_email_file "
1668 "can be passed to this program.")
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001669
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001670 if options.rietveld_email_file:
1671 with open(options.rietveld_email_file, "rb") as f:
1672 options.rietveld_email = f.read().strip()
1673
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001674 change_class, files = load_files(options, args)
1675 if not change_class:
1676 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001677 logging.info('Found %d file(s).' % len(files))
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001678
maruel@chromium.org239f4112011-06-03 20:08:23 +00001679 rietveld_obj = None
1680 if options.rietveld_url:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001681 # The empty password is permitted: '' is not None.
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001682 if options.rietveld_private_key_file:
pgervais@chromium.org92c30092014-04-15 00:30:37 +00001683 rietveld_obj = rietveld.JwtOAuth2Rietveld(
1684 options.rietveld_url,
1685 options.rietveld_email,
1686 options.rietveld_private_key_file)
1687 else:
djacques@chromium.org7b654f52014-04-15 04:02:32 +00001688 rietveld_obj = rietveld.CachingRietveld(
1689 options.rietveld_url,
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001690 auth_config,
1691 options.rietveld_email)
iannucci@chromium.org720fd7a2013-04-24 04:13:50 +00001692 if options.rietveld_fetch:
1693 assert options.issue
1694 props = rietveld_obj.get_issue_properties(options.issue, False)
1695 options.author = props['owner_email']
1696 options.description = props['description']
1697 logging.info('Got author: "%s"', options.author)
1698 logging.info('Got description: """\n%s\n"""', options.description)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001699 if options.trybot_json:
1700 with open(options.trybot_json, 'w') as f:
1701 # Python's sets aren't JSON-encodable, so we convert them to lists here.
1702 class SetEncoder(json.JSONEncoder):
1703 # pylint: disable=E0202
1704 def default(self, obj):
1705 if isinstance(obj, set):
1706 return sorted(obj)
1707 return json.JSONEncoder.default(self, obj)
1708 change = change_class(options.name,
1709 options.description,
1710 options.root,
1711 files,
1712 options.issue,
1713 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001714 options.author,
1715 upstream=options.upstream)
stip@chromium.orgf7d31f52014-01-03 20:14:46 +00001716 trybots = DoGetTrySlaves(
1717 change,
1718 change.LocalPaths(),
1719 change.RepositoryRoot(),
1720 None,
1721 None,
1722 options.verbose,
1723 sys.stdout)
1724 json.dump(trybots, f, cls=SetEncoder)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001725 try:
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001726 with canned_check_filter(options.skip_canned):
1727 results = DoPresubmitChecks(
1728 change_class(options.name,
1729 options.description,
1730 options.root,
1731 files,
1732 options.issue,
1733 options.patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001734 options.author,
1735 upstream=options.upstream),
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001736 options.commit,
1737 options.verbose,
1738 sys.stdout,
1739 sys.stdin,
1740 options.default_presubmit,
1741 options.may_prompt,
1742 rietveld_obj)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001743 return not results.should_continue()
iannucci@chromium.org8a4a2bc2013-03-08 08:13:20 +00001744 except NonexistantCannedCheckFilter, e:
1745 print >> sys.stderr, (
1746 'Attempted to skip nonexistent canned presubmit check: %s' % e.message)
1747 return 2
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001748 except PresubmitFailure, e:
1749 print >> sys.stderr, e
1750 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1751 print >> sys.stderr, 'If all fails, contact maruel@'
1752 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001753
1754
1755if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001756 fix_encoding.fix_encoding()
sbc@chromium.org013731e2015-02-26 18:28:43 +00001757 try:
1758 sys.exit(main())
1759 except KeyboardInterrupt:
1760 sys.stderr.write('interrupted\n')
1761 sys.exit(1)