blob: 8d5084bc4e1fbf818d2a20059b7eb8468d9a51bd [file] [log] [blame]
Doug Anderson44a644f2011-11-02 10:37:37 -07001#!/usr/bin/env python
Jon Salz98255932012-08-18 14:48:02 +08002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysingerae409522014-02-01 03:16:11 -05006"""Presubmit checks to run when doing `repo upload`.
7
8You can add new checks by adding a functions to the HOOKS constants.
9"""
10
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040011from __future__ import print_function
12
Ryan Cui9b651632011-05-11 11:38:58 -070013import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +080014import functools
Dale Curtis2975c432011-05-03 17:25:20 -070015import json
Doug Anderson44a644f2011-11-02 10:37:37 -070016import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070017import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070018import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070019import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070020import subprocess
Peter Ammon811f6702014-06-12 15:45:38 -070021import stat
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070022
Ryan Cui1562fb82011-05-09 11:01:31 -070023from errors import (VerifyException, HookFailure, PrintErrorForProject,
24 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070025
David Jamesc3b68b32013-04-03 09:17:03 -070026# If repo imports us, the __name__ will be __builtin__, and the wrapper will
27# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
28# up. The same logic also happens to work if we're executed directly.
29if __name__ in ('__builtin__', '__main__'):
30 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
31
32from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040033from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070034
Vadim Bendebury2b62d742014-06-22 13:14:51 -070035PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070036
37COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050038 # C++ and friends
39 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
40 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
41 # Scripts
42 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
43 # No extension at all, note that ALL CAPS files are black listed in
44 # COMMON_EXCLUDED_LIST below.
45 r"(^|.*[\\\/])[^.]+$",
46 # Other
47 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070048]
49
Ryan Cui1562fb82011-05-09 11:01:31 -070050
Ryan Cuiec4d6332011-05-02 14:15:25 -070051COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050052 # avoid doing source file checks for kernel
53 r"/src/third_party/kernel/",
54 r"/src/third_party/kernel-next/",
55 r"/src/third_party/ktop/",
56 r"/src/third_party/punybench/",
57 r".*\bexperimental[\\\/].*",
58 r".*\b[A-Z0-9_]{2,}$",
59 r".*[\\\/]debian[\\\/]rules$",
60 # for ebuild trees, ignore any caches and manifest data
61 r".*/Manifest$",
62 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070063
Mike Frysingerae409522014-02-01 03:16:11 -050064 # ignore profiles data (like overlay-tegra2/profiles)
65 r".*/overlay-.*/profiles/.*",
Mike Frysinger98638102014-08-28 00:15:08 -040066 r"^profiles/.*$",
67
Mike Frysingerae409522014-02-01 03:16:11 -050068 # ignore minified js and jquery
69 r".*\.min\.js",
70 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050071
72 # Ignore license files as the content is often taken verbatim.
73 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070074]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070075
Ryan Cui1562fb82011-05-09 11:01:31 -070076
Ryan Cui9b651632011-05-11 11:38:58 -070077_CONFIG_FILE = 'PRESUBMIT.cfg'
78
79
Doug Anderson44a644f2011-11-02 10:37:37 -070080# Exceptions
81
82
83class BadInvocation(Exception):
84 """An Exception indicating a bad invocation of the program."""
85 pass
86
87
Ryan Cui1562fb82011-05-09 11:01:31 -070088# General Helpers
89
Sean Paulba01d402011-05-05 11:36:23 -040090
Doug Anderson44a644f2011-11-02 10:37:37 -070091def _run_command(cmd, cwd=None, stderr=None):
92 """Executes the passed in command and returns raw stdout output.
93
94 Args:
95 cmd: The command to run; should be a list of strings.
96 cwd: The directory to switch to for running the command.
97 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
98 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
99
100 Returns:
101 The standard out from the process.
102 """
103 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
104 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700105
Ryan Cui1562fb82011-05-09 11:01:31 -0700106
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700107def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700108 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700109 if __name__ == '__main__':
110 # Works when file is run on its own (__file__ is defined)...
111 return os.path.abspath(os.path.dirname(__file__))
112 else:
113 # We need to do this when we're run through repo. Since repo executes
114 # us with execfile(), we don't get __file__ defined.
115 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
116 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700117
Ryan Cui1562fb82011-05-09 11:01:31 -0700118
Ryan Cuiec4d6332011-05-02 14:15:25 -0700119def _match_regex_list(subject, expressions):
120 """Try to match a list of regular expressions to a string.
121
122 Args:
123 subject: The string to match regexes on
124 expressions: A list of regular expressions to check for matches with.
125
126 Returns:
127 Whether the passed in subject matches any of the passed in regexes.
128 """
129 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500130 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700131 return True
132 return False
133
Ryan Cui1562fb82011-05-09 11:01:31 -0700134
Mike Frysingerae409522014-02-01 03:16:11 -0500135def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700136 """Filter out files based on the conditions passed in.
137
138 Args:
139 files: list of filepaths to filter
140 include_list: list of regex that when matched with a file path will cause it
141 to be added to the output list unless the file is also matched with a
142 regex in the exclude_list.
143 exclude_list: list of regex that when matched with a file will prevent it
144 from being added to the output list, even if it is also matched with a
145 regex in the include_list.
146
147 Returns:
148 A list of filepaths that contain files matched in the include_list and not
149 in the exclude_list.
150 """
151 filtered = []
152 for f in files:
153 if (_match_regex_list(f, include_list) and
154 not _match_regex_list(f, exclude_list)):
155 filtered.append(f)
156 return filtered
157
Ryan Cuiec4d6332011-05-02 14:15:25 -0700158
159# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700160
161
Ryan Cui4725d952011-05-05 15:41:19 -0700162def _get_upstream_branch():
163 """Returns the upstream tracking branch of the current branch.
164
165 Raises:
166 Error if there is no tracking branch
167 """
168 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
169 current_branch = current_branch.replace('refs/heads/', '')
170 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700171 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700172
173 cfg_option = 'branch.' + current_branch + '.%s'
174 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
175 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
176 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700177 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700178
179 return full_upstream.replace('heads', 'remotes/' + remote)
180
Ryan Cui1562fb82011-05-09 11:01:31 -0700181
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700182def _get_patch(commit):
183 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700184 if commit == PRE_SUBMIT:
185 return _run_command(['git', 'diff', '--cached', 'HEAD'])
186 else:
187 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700188
Ryan Cui1562fb82011-05-09 11:01:31 -0700189
Jon Salz98255932012-08-18 14:48:02 +0800190def _try_utf8_decode(data):
191 """Attempts to decode a string as UTF-8.
192
193 Returns:
194 The decoded Unicode object, or the original string if parsing fails.
195 """
196 try:
197 return unicode(data, 'utf-8', 'strict')
198 except UnicodeDecodeError:
199 return data
200
201
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500202def _get_file_content(path, commit):
203 """Returns the content of a file at a specific commit.
204
205 We can't rely on the file as it exists in the filesystem as people might be
206 uploading a series of changes which modifies the file multiple times.
207
208 Note: The "content" of a symlink is just the target. So if you're expecting
209 a full file, you should check that first. One way to detect is that the
210 content will not have any newlines.
211 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700212 if commit == PRE_SUBMIT:
213 return _run_command(['git', 'diff', 'HEAD', path])
214 else:
215 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500216
217
Mike Frysingerae409522014-02-01 03:16:11 -0500218def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700219 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700220 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff']
221 if commit == PRE_SUBMIT:
222 command += ['HEAD', path]
223 else:
224 command += [commit, path]
225 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700226
227 new_lines = []
228 line_num = 0
229 for line in output.splitlines():
230 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
231 if m:
232 line_num = int(m.groups(1)[0])
233 continue
234 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800235 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700236 if not line.startswith('-'):
237 line_num += 1
238 return new_lines
239
Ryan Cui1562fb82011-05-09 11:01:31 -0700240
Peter Ammon811f6702014-06-12 15:45:38 -0700241def _parse_affected_files(output, include_deletes=False, relative=False):
242 """Parses git diff's 'raw' format, returning a list of modified file paths.
243
244 This excludes directories and symlinks, and optionally includes files that
245 were deleted.
Doug Anderson42b8a052013-06-26 10:45:36 -0700246
247 Args:
Peter Ammon811f6702014-06-12 15:45:38 -0700248 output: The result of the 'git diff --raw' command
249 include_deletes: If true, we'll include deleted files in the result
250 relative: Whether to return relative or full paths to files
Doug Anderson42b8a052013-06-26 10:45:36 -0700251
252 Returns:
253 A list of modified/added (and perhaps deleted) files
254 """
Ryan Cuiec4d6332011-05-02 14:15:25 -0700255 files = []
Peter Ammon811f6702014-06-12 15:45:38 -0700256 # See the documentation for 'git diff --raw' for the relevant format.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700257 for statusline in output.splitlines():
Peter Ammon811f6702014-06-12 15:45:38 -0700258 attributes, paths = statusline.split('\t', 1)
259 _, mode, _, _, status = attributes.split(' ')
260
261 # Ignore symlinks and directories.
262 imode = int(mode, 8)
263 if stat.S_ISDIR(imode) or stat.S_ISLNK(imode):
264 continue
265
266 # Ignore deleted files, and optionally return absolute paths of files.
267 if include_deletes or status != 'D':
268 # If a file was merely modified, we will have a single file path.
269 # If it was moved, we will have two paths (source and destination).
270 # In either case, we want the last path.
271 f = paths.split('\t')[-1]
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500272 if not relative:
273 pwd = os.getcwd()
274 f = os.path.join(pwd, f)
275 files.append(f)
Peter Ammon811f6702014-06-12 15:45:38 -0700276
Ryan Cuiec4d6332011-05-02 14:15:25 -0700277 return files
278
Ryan Cui1562fb82011-05-09 11:01:31 -0700279
Peter Ammon811f6702014-06-12 15:45:38 -0700280def _get_affected_files(commit, include_deletes=False, relative=False):
281 """Returns list of file paths that were modified/added, excluding symlinks.
282
283 Args:
284 commit: The commit
285 include_deletes: If true, we'll include deleted files in the result
286 relative: Whether to return relative or full paths to files
287
288 Returns:
289 A list of modified/added (and perhaps deleted) files
290 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700291 if commit == PRE_SUBMIT:
292 return _run_command(['git', 'diff-index', '--cached',
293 '--name-only', 'HEAD']).split()
Peter Ammon811f6702014-06-12 15:45:38 -0700294 output = _run_command(['git', 'diff', '--raw', commit + '^!'])
295 return _parse_affected_files(output, include_deletes, relative)
296
297
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700298def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700299 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700300 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700301 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700302
Ryan Cui1562fb82011-05-09 11:01:31 -0700303
Ryan Cuiec4d6332011-05-02 14:15:25 -0700304def _get_commit_desc(commit):
305 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700306 if commit == PRE_SUBMIT:
307 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400308 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700309
310
311# Common Hooks
312
Ryan Cui1562fb82011-05-09 11:01:31 -0700313
Mike Frysingerae409522014-02-01 03:16:11 -0500314def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700315 """Checks that there aren't any lines longer than maxlen characters in any of
316 the text files to be submitted.
317 """
318 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800319 SKIP_REGEXP = re.compile('|'.join([
320 r'https?://',
321 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700322
323 errors = []
324 files = _filter_files(_get_affected_files(commit),
325 COMMON_INCLUDED_PATHS,
326 COMMON_EXCLUDED_PATHS)
327
328 for afile in files:
329 for line_num, line in _get_file_diff(afile, commit):
330 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500331 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800332 continue
333
334 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
335 if len(errors) == 5: # Just show the first 5 errors.
336 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700337
338 if errors:
339 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700340 return HookFailure(msg, errors)
341
Ryan Cuiec4d6332011-05-02 14:15:25 -0700342
Mike Frysingerae409522014-02-01 03:16:11 -0500343def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700344 """Checks that there is no stray whitespace at source lines end."""
345 errors = []
346 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500347 COMMON_INCLUDED_PATHS,
348 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700349 for afile in files:
350 for line_num, line in _get_file_diff(afile, commit):
351 if line.rstrip() != line:
352 errors.append('%s, line %s' % (afile, line_num))
353 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700354 return HookFailure('Found line ending with white space in:', errors)
355
Ryan Cuiec4d6332011-05-02 14:15:25 -0700356
Mike Frysingerae409522014-02-01 03:16:11 -0500357def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700358 """Checks there are no unexpanded tabs."""
359 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700360 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700361 r".*\.ebuild$",
362 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500363 r".*/[M|m]akefile$",
364 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700365 ]
366
367 errors = []
368 files = _filter_files(_get_affected_files(commit),
369 COMMON_INCLUDED_PATHS,
370 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
371
372 for afile in files:
373 for line_num, line in _get_file_diff(afile, commit):
374 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500375 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700376 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700377 return HookFailure('Found a tab character in:', errors)
378
Ryan Cuiec4d6332011-05-02 14:15:25 -0700379
Mike Frysingerae409522014-02-01 03:16:11 -0500380def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700381 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700382 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700383
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700384 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700385 msg = 'Changelist description needs TEST field (after first line)'
386 return HookFailure(msg)
387
Ryan Cuiec4d6332011-05-02 14:15:25 -0700388
Mike Frysingerae409522014-02-01 03:16:11 -0500389def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700390 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
391 msg = 'Changelist has invalid CQ-DEPEND target.'
392 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
393 try:
394 patch.GetPaladinDeps(_get_commit_desc(commit))
395 except ValueError as ex:
396 return HookFailure(msg, [example, str(ex)])
397
398
Mike Frysingerae409522014-02-01 03:16:11 -0500399def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700400 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700401 OLD_BUG_RE = r'\nBUG=.*chromium-os'
402 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
403 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
404 'the chromium tracker in your BUG= line now.')
405 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700406
David James5c0073d2013-04-03 08:48:52 -0700407 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700408 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700409 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700410 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700411 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
412 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700413 return HookFailure(msg)
414
Ryan Cuiec4d6332011-05-02 14:15:25 -0700415
Doug Anderson42b8a052013-06-26 10:45:36 -0700416def _check_for_uprev(project, commit):
417 """Check that we're not missing a revbump of an ebuild in the given commit.
418
419 If the given commit touches files in a directory that has ebuilds somewhere
420 up the directory hierarchy, it's very likely that we need an ebuild revbump
421 in order for those changes to take effect.
422
423 It's not totally trivial to detect a revbump, so at least detect that an
424 ebuild with a revision number in it was touched. This should handle the
425 common case where we use a symlink to do the revbump.
426
427 TODO: it would be nice to enhance this hook to:
428 * Handle cases where people revbump with a slightly different syntax. I see
429 one ebuild (puppy) that revbumps with _pN. This is a false positive.
430 * Catches cases where people aren't using symlinks for revbumps. If they
431 edit a revisioned file directly (and are expected to rename it for revbump)
432 we'll miss that. Perhaps we could detect that the file touched is a
433 symlink?
434
435 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
436 still better off than without this check.
437
438 Args:
439 project: The project to look at
440 commit: The commit to look at
441
442 Returns:
443 A HookFailure or None.
444 """
Mike Frysinger011af942014-01-17 16:12:22 -0500445 # If this is the portage-stable overlay, then ignore the check. It's rare
446 # that we're doing anything other than importing files from upstream, so
447 # forcing a rev bump makes no sense.
448 whitelist = (
449 'chromiumos/overlays/portage-stable',
450 )
451 if project in whitelist:
452 return None
453
Doug Anderson42b8a052013-06-26 10:45:36 -0700454 affected_paths = _get_affected_files(commit, include_deletes=True)
455
456 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500457 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700458 affected_paths = [path for path in affected_paths
459 if os.path.basename(path) not in whitelist]
460 if not affected_paths:
461 return None
462
463 # If we've touched any file named with a -rN.ebuild then we'll say we're
464 # OK right away. See TODO above about enhancing this.
465 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
466 for path in affected_paths)
467 if touched_revved_ebuild:
468 return None
469
470 # We want to examine the current contents of all directories that are parents
471 # of files that were touched (up to the top of the project).
472 #
473 # ...note: we use the current directory contents even though it may have
474 # changed since the commit we're looking at. This is just a heuristic after
475 # all. Worst case we don't flag a missing revbump.
476 project_top = os.getcwd()
477 dirs_to_check = set([project_top])
478 for path in affected_paths:
479 path = os.path.dirname(path)
480 while os.path.exists(path) and not os.path.samefile(path, project_top):
481 dirs_to_check.add(path)
482 path = os.path.dirname(path)
483
484 # Look through each directory. If it's got an ebuild in it then we'll
485 # consider this as a case when we need a revbump.
486 for dir_path in dirs_to_check:
487 contents = os.listdir(dir_path)
488 ebuilds = [os.path.join(dir_path, path)
489 for path in contents if path.endswith('.ebuild')]
490 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
491
492 # If the -9999.ebuild file was touched the bot will uprev for us.
493 # ...we'll use a simple intersection here as a heuristic...
494 if set(ebuilds_9999) & set(affected_paths):
495 continue
496
497 if ebuilds:
498 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
499 'or a -r1.ebuild symlink if this is a new ebuild')
500
501 return None
502
503
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500504def _check_ebuild_eapi(project, commit):
505 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
506
507 We want to get away from older EAPI's as it makes life confusing and they
508 have less builtin error checking.
509
510 Args:
511 project: The project to look at
512 commit: The commit to look at
513
514 Returns:
515 A HookFailure or None.
516 """
517 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500518 # that we're doing anything other than importing files from upstream, and
519 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500520 whitelist = (
521 'chromiumos/overlays/portage-stable',
522 )
523 if project in whitelist:
524 return None
525
526 BAD_EAPIS = ('0', '1', '2', '3')
527
528 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
529
530 ebuilds_re = [r'\.ebuild$']
531 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
532 ebuilds_re)
533 bad_ebuilds = []
534
535 for ebuild in ebuilds:
536 # If the ebuild does not specify an EAPI, it defaults to 0.
537 eapi = '0'
538
539 lines = _get_file_content(ebuild, commit).splitlines()
540 if len(lines) == 1:
541 # This is most likely a symlink, so skip it entirely.
542 continue
543
544 for line in lines:
545 m = get_eapi.match(line)
546 if m:
547 # Once we hit the first EAPI line in this ebuild, stop processing.
548 # The spec requires that there only be one and it be first, so
549 # checking all possible values is pointless. We also assume that
550 # it's "the" EAPI line and not something in the middle of a heredoc.
551 eapi = m.group(1)
552 break
553
554 if eapi in BAD_EAPIS:
555 bad_ebuilds.append((ebuild, eapi))
556
557 if bad_ebuilds:
558 # pylint: disable=C0301
559 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
560 # pylint: enable=C0301
561 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500562 'These ebuilds are using old EAPIs. If these are imported from\n'
563 'Gentoo, then you may ignore and upload once with the --no-verify\n'
564 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500565 '\t%s\n'
566 'See this guide for more details:\n%s\n' %
567 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
568
569
Mike Frysinger89bdb852014-02-01 05:26:26 -0500570def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500571 """Make sure we use the new style KEYWORDS when possible in ebuilds.
572
573 If an ebuild generally does not care about the arch it is running on, then
574 ebuilds should flag it with one of:
575 KEYWORDS="*" # A stable ebuild.
576 KEYWORDS="~*" # An unstable ebuild.
577 KEYWORDS="-* ..." # Is known to only work on specific arches.
578
579 Args:
580 project: The project to look at
581 commit: The commit to look at
582
583 Returns:
584 A HookFailure or None.
585 """
586 WHITELIST = set(('*', '-*', '~*'))
587
588 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
589
Mike Frysinger89bdb852014-02-01 05:26:26 -0500590 ebuilds_re = [r'\.ebuild$']
591 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
592 ebuilds_re)
593
Mike Frysingerc51ece72014-01-17 16:23:40 -0500594 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400595 # We get the full content rather than a diff as the latter does not work
596 # on new files (like when adding new ebuilds).
597 lines = _get_file_content(ebuild, commit).splitlines()
598 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500599 m = get_keywords.match(line)
600 if m:
601 keywords = set(m.group(1).split())
602 if not keywords or WHITELIST - keywords != WHITELIST:
603 continue
604
605 return HookFailure(
606 'Please update KEYWORDS to use a glob:\n'
607 'If the ebuild should be marked stable (normal for non-9999 '
608 'ebuilds):\n'
609 ' KEYWORDS="*"\n'
610 'If the ebuild should be marked unstable (normal for '
611 'cros-workon / 9999 ebuilds):\n'
612 ' KEYWORDS="~*"\n'
613 'If the ebuild needs to be marked for only specific arches,'
614 'then use -* like so:\n'
615 ' KEYWORDS="-* arm ..."\n')
616
617
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800618def _check_ebuild_licenses(_project, commit):
619 """Check if the LICENSE field in the ebuild is correct."""
620 affected_paths = _get_affected_files(commit)
621 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
622
623 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800624 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800625
626 for ebuild in touched_ebuilds:
627 # Skip virutal packages.
628 if ebuild.split('/')[-3] == 'virtual':
629 continue
630
631 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400632 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800633 except ValueError as e:
634 return HookFailure(e.message, [ebuild])
635
636 # Also ignore licenses ending with '?'
637 for license_type in [x for x in license_types
638 if x not in LICENSES_IGNORE and not x.endswith('?')]:
639 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400640 licenses_lib.Licensing.FindLicenseType(license_type)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800641 except AssertionError as e:
642 return HookFailure(e.message, [ebuild])
643
644
Mike Frysingercd363c82014-02-01 05:20:18 -0500645def _check_ebuild_virtual_pv(project, commit):
646 """Enforce the virtual PV policies."""
647 # If this is the portage-stable overlay, then ignore the check.
648 # We want to import virtuals as-is from upstream Gentoo.
649 whitelist = (
650 'chromiumos/overlays/portage-stable',
651 )
652 if project in whitelist:
653 return None
654
655 # We assume the repo name is the same as the dir name on disk.
656 # It would be dumb to not have them match though.
657 project = os.path.basename(project)
658
659 is_variant = lambda x: x.startswith('overlay-variant-')
660 is_board = lambda x: x.startswith('overlay-')
661 is_private = lambda x: x.endswith('-private')
662
663 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
664
665 ebuilds_re = [r'\.ebuild$']
666 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
667 ebuilds_re)
668 bad_ebuilds = []
669
670 for ebuild in ebuilds:
671 m = get_pv.match(ebuild)
672 if m:
673 overlay = m.group(1)
674 if not overlay or not is_board(overlay):
675 overlay = project
676
677 pv = m.group(3).split('-', 1)[0]
678
679 if is_private(overlay):
680 want_pv = '3.5' if is_variant(overlay) else '3'
681 elif is_board(overlay):
682 want_pv = '2.5' if is_variant(overlay) else '2'
683 else:
684 want_pv = '1'
685
686 if pv != want_pv:
687 bad_ebuilds.append((ebuild, pv, want_pv))
688
689 if bad_ebuilds:
690 # pylint: disable=C0301
691 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
692 # pylint: enable=C0301
693 return HookFailure(
694 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
695 '\t%s\n'
696 'If this is an upstream Gentoo virtual, then you may ignore this\n'
697 'check (and re-run w/--no-verify). Otherwise, please see this\n'
698 'page for more details:\n%s\n' %
699 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
700 for x in bad_ebuilds]), url))
701
702
Mike Frysingerae409522014-02-01 03:16:11 -0500703def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700704 """Verify that Change-ID is present in last paragraph of commit message."""
705 desc = _get_commit_desc(commit)
706 loc = desc.rfind('\nChange-Id:')
707 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700708 return HookFailure('Change-Id must be in last paragraph of description.')
709
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700710
Mike Frysingerae409522014-02-01 03:16:11 -0500711def _check_license(_project, commit):
Mike Frysinger98638102014-08-28 00:15:08 -0400712 """Verifies the license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700713
Mike Frysinger98638102014-08-28 00:15:08 -0400714 Should be following the spec:
715 http://dev.chromium.org/developers/coding-style#TOC-File-headers
716 """
717 # For older years, be a bit more flexible as our policy says leave them be.
718 LICENSE_HEADER = (
719 r'.* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
720 'All rights reserved\.' '\n'
721 r'.* Use of this source code is governed by a BSD-style license that can '
722 'be\n'
723 r'.* found in the LICENSE file\.'
724 '\n'
725 )
726 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
727
728 # For newer years, be stricter.
729 COPYRIGHT_LINE = (
730 r'.* Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
731 'All rights reserved\.' '\n'
732 )
733 copyright_re = re.compile(COPYRIGHT_LINE)
734
735 bad_files = []
736 bad_copyright_files = []
737 files = _filter_files(_get_affected_files(commit, relative=True),
738 COMMON_INCLUDED_PATHS,
739 COMMON_EXCLUDED_PATHS)
740
741 for f in files:
742 contents = _get_file_content(f, commit)
743 if not contents:
744 # Ignore empty files.
745 continue
746
747 if not license_re.search(contents):
748 bad_files.append(f)
749 elif copyright_re.search(contents):
750 bad_copyright_files.append(f)
751
752 if bad_files:
753 msg = '%s:\n%s\n%s' % (
754 'License must match', license_re.pattern,
755 'Found a bad header in these files:')
756 return HookFailure(msg, bad_files)
757
758 if bad_copyright_files:
759 msg = 'Do not use (c) in copyright headers in new files:'
760 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700761
762
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400763def _check_layout_conf(_project, commit):
764 """Verifies the metadata/layout.conf file."""
765 repo_name = 'profiles/repo_name'
766 layout_path = 'metadata/layout.conf'
767
768 files = _get_affected_files(commit, relative=True)
769
770 # Disallow new repos with the repo_name file.
771 if repo_name in files:
772 return HookFailure('%s: use "repo-name" in %s instead' %
773 (repo_name, layout_path))
774
775 # If the layout.conf file doesn't exist, nothing else to do.
776 if layout_path not in files:
777 return
778
779 errors = []
780
781 # Make sure the config file is sorted.
782 data = [x for x in _get_file_content(layout_path, commit).splitlines()
783 if x and x[0] != '#']
784 if sorted(data) != data:
785 errors += ['keep lines sorted']
786
787 # Require people to set specific values all the time.
788 settings = (
789 # TODO: Enable this for everyone. http://crbug.com/408038
790 #('fast caching', 'cache-format = md5-dict'),
791 ('fast manifests', 'thin-manifests = true'),
792 ('extra features', 'profile-formats = portage-2'),
793 )
794 for reason, line in settings:
795 if line not in data:
796 errors += ['enable %s with: %s' % (reason, line)]
797
798 # Require one of these settings.
799 if ('use-manifests = true' not in data and
800 'use-manifests = strict' not in data):
801 errors += ['enable file checking with: use-manifests = true']
802
803 if errors:
804 lines = [('%s: error(s) detected '
805 '(see the portage(5) man page for more details)') %
806 layout_path] + errors
807 return HookFailure('\n\t- '.join(lines))
808
809
Ryan Cuiec4d6332011-05-02 14:15:25 -0700810# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700811
Ryan Cui1562fb82011-05-09 11:01:31 -0700812
Mike Frysingerae409522014-02-01 03:16:11 -0500813def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700814 """Runs checkpatch.pl on the given project"""
815 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700816 options = list(options)
817 if commit == PRE_SUBMIT:
818 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
819 # this case.
820 options.append('--ignore=MISSING_SIGN_OFF')
821 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700822 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700823 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700824 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700825 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700826
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700827
Anton Staaf815d6852011-08-22 10:08:45 -0700828def _run_checkpatch_no_tree(project, commit):
829 return _run_checkpatch(project, commit, ['--no-tree'])
830
Mike Frysingerae409522014-02-01 03:16:11 -0500831
Randall Spangler7318fd62013-11-21 12:16:58 -0800832def _run_checkpatch_ec(project, commit):
833 """Runs checkpatch with options for Chromium EC projects."""
834 return _run_checkpatch(project, commit, ['--no-tree',
835 '--ignore=MSLEEP,VOLATILE'])
836
Mike Frysingerae409522014-02-01 03:16:11 -0500837
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700838def _run_checkpatch_depthcharge(project, commit):
839 """Runs checkpatch with options for depthcharge."""
840 return _run_checkpatch(project, commit, [
841 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700842 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
843 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
844 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700845
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700846def _run_checkpatch_coreboot(project, commit):
847 """Runs checkpatch with options for coreboot."""
848 return _run_checkpatch(project, commit, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700849 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700850 '--no-tree',
851 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
852 'GLOBAL_INITIALISERS,INITIALISED_STATIC'])
853
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700854
Mike Frysingerae409522014-02-01 03:16:11 -0500855def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700856 """Makes sure kernel config changes are not mixed with code changes"""
857 files = _get_affected_files(commit)
858 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
859 return HookFailure('Changes to chromeos/config/ and regular files must '
860 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700861
Mike Frysingerae409522014-02-01 03:16:11 -0500862
863def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700864 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700865 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700866 try:
867 json.load(open(f))
868 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700869 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700870
871
Mike Frysingerae409522014-02-01 03:16:11 -0500872def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400873 """Make sure Manifest files only have DIST lines"""
874 paths = []
875
876 for path in _get_affected_files(commit):
877 if os.path.basename(path) != 'Manifest':
878 continue
879 if not os.path.exists(path):
880 continue
881
882 with open(path, 'r') as f:
883 for line in f.readlines():
884 if not line.startswith('DIST '):
885 paths.append(path)
886 break
887
888 if paths:
889 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
890 ('\n'.join(paths),))
891
892
Mike Frysingerae409522014-02-01 03:16:11 -0500893def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700894 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700895 if commit == PRE_SUBMIT:
896 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700897 BRANCH_RE = r'\nBRANCH=\S+'
898
899 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
900 msg = ('Changelist description needs BRANCH field (after first line)\n'
901 'E.g. BRANCH=none or BRANCH=link,snow')
902 return HookFailure(msg)
903
904
Mike Frysingerae409522014-02-01 03:16:11 -0500905def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800906 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700907 if commit == PRE_SUBMIT:
908 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800909 SIGNOFF_RE = r'\nSigned-off-by: \S+'
910
911 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
912 msg = ('Changelist description needs Signed-off-by: field\n'
913 'E.g. Signed-off-by: My Name <me@chromium.org>')
914 return HookFailure(msg)
915
916
Jon Salz3ee59de2012-08-18 13:54:22 +0800917def _run_project_hook_script(script, project, commit):
918 """Runs a project hook script.
919
920 The script is run with the following environment variables set:
921 PRESUBMIT_PROJECT: The affected project
922 PRESUBMIT_COMMIT: The affected commit
923 PRESUBMIT_FILES: A newline-separated list of affected files
924
925 The script is considered to fail if the exit code is non-zero. It should
926 write an error message to stdout.
927 """
928 env = dict(os.environ)
929 env['PRESUBMIT_PROJECT'] = project
930 env['PRESUBMIT_COMMIT'] = commit
931
932 # Put affected files in an environment variable
933 files = _get_affected_files(commit)
934 env['PRESUBMIT_FILES'] = '\n'.join(files)
935
936 process = subprocess.Popen(script, env=env, shell=True,
937 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800938 stdout=subprocess.PIPE,
939 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800940 stdout, _ = process.communicate()
941 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800942 if stdout:
943 stdout = re.sub('(?m)^', ' ', stdout)
944 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800945 (script, process.returncode,
946 ':\n' + stdout if stdout else ''))
947
948
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700949def _moved_to_platform2(project, _commit):
950 """Forbids commits to legacy repo in src/platform."""
951 return HookFailure('%s has been moved to platform2. This change should be '
952 'made there.' % project)
953
954
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700955def _check_project_prefix(_project, commit):
956 """Fails if the change is project specific and the commit message is not
957 prefixed by the project_name.
958 """
959
960 files = _get_affected_files(commit, relative=True)
961 prefix = os.path.commonprefix(files)
962 prefix = os.path.dirname(prefix)
963
964 # If there is no common prefix, the CL span multiple projects.
965 if prefix == '':
966 return
967
968 project_name = prefix.split('/')[0]
969 alias_file = os.path.join(prefix, '.project_alias')
970 # If an alias exists, use it.
971 if os.path.isfile(alias_file):
972 with open(alias_file, 'r') as f:
973 project_name = f.read().strip()
974
975 if not _get_commit_desc(commit).startswith(project_name + ': '):
976 return HookFailure('The commit title for changes affecting only %s'
977 ' should start with \"%s: \"'
978 % (project_name, project_name))
979
980
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700981# Base
982
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700983# A list of hooks which are not project specific and check patch description
984# (as opposed to patch body).
985_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -0700986 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700987 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700988 _check_change_has_test_field,
989 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700990]
991
992
993# A list of hooks that are not project-specific
994_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500995 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500996 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800997 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500998 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700999 _check_no_stray_whitespace,
1000 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001001 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001002 _check_license,
1003 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001004 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001005]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001006
Ryan Cui1562fb82011-05-09 11:01:31 -07001007
Ryan Cui9b651632011-05-11 11:38:58 -07001008# A dictionary of project-specific hooks(callbacks), indexed by project name.
1009# dict[project] = [callback1, callback2]
1010_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001011 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001012 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1013 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001014 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001015 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001016 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001017 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1018 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001019 "chromiumos/overlays/board-overlays": [_check_manifests],
1020 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1021 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001022 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001023 "chromiumos/platform/depthcharge": [_check_change_has_signoff_field,
1024 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001025 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001026 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001027 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001028 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001029 "chromiumos/third_party/coreboot": [_check_change_has_signoff_field,
1030 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001031 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001032 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1033 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1034 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001035 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001036}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001037
Ryan Cui1562fb82011-05-09 11:01:31 -07001038
Ryan Cui9b651632011-05-11 11:38:58 -07001039# A dictionary of flags (keys) that can appear in the config file, and the hook
1040# that the flag disables (value)
1041_DISABLE_FLAGS = {
1042 'stray_whitespace_check': _check_no_stray_whitespace,
1043 'long_line_check': _check_no_long_lines,
1044 'cros_license_check': _check_license,
1045 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001046 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001047 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001048 'bug_field_check': _check_change_has_bug_field,
1049 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001050}
1051
1052
Jon Salz3ee59de2012-08-18 13:54:22 +08001053def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001054 """Returns a set of hooks disabled by the current project's config file.
1055
1056 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001057
1058 Args:
1059 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001060 """
1061 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001062 if not config.has_section(SECTION):
1063 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001064
1065 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001066 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001067 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001068 if not config.getboolean(SECTION, flag):
1069 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001070 except ValueError as e:
1071 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001072 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001073
1074 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1075 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1076
1077
Jon Salz3ee59de2012-08-18 13:54:22 +08001078def _get_project_hook_scripts(config):
1079 """Returns a list of project-specific hook scripts.
1080
1081 Args:
1082 config: A ConfigParser for the project's config file.
1083 """
1084 SECTION = 'Hook Scripts'
1085 if not config.has_section(SECTION):
1086 return []
1087
1088 hook_names_values = config.items(SECTION)
1089 hook_names_values.sort(key=lambda x: x[0])
1090 return [x[1] for x in hook_names_values]
1091
1092
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001093def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001094 """Returns a list of hooks that need to be run for a project.
1095
1096 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001097
1098 Args:
1099 project: A string, name of the project.
1100 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001101 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001102 config = ConfigParser.RawConfigParser()
1103 try:
1104 config.read(_CONFIG_FILE)
1105 except ConfigParser.Error:
1106 # Just use an empty config file
1107 config = ConfigParser.RawConfigParser()
1108
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001109 if presubmit:
1110 hook_list = _COMMON_HOOKS
1111 else:
1112 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1113
Jon Salz3ee59de2012-08-18 13:54:22 +08001114 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001115 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001116
1117 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001118 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1119 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001120
Jon Salz3ee59de2012-08-18 13:54:22 +08001121 for script in _get_project_hook_scripts(config):
1122 hooks.append(functools.partial(_run_project_hook_script, script))
1123
Ryan Cui9b651632011-05-11 11:38:58 -07001124 return hooks
1125
1126
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001127def _run_project_hooks(project, proj_dir=None,
1128 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001129 """For each project run its project specific hook from the hooks dictionary.
1130
1131 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001132 project: The name of project to run hooks for.
1133 proj_dir: If non-None, this is the directory the project is in. If None,
1134 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001135 commit_list: A list of commits to run hooks against. If None or empty list
1136 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001137 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001138
1139 Returns:
1140 Boolean value of whether any errors were ecountered while running the hooks.
1141 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001142 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001143 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1144 if len(proj_dirs) == 0:
1145 print('%s cannot be found.' % project, file=sys.stderr)
1146 print('Please specify a valid project.', file=sys.stderr)
1147 return True
1148 if len(proj_dirs) > 1:
1149 print('%s is associated with multiple directories.' % project,
1150 file=sys.stderr)
1151 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1152 return True
1153 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001154
Ryan Cuiec4d6332011-05-02 14:15:25 -07001155 pwd = os.getcwd()
1156 # hooks assume they are run from the root of the project
1157 os.chdir(proj_dir)
1158
Doug Anderson14749562013-06-26 13:38:29 -07001159 if not commit_list:
1160 try:
1161 commit_list = _get_commits()
1162 except VerifyException as e:
1163 PrintErrorForProject(project, HookFailure(str(e)))
1164 os.chdir(pwd)
1165 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001166
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001167 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001168 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001169 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001170 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001171 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001172 hook_error = hook(project, commit)
1173 if hook_error:
1174 error_list.append(hook_error)
1175 error_found = True
1176 if error_list:
1177 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1178 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001179
Ryan Cuiec4d6332011-05-02 14:15:25 -07001180 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001181 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001182
Mike Frysingerae409522014-02-01 03:16:11 -05001183
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001184# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001185
Ryan Cui1562fb82011-05-09 11:01:31 -07001186
Mike Frysingerae409522014-02-01 03:16:11 -05001187def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001188 """Main function invoked directly by repo.
1189
1190 This function will exit directly upon error so that repo doesn't print some
1191 obscure error message.
1192
1193 Args:
1194 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001195 worktree_list: A list of directories. It should be the same length as
1196 project_list, so that each entry in project_list matches with a directory
1197 in worktree_list. If None, we will attempt to calculate the directories
1198 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001199 kwargs: Leave this here for forward-compatibility.
1200 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001201 found_error = False
David James2edd9002013-10-11 14:09:19 -07001202 if not worktree_list:
1203 worktree_list = [None] * len(project_list)
1204 for project, worktree in zip(project_list, worktree_list):
1205 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001206 found_error = True
1207
Mike Frysingerae409522014-02-01 03:16:11 -05001208 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001209 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001210 '- To disable some source style checks, and for other hints, see '
1211 '<checkout_dir>/src/repohooks/README\n'
1212 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001213 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001214 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001215
Ryan Cui1562fb82011-05-09 11:01:31 -07001216
Doug Anderson44a644f2011-11-02 10:37:37 -07001217def _identify_project(path):
1218 """Identify the repo project associated with the given path.
1219
1220 Returns:
1221 A string indicating what project is associated with the path passed in or
1222 a blank string upon failure.
1223 """
1224 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1225 stderr=subprocess.PIPE, cwd=path).strip()
1226
1227
1228def direct_main(args, verbose=False):
1229 """Run hooks directly (outside of the context of repo).
1230
1231 # Setup for doctests below.
1232 # ...note that some tests assume that running pre-upload on this CWD is fine.
1233 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1234 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1235 >>> olddir = os.getcwd()
1236
1237 # OK to run w/ no arugments; will run with CWD.
1238 >>> os.chdir(mydir)
1239 >>> direct_main(['prog_name'], verbose=True)
1240 Running hooks on chromiumos/repohooks
1241 0
1242 >>> os.chdir(olddir)
1243
1244 # Run specifying a dir
1245 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1246 Running hooks on chromiumos/repohooks
1247 0
1248
1249 # Not a problem to use a bogus project; we'll just get default settings.
1250 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1251 Running hooks on X
1252 0
1253
1254 # Run with project but no dir
1255 >>> os.chdir(mydir)
1256 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1257 Running hooks on X
1258 0
1259 >>> os.chdir(olddir)
1260
1261 # Try with a non-git CWD
1262 >>> os.chdir('/tmp')
1263 >>> direct_main(['prog_name'])
1264 Traceback (most recent call last):
1265 ...
1266 BadInvocation: The current directory is not part of a git project.
1267
1268 # Check various bad arguments...
1269 >>> direct_main(['prog_name', 'bogus'])
1270 Traceback (most recent call last):
1271 ...
1272 BadInvocation: Unexpected arguments: bogus
1273 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1274 Traceback (most recent call last):
1275 ...
1276 BadInvocation: Invalid dir: bogusdir
1277 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1278 Traceback (most recent call last):
1279 ...
1280 BadInvocation: Not a git directory: /tmp
1281
1282 Args:
1283 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001284 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001285
1286 Returns:
1287 0 if no pre-upload failures, 1 if failures.
1288
1289 Raises:
1290 BadInvocation: On some types of invocation errors.
1291 """
1292 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1293 parser = optparse.OptionParser(description=desc)
1294
1295 parser.add_option('--dir', default=None,
1296 help='The directory that the project lives in. If not '
1297 'specified, use the git project root based on the cwd.')
1298 parser.add_option('--project', default=None,
1299 help='The project repo path; this can affect how the hooks '
1300 'get run, since some hooks are project-specific. For '
1301 'chromite this is chromiumos/chromite. If not specified, '
1302 'the repo tool will be used to figure this out based on '
1303 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001304 parser.add_option('--rerun-since', default=None,
1305 help='Rerun hooks on old commits since the given date. '
1306 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001307 'e.g. 2012-06-20. This option is mutually exclusive '
1308 'with --pre-submit.')
1309 parser.add_option('--pre-submit', action="store_true",
1310 help='Run the check against the pending commit. '
1311 'This option should be used at the \'git commit\' '
1312 'phase as opposed to \'repo upload\'. This option '
1313 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001314
1315 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001316
1317 opts, args = parser.parse_args(args[1:])
1318
Doug Anderson14749562013-06-26 13:38:29 -07001319 if opts.rerun_since:
1320 if args:
1321 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1322 ' '.join(args))
1323
1324 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1325 all_commits = _run_command(cmd).splitlines()
1326 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1327
1328 # Eliminate chrome-bot commits but keep ordering the same...
1329 bot_commits = set(bot_commits)
1330 args = [c for c in all_commits if c not in bot_commits]
1331
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001332 if opts.pre_submit:
1333 raise BadInvocation('rerun-since and pre-submit can not be '
1334 'used together')
1335 if opts.pre_submit:
1336 if args:
1337 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1338 ' '.join(args))
1339 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001340
1341 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1342 # project from CWD
1343 if opts.dir is None:
1344 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1345 stderr=subprocess.PIPE).strip()
1346 if not git_dir:
1347 raise BadInvocation('The current directory is not part of a git project.')
1348 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1349 elif not os.path.isdir(opts.dir):
1350 raise BadInvocation('Invalid dir: %s' % opts.dir)
1351 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1352 raise BadInvocation('Not a git directory: %s' % opts.dir)
1353
1354 # Identify the project if it wasn't specified; this _requires_ the repo
1355 # tool to be installed and for the project to be part of a repo checkout.
1356 if not opts.project:
1357 opts.project = _identify_project(opts.dir)
1358 if not opts.project:
1359 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1360
1361 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001362 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001363
Doug Anderson14749562013-06-26 13:38:29 -07001364 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001365 commit_list=args,
1366 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001367 if found_error:
1368 return 1
1369 return 0
1370
1371
1372def _test():
1373 """Run any built-in tests."""
1374 import doctest
1375 doctest.testmod()
1376
1377
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001378if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001379 if sys.argv[1:2] == ["--test"]:
1380 _test()
1381 exit_code = 0
1382 else:
1383 prog_name = os.path.basename(sys.argv[0])
1384 try:
1385 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001386 except BadInvocation, err:
1387 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001388 exit_code = 1
1389 sys.exit(exit_code)