blob: 1e16758b02c0786f124b4bc9faebfed2e7c723ea [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
2# Copyright (c) 2011 The Chromium Authors. All rights reserved.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Enables directory-specific presubmit checks to run at upload and/or commit.
7"""
8
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00009__version__ = '1.6.1'
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000010
11# TODO(joi) Add caching where appropriate/needed. The API is designed to allow
12# caching (between all different invocations of presubmit scripts for a given
13# change). We should add it as our presubmit scripts start feeling slow.
14
15import cPickle # Exposed through the API.
16import cStringIO # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000017import fnmatch
18import glob
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +000019import logging
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000020import marshal # Exposed through the API.
21import optparse
22import os # Somewhat exposed through the API.
23import pickle # Exposed through the API.
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000024import random
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000025import re # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000026import sys # Parts exposed through API.
27import tempfile # Exposed through the API.
jam@chromium.org2a891dc2009-08-20 20:33:37 +000028import time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +000029import traceback # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000030import types
maruel@chromium.org1487d532009-06-06 00:22:57 +000031import unittest # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000032import urllib2 # Exposed through the API.
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000033from warnings import warn
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000034
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000035try:
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000036 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000037except ImportError:
38 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000039 import json # pylint: disable=F0401
40 except ImportError:
maruel@chromium.org59c7ba62010-03-20 00:13:07 +000041 # Import the one included in depot_tools.
maruel@chromium.orgd08cb1e2010-03-20 00:25:19 +000042 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgd945f362011-03-11 22:52:19 +000043 import simplejson as json # pylint: disable=F0401
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +000044
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000045# Local imports.
maruel@chromium.org35625c72011-03-23 17:34:02 +000046import fix_encoding
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000047import gclient_utils
dpranke@chromium.org2a009622011-03-01 02:43:31 +000048import owners
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000049import presubmit_canned_checks
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +000050import scm
maruel@chromium.org84f4fe32011-04-06 13:26:45 +000051import subprocess2 as subprocess # Exposed through the API.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000052
53
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +000054# Ask for feedback only once in program lifetime.
55_ASKED_FOR_FEEDBACK = False
56
57
maruel@chromium.org899e1c12011-04-07 17:03:18 +000058class PresubmitFailure(Exception):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +000059 pass
60
61
62def normpath(path):
63 '''Version of os.path.normpath that also changes backward slashes to
64 forward slashes when not running on Windows.
65 '''
66 # This is safe to always do because the Windows version of os.path.normpath
67 # will replace forward slashes with backward slashes.
68 path = path.replace(os.sep, '/')
69 return os.path.normpath(path)
70
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000071
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000072def _RightHandSideLinesImpl(affected_files):
73 """Implements RightHandSideLines for InputApi and GclChange."""
74 for af in affected_files:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000075 lines = af.ChangedContents()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000076 for line in lines:
maruel@chromium.orgab05d582011-02-09 23:41:22 +000077 yield (af, line[0], line[1])
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +000078
79
dpranke@chromium.org5ac21012011-03-16 02:58:25 +000080class PresubmitOutput(object):
81 def __init__(self, input_stream=None, output_stream=None):
82 self.input_stream = input_stream
83 self.output_stream = output_stream
84 self.reviewers = []
85 self.written_output = []
86 self.error_count = 0
87
88 def prompt_yes_no(self, prompt_string):
89 self.write(prompt_string)
90 if self.input_stream:
91 response = self.input_stream.readline().strip().lower()
92 if response not in ('y', 'yes'):
93 self.fail()
94 else:
95 self.fail()
96
97 def fail(self):
98 self.error_count += 1
99
100 def should_continue(self):
101 return not self.error_count
102
103 def write(self, s):
104 self.written_output.append(s)
105 if self.output_stream:
106 self.output_stream.write(s)
107
108 def getvalue(self):
109 return ''.join(self.written_output)
110
111
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000112class OutputApi(object):
113 """This class (more like a module) gets passed to presubmit scripts so that
114 they can specify various types of results.
115 """
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000116 class PresubmitResult(object):
117 """Base class for result objects."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000118 fatal = False
119 should_prompt = False
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000120
121 def __init__(self, message, items=None, long_text=''):
122 """
123 message: A short one-line message to indicate errors.
124 items: A list of short strings to indicate where errors occurred.
125 long_text: multi-line text output, e.g. from another tool
126 """
127 self._message = message
128 self._items = []
129 if items:
130 self._items = items
131 self._long_text = long_text.rstrip()
132
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000133 def handle(self, output):
134 output.write(self._message)
135 output.write('\n')
maruel@chromium.org35625c72011-03-23 17:34:02 +0000136 for index, item in enumerate(self._items):
137 output.write(' ')
138 # Write separately in case it's unicode.
maruel@chromium.org604a5892011-03-23 23:55:48 +0000139 output.write(str(item))
maruel@chromium.org35625c72011-03-23 17:34:02 +0000140 if index < len(self._items) - 1:
141 output.write(' \\')
142 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000143 if self._long_text:
maruel@chromium.org35625c72011-03-23 17:34:02 +0000144 output.write('\n***************\n')
145 # Write separately in case it's unicode.
146 output.write(self._long_text)
147 output.write('\n***************\n')
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000148 if self.fatal:
149 output.fail()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000150
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000151 class PresubmitAddReviewers(PresubmitResult):
152 """Add some suggested reviewers to the change."""
153 def __init__(self, reviewers):
154 super(OutputApi.PresubmitAddReviewers, self).__init__('')
155 self.reviewers = reviewers
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000156
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000157 def handle(self, output):
158 output.reviewers.extend(self.reviewers)
dpranke@chromium.org3ae183f2011-03-09 21:40:32 +0000159
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000160 class PresubmitError(PresubmitResult):
161 """A hard presubmit error."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000162 fatal = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000163
164 class PresubmitPromptWarning(PresubmitResult):
165 """An warning that prompts the user if they want to continue."""
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000166 should_prompt = True
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000167
168 class PresubmitNotifyResult(PresubmitResult):
169 """Just print something to the screen -- but it's not even a warning."""
170 pass
171
172 class MailTextResult(PresubmitResult):
173 """A warning that should be included in the review request email."""
174 def __init__(self, *args, **kwargs):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000175 super(OutputApi.MailTextResult, self).__init__()
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000176 raise NotImplementedError()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000177
178
179class InputApi(object):
180 """An instance of this object is passed to presubmit scripts so they can
181 know stuff about the change they're looking at.
182 """
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000183 # Method could be a function
184 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000185
maruel@chromium.org3410d912009-06-09 20:56:16 +0000186 # File extensions that are considered source files from a style guide
187 # perspective. Don't modify this list from a presubmit script!
188 DEFAULT_WHITE_LIST = (
189 # C++ and friends
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000190 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000191 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000192 # Scripts
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000193 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
maruel@chromium.orgef776482010-01-28 16:04:32 +0000194 # No extension at all, note that ALL CAPS files are black listed in
195 # DEFAULT_BLACK_LIST below.
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000196 r"(^|.*?[\\\/])[^.]+$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000197 # Other
maruel@chromium.orga73d7932010-01-28 23:50:06 +0000198 r".*\.java$", r".*\.mk$", r".*\.am$",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000199 )
200
201 # Path regexp that should be excluded from being considered containing source
202 # files. Don't modify this list from a presubmit script!
203 DEFAULT_BLACK_LIST = (
204 r".*\bexperimental[\\\/].*",
205 r".*\bthird_party[\\\/].*",
206 # Output directories (just in case)
207 r".*\bDebug[\\\/].*",
208 r".*\bRelease[\\\/].*",
209 r".*\bxcodebuild[\\\/].*",
210 r".*\bsconsbuild[\\\/].*",
211 # All caps files like README and LICENCE.
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000212 r".*\b[A-Z0-9_]{2,}$",
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000213 # SCM (can happen in dual SCM configuration). (Slightly over aggressive)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000214 r"(|.*[\\\/])\.git[\\\/].*",
215 r"(|.*[\\\/])\.svn[\\\/].*",
maruel@chromium.org3410d912009-06-09 20:56:16 +0000216 )
217
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000218 def __init__(self, change, presubmit_path, is_committing, tbr,
219 rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000220 """Builds an InputApi object.
221
222 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000223 change: A presubmit.Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000224 presubmit_path: The path to the presubmit script being processed.
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000225 is_committing: True if the change is about to be committed.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000226 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000227 rietveld: rietveld client object
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000228 """
maruel@chromium.org9711bba2009-05-22 23:51:39 +0000229 # Version number of the presubmit_support script.
230 self.version = [int(x) for x in __version__.split('.')]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000231 self.change = change
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000232 self.is_committing = is_committing
dpranke@chromium.org627ea672011-03-11 23:29:03 +0000233 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000234 self.rietveld = rietveld
235 # TBD
236 self.host_url = 'http://codereview.chromium.org'
237 if self.rietveld:
238 self.host_url = rietveld.url
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000239
240 # We expose various modules and functions as attributes of the input_api
241 # so that presubmit scripts don't have to import them.
242 self.basename = os.path.basename
243 self.cPickle = cPickle
244 self.cStringIO = cStringIO
maruel@chromium.orgfb11c7b2010-03-18 18:22:14 +0000245 self.json = json
maruel@chromium.org2b5ce562011-03-31 16:15:44 +0000246 self.os_listdir = os.listdir
247 self.os_walk = os.walk
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000248 self.os_path = os.path
249 self.pickle = pickle
250 self.marshal = marshal
251 self.re = re
252 self.subprocess = subprocess
253 self.tempfile = tempfile
dpranke@chromium.org0d1bdea2011-03-24 22:54:38 +0000254 self.time = time
maruel@chromium.orgd7dccf52009-06-06 18:51:58 +0000255 self.traceback = traceback
maruel@chromium.org1487d532009-06-06 00:22:57 +0000256 self.unittest = unittest
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000257 self.urllib2 = urllib2
258
maruel@chromium.orgc0b22972009-06-25 16:19:14 +0000259 # To easily fork python.
260 self.python_executable = sys.executable
261 self.environ = os.environ
262
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000263 # InputApi.platform is the platform you're currently running on.
264 self.platform = sys.platform
265
266 # The local path of the currently-being-processed presubmit script.
maruel@chromium.org3d235242009-05-15 12:40:48 +0000267 self._current_presubmit_path = os.path.dirname(presubmit_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000268
269 # We carry the canned checks so presubmit scripts can easily use them.
270 self.canned_checks = presubmit_canned_checks
271
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000272 # TODO(dpranke): figure out a list of all approved owners for a repo
273 # in order to be able to handle wildcard OWNERS files?
274 self.owners_db = owners.Database(change.RepositoryRoot(),
275 fopen=file, os_path=self.os_path)
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000276 self.verbose = verbose
dpranke@chromium.org2a009622011-03-01 02:43:31 +0000277
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000278 def PresubmitLocalPath(self):
279 """Returns the local path of the presubmit script currently being run.
280
281 This is useful if you don't want to hard-code absolute paths in the
282 presubmit script. For example, It can be used to find another file
283 relative to the PRESUBMIT.py script, so the whole tree can be branched and
284 the presubmit script still works, without editing its content.
285 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000286 return self._current_presubmit_path
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000287
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000288 def DepotToLocalPath(self, depot_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000289 """Translate a depot path to a local path (relative to client root).
290
291 Args:
292 Depot path as a string.
293
294 Returns:
295 The local path of the depot path under the user's current client, or None
296 if the file is not mapped.
297
298 Remember to check for the None case and show an appropriate error!
299 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000300 local_path = scm.SVN.CaptureInfo(depot_path).get('Path')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000301 if local_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000302 return local_path
303
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000304 def LocalToDepotPath(self, local_path):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000305 """Translate a local path to a depot path.
306
307 Args:
308 Local path (relative to current directory, or absolute) as a string.
309
310 Returns:
311 The depot path (SVN URL) of the file if mapped, otherwise None.
312 """
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000313 depot_path = scm.SVN.CaptureInfo(local_path).get('URL')
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000314 if depot_path:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000315 return depot_path
316
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000317 def AffectedFiles(self, include_dirs=False, include_deletes=True):
318 """Same as input_api.change.AffectedFiles() except only lists files
319 (and optionally directories) in the same directory as the current presubmit
320 script, or subdirectories thereof.
321 """
maruel@chromium.org3d235242009-05-15 12:40:48 +0000322 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000323 if len(dir_with_slash) == 1:
324 dir_with_slash = ''
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000325 return filter(
326 lambda x: normpath(x.AbsoluteLocalPath()).startswith(dir_with_slash),
327 self.change.AffectedFiles(include_dirs, include_deletes))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000328
329 def LocalPaths(self, include_dirs=False):
330 """Returns local paths of input_api.AffectedFiles()."""
331 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
332
333 def AbsoluteLocalPaths(self, include_dirs=False):
334 """Returns absolute local paths of input_api.AffectedFiles()."""
335 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
336
337 def ServerPaths(self, include_dirs=False):
338 """Returns server paths of input_api.AffectedFiles()."""
339 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
340
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000341 def AffectedTextFiles(self, include_deletes=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000342 """Same as input_api.change.AffectedTextFiles() except only lists files
343 in the same directory as the current presubmit script, or subdirectories
344 thereof.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000345 """
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000346 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000347 warn("AffectedTextFiles(include_deletes=%s)"
348 " is deprecated and ignored" % str(include_deletes),
349 category=DeprecationWarning,
350 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000351 return filter(lambda x: x.IsTextFile(),
352 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000353
maruel@chromium.org3410d912009-06-09 20:56:16 +0000354 def FilterSourceFile(self, affected_file, white_list=None, black_list=None):
355 """Filters out files that aren't considered "source file".
356
357 If white_list or black_list is None, InputApi.DEFAULT_WHITE_LIST
358 and InputApi.DEFAULT_BLACK_LIST is used respectively.
359
360 The lists will be compiled as regular expression and
361 AffectedFile.LocalPath() needs to pass both list.
362
363 Note: Copy-paste this function to suit your needs or use a lambda function.
364 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000365 def Find(affected_file, items):
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000366 local_path = affected_file.LocalPath()
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000367 for item in items:
maruel@chromium.orgdf1595a2009-06-11 02:00:13 +0000368 if self.re.match(item, local_path):
369 logging.debug("%s matched %s" % (item, local_path))
maruel@chromium.org3410d912009-06-09 20:56:16 +0000370 return True
371 return False
372 return (Find(affected_file, white_list or self.DEFAULT_WHITE_LIST) and
373 not Find(affected_file, black_list or self.DEFAULT_BLACK_LIST))
374
375 def AffectedSourceFiles(self, source_file):
376 """Filter the list of AffectedTextFiles by the function source_file.
377
378 If source_file is None, InputApi.FilterSourceFile() is used.
379 """
380 if not source_file:
381 source_file = self.FilterSourceFile
382 return filter(source_file, self.AffectedTextFiles())
383
384 def RightHandSideLines(self, source_file_filter=None):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000385 """An iterator over all text lines in "new" version of changed files.
386
387 Only lists lines from new or modified text files in the change that are
388 contained by the directory of the currently executing presubmit script.
389
390 This is useful for doing line-by-line regex checks, like checking for
391 trailing whitespace.
392
393 Yields:
394 a 3 tuple:
395 the AffectedFile instance of the current file;
396 integer line number (1-based); and
397 the contents of the line as a string.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000398
399 Note: The cariage return (LF or CR) is stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000400 """
maruel@chromium.org3410d912009-06-09 20:56:16 +0000401 files = self.AffectedSourceFiles(source_file_filter)
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000402 return _RightHandSideLinesImpl(files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000403
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000404 def ReadFile(self, file_item, mode='r'):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000405 """Reads an arbitrary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000406
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000407 Deny reading anything outside the repository.
408 """
maruel@chromium.orge3608df2009-11-10 20:22:57 +0000409 if isinstance(file_item, AffectedFile):
410 file_item = file_item.AbsoluteLocalPath()
411 if not file_item.startswith(self.change.RepositoryRoot()):
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000412 raise IOError('Access outside the repository root is denied.')
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000413 return gclient_utils.FileRead(file_item, mode)
maruel@chromium.org44a17ad2009-06-08 14:14:35 +0000414
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000415
416class AffectedFile(object):
417 """Representation of a file in a change."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000418 # Method could be a function
419 # pylint: disable=R0201
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000420 def __init__(self, path, action, repository_root=''):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000421 self._path = path
422 self._action = action
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000423 self._local_root = repository_root
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000424 self._is_directory = None
425 self._properties = {}
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000426 logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000427
428 def ServerPath(self):
429 """Returns a path string that identifies the file in the SCM system.
430
431 Returns the empty string if the file does not exist in SCM.
432 """
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000433 return ""
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000434
435 def LocalPath(self):
436 """Returns the path of this file on the local disk relative to client root.
437 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000438 return normpath(self._path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000439
440 def AbsoluteLocalPath(self):
441 """Returns the absolute path of this file on the local disk.
442 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000443 return os.path.abspath(os.path.join(self._local_root, self.LocalPath()))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000444
445 def IsDirectory(self):
446 """Returns true if this object is a directory."""
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000447 if self._is_directory is None:
448 path = self.AbsoluteLocalPath()
449 self._is_directory = (os.path.exists(path) and
450 os.path.isdir(path))
451 return self._is_directory
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000452
453 def Action(self):
454 """Returns the action on this opened file, e.g. A, M, D, etc."""
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000455 # TODO(maruel): Somewhat crappy, Could be "A" or "A +" for svn but
456 # different for other SCM.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000457 return self._action
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000458
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000459 def Property(self, property_name):
460 """Returns the specified SCM property of this file, or None if no such
461 property.
462 """
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000463 return self._properties.get(property_name, None)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000464
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000465 def IsTextFile(self):
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000466 """Returns True if the file is a text file and not a binary file.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000467
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000468 Deleted files are not text file."""
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000469 raise NotImplementedError() # Implement when needed
470
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000471 def NewContents(self):
472 """Returns an iterator over the lines in the new version of file.
473
474 The new version is the file in the user's workspace, i.e. the "right hand
475 side".
476
477 Contents will be empty if the file is a directory or does not exist.
maruel@chromium.org1487d532009-06-06 00:22:57 +0000478 Note: The cariage returns (LF or CR) are stripped off.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000479 """
480 if self.IsDirectory():
481 return []
482 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000483 return gclient_utils.FileRead(self.AbsoluteLocalPath(),
484 'rU').splitlines()
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000485
486 def OldContents(self):
487 """Returns an iterator over the lines in the old version of file.
488
489 The old version is the file in depot, i.e. the "left hand side".
490 """
491 raise NotImplementedError() # Implement when needed
492
493 def OldFileTempPath(self):
494 """Returns the path on local disk where the old contents resides.
495
496 The old version is the file in depot, i.e. the "left hand side".
497 This is a read-only cached copy of the old contents. *DO NOT* try to
498 modify this file.
499 """
500 raise NotImplementedError() # Implement if/when needed.
501
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000502 def ChangedContents(self):
503 """Returns a list of tuples (line number, line text) of all new lines.
504
505 This relies on the scm diff output describing each changed code section
506 with a line of the form
507
508 ^@@ <old line num>,<old size> <new line num>,<new size> @@$
509 """
510 new_lines = []
511 line_num = 0
512
513 if self.IsDirectory():
514 return []
515
516 for line in self.GenerateScmDiff().splitlines():
517 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
518 if m:
519 line_num = int(m.groups(1)[0])
520 continue
521 if line.startswith('+') and not line.startswith('++'):
522 new_lines.append((line_num, line[1:]))
523 if not line.startswith('-'):
524 line_num += 1
525 return new_lines
526
maruel@chromium.org5de13972009-06-10 18:16:06 +0000527 def __str__(self):
528 return self.LocalPath()
529
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000530 def GenerateScmDiff(self):
531 raise NotImplementedError() # Implemented in derived classes.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000532
maruel@chromium.org58407af2011-04-12 23:15:57 +0000533
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000534class SvnAffectedFile(AffectedFile):
535 """Representation of a file in a change out of a Subversion checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000536 # Method 'NNN' is abstract in class 'NNN' but is not overridden
537 # pylint: disable=W0223
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000538
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000539 def __init__(self, *args, **kwargs):
540 AffectedFile.__init__(self, *args, **kwargs)
541 self._server_path = None
542 self._is_text_file = None
543
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000544 def ServerPath(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000545 if self._server_path is None:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000546 self._server_path = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000547 self.AbsoluteLocalPath()).get('URL', '')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000548 return self._server_path
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000549
550 def IsDirectory(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000551 if self._is_directory is None:
552 path = self.AbsoluteLocalPath()
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000553 if os.path.exists(path):
554 # Retrieve directly from the file system; it is much faster than
555 # querying subversion, especially on Windows.
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000556 self._is_directory = os.path.isdir(path)
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000557 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000558 self._is_directory = scm.SVN.CaptureInfo(
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000559 path).get('Node Kind') in ('dir', 'directory')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000560 return self._is_directory
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000561
562 def Property(self, property_name):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000563 if not property_name in self._properties:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000564 self._properties[property_name] = scm.SVN.GetFileProperty(
maruel@chromium.org196f8cb2009-06-11 00:32:06 +0000565 self.AbsoluteLocalPath(), property_name).rstrip()
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000566 return self._properties[property_name]
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000567
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000568 def IsTextFile(self):
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000569 if self._is_text_file is None:
570 if self.Action() == 'D':
571 # A deleted file is not a text file.
572 self._is_text_file = False
573 elif self.IsDirectory():
574 self._is_text_file = False
575 else:
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000576 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(),
577 'svn:mime-type')
maruel@chromium.org15bdffa2009-05-29 11:16:29 +0000578 self._is_text_file = (not mime_type or mime_type.startswith('text/'))
579 return self._is_text_file
maruel@chromium.org1e08c002009-05-28 19:09:33 +0000580
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000581 def GenerateScmDiff(self):
maruel@chromium.org1f312812011-02-10 01:33:57 +0000582 return scm.SVN.GenerateDiff([self.AbsoluteLocalPath()])
583
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000584
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000585class GitAffectedFile(AffectedFile):
586 """Representation of a file in a change out of a git checkout."""
maruel@chromium.orgb17b55b2010-11-03 14:42:37 +0000587 # Method 'NNN' is abstract in class 'NNN' but is not overridden
588 # pylint: disable=W0223
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000589
590 def __init__(self, *args, **kwargs):
591 AffectedFile.__init__(self, *args, **kwargs)
592 self._server_path = None
593 self._is_text_file = None
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000594
595 def ServerPath(self):
596 if self._server_path is None:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000597 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000598 return self._server_path
599
600 def IsDirectory(self):
601 if self._is_directory is None:
602 path = self.AbsoluteLocalPath()
603 if os.path.exists(path):
604 # Retrieve directly from the file system; it is much faster than
605 # querying subversion, especially on Windows.
606 self._is_directory = os.path.isdir(path)
607 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000608 self._is_directory = False
609 return self._is_directory
610
611 def Property(self, property_name):
612 if not property_name in self._properties:
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000613 raise NotImplementedError('TODO(maruel) Implement.')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000614 return self._properties[property_name]
615
616 def IsTextFile(self):
617 if self._is_text_file is None:
618 if self.Action() == 'D':
619 # A deleted file is not a text file.
620 self._is_text_file = False
621 elif self.IsDirectory():
622 self._is_text_file = False
623 else:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000624 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
625 return self._is_text_file
626
maruel@chromium.orgab05d582011-02-09 23:41:22 +0000627 def GenerateScmDiff(self):
628 return scm.GIT.GenerateDiff(self._local_root, files=[self.LocalPath(),])
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000629
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000630
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000631class Change(object):
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000632 """Describe a change.
633
634 Used directly by the presubmit scripts to query the current change being
635 tested.
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000636
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000637 Instance members:
638 tags: Dictionnary of KEY=VALUE pairs found in the change description.
639 self.KEY: equivalent to tags['KEY']
640 """
641
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000642 _AFFECTED_FILES = AffectedFile
643
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000644 # Matches key/value (or "tag") lines in changelist descriptions.
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000645 _TAG_LINE_RE = re.compile(
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000646 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$')
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000647 scm = ''
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000648
maruel@chromium.org58407af2011-04-12 23:15:57 +0000649 def __init__(
650 self, name, description, local_root, files, issue, patchset, author):
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000651 if files is None:
652 files = []
653 self._name = name
654 self._full_description = description
chase@chromium.org8e416c82009-10-06 04:30:44 +0000655 # Convert root into an absolute path.
656 self._local_root = os.path.abspath(local_root)
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000657 self.issue = issue
658 self.patchset = patchset
maruel@chromium.org58407af2011-04-12 23:15:57 +0000659 self.author_email = author
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000660
661 # From the description text, build up a dictionary of key/value pairs
662 # plus the description minus all key/value or "tag" lines.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000663 description_without_tags = []
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000664 self.tags = {}
maruel@chromium.org8d5c9a52009-06-12 15:59:08 +0000665 for line in self._full_description.splitlines():
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000666 m = self._TAG_LINE_RE.match(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000667 if m:
668 self.tags[m.group('key')] = m.group('value')
669 else:
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000670 description_without_tags.append(line)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000671
672 # Change back to text and remove whitespace at end.
maruel@chromium.orgfa410372010-09-10 17:01:01 +0000673 self._description_without_tags = (
674 '\n'.join(description_without_tags).rstrip())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000675
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000676 self._affected_files = [
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000677 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
678 for info in files
maruel@chromium.orgdbbeedc2009-05-22 20:26:17 +0000679 ]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000680
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000681 def Name(self):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000682 """Returns the change name."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000683 return self._name
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000684
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000685 def DescriptionText(self):
686 """Returns the user-entered changelist description, minus tags.
687
688 Any line in the user-provided description starting with e.g. "FOO="
689 (whitespace permitted before and around) is considered a tag line. Such
690 lines are stripped out of the description this function returns.
691 """
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000692 return self._description_without_tags
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000693
694 def FullDescriptionText(self):
695 """Returns the complete changelist description including tags."""
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000696 return self._full_description
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000697
698 def RepositoryRoot(self):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000699 """Returns the repository (checkout) root directory for this change,
700 as an absolute path.
701 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000702 return self._local_root
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000703
704 def __getattr__(self, attr):
maruel@chromium.org92022ec2009-06-11 01:59:28 +0000705 """Return tags directly as attributes on the object."""
706 if not re.match(r"^[A-Z_]*$", attr):
707 raise AttributeError(self, attr)
maruel@chromium.orge1a524f2009-05-27 14:43:46 +0000708 return self.tags.get(attr)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000709
710 def AffectedFiles(self, include_dirs=False, include_deletes=True):
711 """Returns a list of AffectedFile instances for all files in the change.
712
713 Args:
714 include_deletes: If false, deleted files will be filtered out.
715 include_dirs: True to include directories in the list
716
717 Returns:
718 [AffectedFile(path, action), AffectedFile(path, action)]
719 """
720 if include_dirs:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000721 affected = self._affected_files
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000722 else:
maruel@chromium.org6ebe68a2009-05-27 23:43:40 +0000723 affected = filter(lambda x: not x.IsDirectory(), self._affected_files)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000724
725 if include_deletes:
726 return affected
727 else:
728 return filter(lambda x: x.Action() != 'D', affected)
729
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000730 def AffectedTextFiles(self, include_deletes=None):
731 """Return a list of the existing text files in a change."""
732 if include_deletes is not None:
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000733 warn("AffectedTextFiles(include_deletes=%s)"
734 " is deprecated and ignored" % str(include_deletes),
735 category=DeprecationWarning,
736 stacklevel=2)
maruel@chromium.org77c4f0f2009-05-29 18:53:04 +0000737 return filter(lambda x: x.IsTextFile(),
738 self.AffectedFiles(include_dirs=False, include_deletes=False))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000739
740 def LocalPaths(self, include_dirs=False):
741 """Convenience function."""
742 return [af.LocalPath() for af in self.AffectedFiles(include_dirs)]
743
744 def AbsoluteLocalPaths(self, include_dirs=False):
745 """Convenience function."""
746 return [af.AbsoluteLocalPath() for af in self.AffectedFiles(include_dirs)]
747
748 def ServerPaths(self, include_dirs=False):
749 """Convenience function."""
750 return [af.ServerPath() for af in self.AffectedFiles(include_dirs)]
751
752 def RightHandSideLines(self):
753 """An iterator over all text lines in "new" version of changed files.
754
755 Lists lines from new or modified text files in the change.
756
757 This is useful for doing line-by-line regex checks, like checking for
758 trailing whitespace.
759
760 Yields:
761 a 3 tuple:
762 the AffectedFile instance of the current file;
763 integer line number (1-based); and
764 the contents of the line as a string.
765 """
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000766 return _RightHandSideLinesImpl(
767 x for x in self.AffectedFiles(include_deletes=False)
768 if x.IsTextFile())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000769
770
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000771class SvnChange(Change):
772 _AFFECTED_FILES = SvnAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000773 scm = 'svn'
774 _changelists = None
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000775
776 def _GetChangeLists(self):
777 """Get all change lists."""
778 if self._changelists == None:
779 previous_cwd = os.getcwd()
780 os.chdir(self.RepositoryRoot())
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000781 # Need to import here to avoid circular dependency.
782 import gcl
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000783 self._changelists = gcl.GetModifiedFiles()
784 os.chdir(previous_cwd)
785 return self._changelists
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000786
787 def GetAllModifiedFiles(self):
788 """Get all modified files."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000789 changelists = self._GetChangeLists()
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000790 all_modified_files = []
791 for cl in changelists.values():
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000792 all_modified_files.extend(
793 [os.path.join(self.RepositoryRoot(), f[1]) for f in cl])
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000794 return all_modified_files
795
796 def GetModifiedFiles(self):
797 """Get modified files in the current CL."""
thestig@chromium.org6bd31702009-09-02 23:29:07 +0000798 changelists = self._GetChangeLists()
799 return [os.path.join(self.RepositoryRoot(), f[1])
800 for f in changelists[self.Name()]]
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000801
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000802
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000803class GitChange(Change):
804 _AFFECTED_FILES = GitAffectedFile
maruel@chromium.orgc1938752011-04-12 23:11:13 +0000805 scm = 'git'
thestig@chromium.orgda8cddd2009-08-13 00:25:55 +0000806
maruel@chromium.orgc70a2202009-06-17 12:55:10 +0000807
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000808def ListRelevantPresubmitFiles(files, root):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000809 """Finds all presubmit files that apply to a given set of source files.
810
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000811 If inherit-review-settings-ok is present right under root, looks for
812 PRESUBMIT.py in directories enclosing root.
813
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000814 Args:
815 files: An iterable container containing file paths.
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000816 root: Path where to stop searching.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000817
818 Return:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000819 List of absolute paths of the existing PRESUBMIT.py scripts.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000820 """
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000821 files = [normpath(os.path.join(root, f)) for f in files]
822
823 # List all the individual directories containing files.
824 directories = set([os.path.dirname(f) for f in files])
825
826 # Ignore root if inherit-review-settings-ok is present.
827 if os.path.isfile(os.path.join(root, 'inherit-review-settings-ok')):
828 root = None
829
830 # Collect all unique directories that may contain PRESUBMIT.py.
831 candidates = set()
832 for directory in directories:
833 while True:
834 if directory in candidates:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000835 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000836 candidates.add(directory)
837 if directory == root:
maruel@chromium.org4661e0c2009-06-04 00:45:26 +0000838 break
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000839 parent_dir = os.path.dirname(directory)
840 if parent_dir == directory:
841 # We hit the system root directory.
842 break
843 directory = parent_dir
844
845 # Look for PRESUBMIT.py in all candidate directories.
846 results = []
847 for directory in sorted(list(candidates)):
848 p = os.path.join(directory, 'PRESUBMIT.py')
849 if os.path.isfile(p):
850 results.append(p)
851
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000852 logging.debug('Presubmit files: %s' % ','.join(results))
maruel@chromium.orgb1901a62010-06-16 00:18:47 +0000853 return results
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000854
855
thestig@chromium.orgde243452009-10-06 21:02:56 +0000856class GetTrySlavesExecuter(object):
maruel@chromium.orgcb2985f2010-11-03 14:08:31 +0000857 @staticmethod
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000858 def ExecPresubmitScript(script_text, presubmit_path):
thestig@chromium.orgde243452009-10-06 21:02:56 +0000859 """Executes GetPreferredTrySlaves() from a single presubmit script.
860
861 Args:
862 script_text: The text of the presubmit script.
863
864 Return:
865 A list of try slaves.
866 """
867 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000868 try:
869 exec script_text in context
870 except Exception, e:
871 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
thestig@chromium.orgde243452009-10-06 21:02:56 +0000872
873 function_name = 'GetPreferredTrySlaves'
874 if function_name in context:
875 result = eval(function_name + '()', context)
876 if not isinstance(result, types.ListType):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000877 raise PresubmitFailure(
thestig@chromium.orgde243452009-10-06 21:02:56 +0000878 'Presubmit functions must return a list, got a %s instead: %s' %
879 (type(result), str(result)))
880 for item in result:
881 if not isinstance(item, basestring):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000882 raise PresubmitFailure('All try slaves names must be strings.')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000883 if item != item.strip():
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000884 raise PresubmitFailure(
885 'Try slave names cannot start/end with whitespace')
thestig@chromium.orgde243452009-10-06 21:02:56 +0000886 else:
887 result = []
888 return result
889
890
891def DoGetTrySlaves(changed_files,
892 repository_root,
893 default_presubmit,
894 verbose,
895 output_stream):
896 """Get the list of try servers from the presubmit scripts.
897
898 Args:
899 changed_files: List of modified files.
900 repository_root: The repository root.
901 default_presubmit: A default presubmit script to execute in any case.
902 verbose: Prints debug info.
903 output_stream: A stream to write debug output to.
904
905 Return:
906 List of try slaves
907 """
908 presubmit_files = ListRelevantPresubmitFiles(changed_files, repository_root)
909 if not presubmit_files and verbose:
910 output_stream.write("Warning, no presubmit.py found.\n")
911 results = []
912 executer = GetTrySlavesExecuter()
913 if default_presubmit:
914 if verbose:
915 output_stream.write("Running default presubmit script.\n")
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000916 fake_path = os.path.join(repository_root, 'PRESUBMIT.py')
917 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000918 for filename in presubmit_files:
919 filename = os.path.abspath(filename)
920 if verbose:
921 output_stream.write("Running %s\n" % filename)
922 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +0000923 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000924 results += executer.ExecPresubmitScript(presubmit_script, filename)
thestig@chromium.orgde243452009-10-06 21:02:56 +0000925
926 slaves = list(set(results))
927 if slaves and verbose:
928 output_stream.write(', '.join(slaves))
929 output_stream.write('\n')
930 return slaves
931
932
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000933class PresubmitExecuter(object):
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000934 def __init__(self, change, committing, tbr, rietveld, verbose):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000935 """
936 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000937 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000938 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000939 tbr: True if '--tbr' was passed to skip any reviewer/owner checks
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000940 rietveld: rietveld client object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000941 """
maruel@chromium.org4ff922a2009-06-12 20:20:19 +0000942 self.change = change
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000943 self.committing = committing
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000944 self.tbr = tbr
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000945 self.rietveld = rietveld
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000946 self.verbose = verbose
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000947
948 def ExecPresubmitScript(self, script_text, presubmit_path):
949 """Executes a single presubmit script.
950
951 Args:
952 script_text: The text of the presubmit script.
953 presubmit_path: The path to the presubmit file (this will be reported via
954 input_api.PresubmitLocalPath()).
955
956 Return:
957 A list of result objects, empty if no problems.
958 """
chase@chromium.org8e416c82009-10-06 04:30:44 +0000959
960 # Change to the presubmit file's directory to support local imports.
961 main_path = os.getcwd()
962 os.chdir(os.path.dirname(presubmit_path))
963
964 # Load the presubmit script into context.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000965 input_api = InputApi(self.change, presubmit_path, self.committing,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000966 self.tbr, self.rietveld, self.verbose)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000967 context = {}
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000968 try:
969 exec script_text in context
970 except Exception, e:
971 raise PresubmitFailure('"%s" had an exception.\n%s' % (presubmit_path, e))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000972
973 # These function names must change if we make substantial changes to
974 # the presubmit API that are not backwards compatible.
975 if self.committing:
976 function_name = 'CheckChangeOnCommit'
977 else:
978 function_name = 'CheckChangeOnUpload'
979 if function_name in context:
980 context['__args'] = (input_api, OutputApi())
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000981 logging.debug('Running %s in %s' % (function_name, presubmit_path))
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000982 result = eval(function_name + '(*__args)', context)
maruel@chromium.org5d0dc432011-01-03 02:40:37 +0000983 logging.debug('Running %s done.' % function_name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000984 if not (isinstance(result, types.TupleType) or
985 isinstance(result, types.ListType)):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000986 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000987 'Presubmit functions must return a tuple or list')
988 for item in result:
989 if not isinstance(item, OutputApi.PresubmitResult):
maruel@chromium.org899e1c12011-04-07 17:03:18 +0000990 raise PresubmitFailure(
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000991 'All presubmit results must be of types derived from '
992 'output_api.PresubmitResult')
993 else:
994 result = () # no error since the script doesn't care about current event.
995
chase@chromium.org8e416c82009-10-06 04:30:44 +0000996 # Return the process to the original working directory.
997 os.chdir(main_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +0000998 return result
999
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001000
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001001def DoPresubmitChecks(change,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001002 committing,
1003 verbose,
1004 output_stream,
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001005 input_stream,
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001006 default_presubmit,
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001007 may_prompt,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001008 tbr,
1009 rietveld):
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001010 """Runs all presubmit checks that apply to the files in the change.
1011
1012 This finds all PRESUBMIT.py files in directories enclosing the files in the
1013 change (up to the repository root) and calls the relevant entrypoint function
1014 depending on whether the change is being committed or uploaded.
1015
1016 Prints errors, warnings and notifications. Prompts the user for warnings
1017 when needed.
1018
1019 Args:
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001020 change: The Change object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001021 committing: True if 'gcl commit' is running, False if 'gcl upload' is.
1022 verbose: Prints debug info.
1023 output_stream: A stream to write output from presubmit tests to.
1024 input_stream: A stream to read input from the user.
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001025 default_presubmit: A default presubmit script to execute in any case.
maruel@chromium.orgb0dfd352009-06-10 14:12:54 +00001026 may_prompt: Enable (y/n) questions on warning or error.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001027 tbr: was --tbr specified to skip any reviewer/owner checks?
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001028 rietveld: rietveld object.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001029
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001030 Warning:
1031 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream
1032 SHOULD be sys.stdin.
1033
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001034 Return:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001035 A PresubmitOutput object. Use output.should_continue() to figure out
1036 if there were errors or warnings and the caller should abort.
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001037 """
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001038 output = PresubmitOutput(input_stream, output_stream)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001039 if committing:
1040 output.write("Running presubmit commit checks ...\n")
1041 else:
1042 output.write("Running presubmit upload checks ...\n")
jam@chromium.org2a891dc2009-08-20 20:33:37 +00001043 start_time = time.time()
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001044 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
1045 change.RepositoryRoot())
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001046 if not presubmit_files and verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001047 output.write("Warning, no presubmit.py found.\n")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001048 results = []
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001049 executer = PresubmitExecuter(change, committing, tbr, rietveld, verbose)
maruel@chromium.org0ff1fab2009-05-22 13:08:15 +00001050 if default_presubmit:
1051 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001052 output.write("Running default presubmit script.\n")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001053 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
maruel@chromium.org4661e0c2009-06-04 00:45:26 +00001054 results += executer.ExecPresubmitScript(default_presubmit, fake_path)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001055 for filename in presubmit_files:
maruel@chromium.org3d235242009-05-15 12:40:48 +00001056 filename = os.path.abspath(filename)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001057 if verbose:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001058 output.write("Running %s\n" % filename)
maruel@chromium.orgc1675e22009-04-27 20:30:48 +00001059 # Accept CRLF presubmit script.
maruel@chromium.org5aeb7dd2009-11-17 18:09:01 +00001060 presubmit_script = gclient_utils.FileRead(filename, 'rU')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001061 results += executer.ExecPresubmitScript(presubmit_script, filename)
1062
1063 errors = []
1064 notifications = []
1065 warnings = []
1066 for result in results:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001067 if result.fatal:
1068 errors.append(result)
1069 elif result.should_prompt:
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001070 warnings.append(result)
1071 else:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001072 notifications.append(result)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001073
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001074 output.write('\n')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001075 for name, items in (('Messages', notifications),
1076 ('Warnings', warnings),
1077 ('ERRORS', errors)):
1078 if items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001079 output.write('** Presubmit %s **\n' % name)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001080 for item in items:
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001081 item.handle(output)
1082 output.write('\n')
pam@chromium.orged9a0832009-09-09 22:48:55 +00001083
1084 total_time = time.time() - start_time
1085 if total_time > 1.0:
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001086 output.write("Presubmit checks took %.1fs to calculate.\n\n" % total_time)
pam@chromium.orged9a0832009-09-09 22:48:55 +00001087
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001088 if not errors:
1089 if not warnings:
1090 output.write('Presubmit checks passed.\n')
1091 elif may_prompt:
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001092 output.prompt_yes_no('There were presubmit warnings. '
1093 'Are you sure you wish to continue? (y/N): ')
1094 else:
1095 output.fail()
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001096
1097 global _ASKED_FOR_FEEDBACK
1098 # Ask for feedback one time out of 5.
1099 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001100 output.write("Was the presubmit check useful? Please send feedback "
1101 "& hate mail to maruel@chromium.org!\n")
maruel@chromium.orgce8e46b2009-06-26 22:31:51 +00001102 _ASKED_FOR_FEEDBACK = True
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001103 return output
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001104
1105
1106def ScanSubDirs(mask, recursive):
1107 if not recursive:
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001108 return [x for x in glob.glob(mask) if '.svn' not in x and '.git' not in x]
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001109 else:
1110 results = []
1111 for root, dirs, files in os.walk('.'):
1112 if '.svn' in dirs:
1113 dirs.remove('.svn')
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001114 if '.git' in dirs:
1115 dirs.remove('.git')
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001116 for name in files:
1117 if fnmatch.fnmatch(name, mask):
1118 results.append(os.path.join(root, name))
1119 return results
1120
1121
1122def ParseFiles(args, recursive):
maruel@chromium.org7444c502011-02-09 14:02:11 +00001123 logging.debug('Searching for %s' % args)
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001124 files = []
1125 for arg in args:
maruel@chromium.orge3608df2009-11-10 20:22:57 +00001126 files.extend([('M', f) for f in ScanSubDirs(arg, recursive)])
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001127 return files
1128
1129
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001130def load_files(options, args):
1131 """Tries to determine the SCM."""
1132 change_scm = scm.determine_scm(options.root)
1133 files = []
1134 if change_scm == 'svn':
1135 change_class = SvnChange
1136 status_fn = scm.SVN.CaptureStatus
1137 elif change_scm == 'git':
1138 change_class = GitChange
1139 status_fn = scm.GIT.CaptureStatus
1140 else:
1141 logging.info('Doesn\'t seem under source control. Got %d files' % len(args))
1142 if not args:
1143 return None, None
1144 change_class = Change
1145 if args:
1146 files = ParseFiles(args, options.recursive)
1147 else:
1148 # Grab modified files.
1149 files = status_fn([options.root])
1150 return change_class, files
1151
1152
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001153def Main(argv):
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001154 parser = optparse.OptionParser(usage="%prog [options] <files...>",
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001155 version="%prog " + str(__version__))
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001156 parser.add_option("-c", "--commit", action="store_true", default=False,
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001157 help="Use commit instead of upload checks")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001158 parser.add_option("-u", "--upload", action="store_false", dest='commit',
1159 help="Use upload instead of commit checks")
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001160 parser.add_option("-r", "--recursive", action="store_true",
1161 help="Act recursively")
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001162 parser.add_option("-v", "--verbose", action="count", default=0,
1163 help="Use 2 times for more debug info")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001164 parser.add_option("--name", default='no name')
maruel@chromium.org58407af2011-04-12 23:15:57 +00001165 parser.add_option("--author")
maruel@chromium.org4ff922a2009-06-12 20:20:19 +00001166 parser.add_option("--description", default='')
1167 parser.add_option("--issue", type='int', default=0)
1168 parser.add_option("--patchset", type='int', default=0)
maruel@chromium.orgb1901a62010-06-16 00:18:47 +00001169 parser.add_option("--root", default=os.getcwd(),
1170 help="Search for PRESUBMIT.py up to this directory. "
1171 "If inherit-review-settings-ok is present in this "
1172 "directory, parent directories up to the root file "
1173 "system directories will also be searched.")
maruel@chromium.orgc70a2202009-06-17 12:55:10 +00001174 parser.add_option("--default_presubmit")
1175 parser.add_option("--may_prompt", action='store_true', default=False)
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001176 options, args = parser.parse_args(argv)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001177 if options.verbose >= 2:
maruel@chromium.org7444c502011-02-09 14:02:11 +00001178 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001179 elif options.verbose:
1180 logging.basicConfig(level=logging.INFO)
1181 else:
1182 logging.basicConfig(level=logging.ERROR)
maruel@chromium.org5c8c6de2011-03-18 16:20:18 +00001183 change_class, files = load_files(options, args)
1184 if not change_class:
1185 parser.error('For unversioned directory, <files> is not optional.')
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001186 logging.info('Found %d file(s).' % len(files))
1187 try:
1188 results = DoPresubmitChecks(
1189 change_class(options.name,
1190 options.description,
1191 options.root,
1192 files,
1193 options.issue,
maruel@chromium.org58407af2011-04-12 23:15:57 +00001194 options.patchset,
1195 options.author),
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001196 options.commit,
1197 options.verbose,
1198 sys.stdout,
1199 sys.stdin,
1200 options.default_presubmit,
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00001201 options.may_prompt,
1202 False,
1203 None)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001204 return not results.should_continue()
1205 except PresubmitFailure, e:
1206 print >> sys.stderr, e
1207 print >> sys.stderr, 'Maybe your depot_tools is out of date?'
1208 print >> sys.stderr, 'If all fails, contact maruel@'
1209 return 2
maruel@google.comfb2b8eb2009-04-23 21:03:42 +00001210
1211
1212if __name__ == '__main__':
maruel@chromium.org35625c72011-03-23 17:34:02 +00001213 fix_encoding.fix_encoding()
maruel@chromium.org82e5f282011-03-17 14:08:55 +00001214 sys.exit(Main(None))