blob: 66849e5bc6fc7fd860cc83df3595007ed149d33b [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 Frysinger09d6a3d2013-10-08 22:21:03 -04006from __future__ import print_function
7
Ryan Cui9b651632011-05-11 11:38:58 -07008import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +08009import functools
Dale Curtis2975c432011-05-03 17:25:20 -070010import json
Doug Anderson44a644f2011-11-02 10:37:37 -070011import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070012import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070013import re
David Hendricks4c018e72013-02-06 13:46:38 -080014import socket
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070015import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070016import subprocess
17
Ryan Cui1562fb82011-05-09 11:01:31 -070018from errors import (VerifyException, HookFailure, PrintErrorForProject,
19 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070020
David Jamesc3b68b32013-04-03 09:17:03 -070021# If repo imports us, the __name__ will be __builtin__, and the wrapper will
22# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
23# up. The same logic also happens to work if we're executed directly.
24if __name__ in ('__builtin__', '__main__'):
25 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
26
27from chromite.lib import patch
28
Ryan Cuiec4d6332011-05-02 14:15:25 -070029
30COMMON_INCLUDED_PATHS = [
31 # C++ and friends
32 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
33 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
34 # Scripts
35 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
36 # No extension at all, note that ALL CAPS files are black listed in
37 # COMMON_EXCLUDED_LIST below.
David Hendricks0af30eb2013-02-05 11:35:56 -080038 r"(^|.*[\\\/])[^.]+$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070039 # Other
40 r".*\.java$", r".*\.mk$", r".*\.am$",
41]
42
Ryan Cui1562fb82011-05-09 11:01:31 -070043
Ryan Cuiec4d6332011-05-02 14:15:25 -070044COMMON_EXCLUDED_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -070045 # avoid doing source file checks for kernel
46 r"/src/third_party/kernel/",
47 r"/src/third_party/kernel-next/",
Paul Taysomf8b6e012011-05-09 14:32:42 -070048 r"/src/third_party/ktop/",
49 r"/src/third_party/punybench/",
Ryan Cuiec4d6332011-05-02 14:15:25 -070050 r".*\bexperimental[\\\/].*",
51 r".*\b[A-Z0-9_]{2,}$",
52 r".*[\\\/]debian[\\\/]rules$",
Brian Harringd780d602011-10-18 16:48:08 -070053 # for ebuild trees, ignore any caches and manifest data
54 r".*/Manifest$",
55 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070056
57 # ignore profiles data (like overlay-tegra2/profiles)
58 r".*/overlay-.*/profiles/.*",
Andrew de los Reyes0e679922012-05-02 11:42:54 -070059 # ignore minified js and jquery
60 r".*\.min\.js",
61 r".*jquery.*\.js",
Ryan Cuiec4d6332011-05-02 14:15:25 -070062]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070063
Ryan Cui1562fb82011-05-09 11:01:31 -070064
Ryan Cui9b651632011-05-11 11:38:58 -070065_CONFIG_FILE = 'PRESUBMIT.cfg'
66
67
Doug Anderson44a644f2011-11-02 10:37:37 -070068# Exceptions
69
70
71class BadInvocation(Exception):
72 """An Exception indicating a bad invocation of the program."""
73 pass
74
75
Ryan Cui1562fb82011-05-09 11:01:31 -070076# General Helpers
77
Sean Paulba01d402011-05-05 11:36:23 -040078
Doug Anderson44a644f2011-11-02 10:37:37 -070079def _run_command(cmd, cwd=None, stderr=None):
80 """Executes the passed in command and returns raw stdout output.
81
82 Args:
83 cmd: The command to run; should be a list of strings.
84 cwd: The directory to switch to for running the command.
85 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
86 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
87
88 Returns:
89 The standard out from the process.
90 """
91 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
92 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -070093
Ryan Cui1562fb82011-05-09 11:01:31 -070094
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070095def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -070096 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -070097 if __name__ == '__main__':
98 # Works when file is run on its own (__file__ is defined)...
99 return os.path.abspath(os.path.dirname(__file__))
100 else:
101 # We need to do this when we're run through repo. Since repo executes
102 # us with execfile(), we don't get __file__ defined.
103 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
104 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700105
Ryan Cui1562fb82011-05-09 11:01:31 -0700106
Ryan Cuiec4d6332011-05-02 14:15:25 -0700107def _match_regex_list(subject, expressions):
108 """Try to match a list of regular expressions to a string.
109
110 Args:
111 subject: The string to match regexes on
112 expressions: A list of regular expressions to check for matches with.
113
114 Returns:
115 Whether the passed in subject matches any of the passed in regexes.
116 """
117 for expr in expressions:
118 if (re.search(expr, subject)):
119 return True
120 return False
121
Ryan Cui1562fb82011-05-09 11:01:31 -0700122
Ryan Cuiec4d6332011-05-02 14:15:25 -0700123def _filter_files(files, include_list, exclude_list=[]):
124 """Filter out files based on the conditions passed in.
125
126 Args:
127 files: list of filepaths to filter
128 include_list: list of regex that when matched with a file path will cause it
129 to be added to the output list unless the file is also matched with a
130 regex in the exclude_list.
131 exclude_list: list of regex that when matched with a file will prevent it
132 from being added to the output list, even if it is also matched with a
133 regex in the include_list.
134
135 Returns:
136 A list of filepaths that contain files matched in the include_list and not
137 in the exclude_list.
138 """
139 filtered = []
140 for f in files:
141 if (_match_regex_list(f, include_list) and
142 not _match_regex_list(f, exclude_list)):
143 filtered.append(f)
144 return filtered
145
Ryan Cuiec4d6332011-05-02 14:15:25 -0700146
David Hendricks35030d02013-02-04 17:49:16 -0800147def _verify_header_content(commit, content, fail_msg):
148 """Verify that file headers contain specified content.
149
150 Args:
151 commit: the affected commit.
152 content: the content of the header to be verified.
153 fail_msg: the first message to display in case of failure.
154
155 Returns:
156 The return value of HookFailure().
157 """
158 license_re = re.compile(content, re.MULTILINE)
159 bad_files = []
160 files = _filter_files(_get_affected_files(commit),
161 COMMON_INCLUDED_PATHS,
162 COMMON_EXCLUDED_PATHS)
163
164 for f in files:
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800165 if os.path.exists(f): # Ignore non-existant files
166 contents = open(f).read()
167 if len(contents) == 0: continue # Ignore empty files
168 if not license_re.search(contents):
169 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800170 if bad_files:
171 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
172 "Found a bad header in these files:")
173 return HookFailure(msg, bad_files)
174
175
Ryan Cuiec4d6332011-05-02 14:15:25 -0700176# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700177
178
Ryan Cui4725d952011-05-05 15:41:19 -0700179def _get_upstream_branch():
180 """Returns the upstream tracking branch of the current branch.
181
182 Raises:
183 Error if there is no tracking branch
184 """
185 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
186 current_branch = current_branch.replace('refs/heads/', '')
187 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700188 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700189
190 cfg_option = 'branch.' + current_branch + '.%s'
191 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
192 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
193 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700194 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700195
196 return full_upstream.replace('heads', 'remotes/' + remote)
197
Ryan Cui1562fb82011-05-09 11:01:31 -0700198
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700199def _get_patch(commit):
200 """Returns the patch for this commit."""
201 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700202
Ryan Cui1562fb82011-05-09 11:01:31 -0700203
Jon Salz98255932012-08-18 14:48:02 +0800204def _try_utf8_decode(data):
205 """Attempts to decode a string as UTF-8.
206
207 Returns:
208 The decoded Unicode object, or the original string if parsing fails.
209 """
210 try:
211 return unicode(data, 'utf-8', 'strict')
212 except UnicodeDecodeError:
213 return data
214
215
Ryan Cuiec4d6332011-05-02 14:15:25 -0700216def _get_file_diff(file, commit):
217 """Returns a list of (linenum, lines) tuples that the commit touched."""
Ryan Cui72834d12011-05-05 14:51:33 -0700218 output = _run_command(['git', 'show', '-p', '--no-ext-diff', commit, file])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700219
220 new_lines = []
221 line_num = 0
222 for line in output.splitlines():
223 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
224 if m:
225 line_num = int(m.groups(1)[0])
226 continue
227 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800228 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700229 if not line.startswith('-'):
230 line_num += 1
231 return new_lines
232
Ryan Cui1562fb82011-05-09 11:01:31 -0700233
Doug Anderson42b8a052013-06-26 10:45:36 -0700234def _get_affected_files(commit, include_deletes=False):
235 """Returns list of absolute filepaths that were modified/added.
236
237 Args:
238 commit: The commit
239 include_deletes: If true we'll include delete in the list.
240
241 Returns:
242 A list of modified/added (and perhaps deleted) files
243 """
Ryan Cui72834d12011-05-05 14:51:33 -0700244 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700245 files = []
246 for statusline in output.splitlines():
247 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
248 # Ignore deleted files, and return absolute paths of files
Doug Anderson42b8a052013-06-26 10:45:36 -0700249 if (include_deletes or m.group(1)[0] != 'D'):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700250 pwd = os.getcwd()
251 files.append(os.path.join(pwd, m.group(2)))
252 return files
253
Ryan Cui1562fb82011-05-09 11:01:31 -0700254
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700255def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700256 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700257 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700258 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700259
Ryan Cui1562fb82011-05-09 11:01:31 -0700260
Ryan Cuiec4d6332011-05-02 14:15:25 -0700261def _get_commit_desc(commit):
262 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400263 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700264
265
266# Common Hooks
267
Ryan Cui1562fb82011-05-09 11:01:31 -0700268
Ryan Cuiec4d6332011-05-02 14:15:25 -0700269def _check_no_long_lines(project, commit):
270 """Checks that there aren't any lines longer than maxlen characters in any of
271 the text files to be submitted.
272 """
273 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800274 SKIP_REGEXP = re.compile('|'.join([
275 r'https?://',
276 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700277
278 errors = []
279 files = _filter_files(_get_affected_files(commit),
280 COMMON_INCLUDED_PATHS,
281 COMMON_EXCLUDED_PATHS)
282
283 for afile in files:
284 for line_num, line in _get_file_diff(afile, commit):
285 # Allow certain lines to exceed the maxlen rule.
Jon Salz98255932012-08-18 14:48:02 +0800286 if (len(line) <= MAX_LEN or SKIP_REGEXP.search(line)):
287 continue
288
289 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
290 if len(errors) == 5: # Just show the first 5 errors.
291 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700292
293 if errors:
294 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700295 return HookFailure(msg, errors)
296
Ryan Cuiec4d6332011-05-02 14:15:25 -0700297
298def _check_no_stray_whitespace(project, commit):
299 """Checks that there is no stray whitespace at source lines end."""
300 errors = []
301 files = _filter_files(_get_affected_files(commit),
302 COMMON_INCLUDED_PATHS,
303 COMMON_EXCLUDED_PATHS)
304
305 for afile in files:
306 for line_num, line in _get_file_diff(afile, commit):
307 if line.rstrip() != line:
308 errors.append('%s, line %s' % (afile, line_num))
309 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700310 return HookFailure('Found line ending with white space in:', errors)
311
Ryan Cuiec4d6332011-05-02 14:15:25 -0700312
313def _check_no_tabs(project, commit):
314 """Checks there are no unexpanded tabs."""
315 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700316 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700317 r".*\.ebuild$",
318 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500319 r".*/[M|m]akefile$",
320 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700321 ]
322
323 errors = []
324 files = _filter_files(_get_affected_files(commit),
325 COMMON_INCLUDED_PATHS,
326 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
327
328 for afile in files:
329 for line_num, line in _get_file_diff(afile, commit):
330 if '\t' in line:
331 errors.append('%s, line %s' % (afile, line_num))
332 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700333 return HookFailure('Found a tab character in:', errors)
334
Ryan Cuiec4d6332011-05-02 14:15:25 -0700335
336def _check_change_has_test_field(project, commit):
337 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700338 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700339
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700340 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700341 msg = 'Changelist description needs TEST field (after first line)'
342 return HookFailure(msg)
343
Ryan Cuiec4d6332011-05-02 14:15:25 -0700344
David Jamesc3b68b32013-04-03 09:17:03 -0700345def _check_change_has_valid_cq_depend(project, commit):
346 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
347 msg = 'Changelist has invalid CQ-DEPEND target.'
348 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
349 try:
350 patch.GetPaladinDeps(_get_commit_desc(commit))
351 except ValueError as ex:
352 return HookFailure(msg, [example, str(ex)])
353
354
Ryan Cuiec4d6332011-05-02 14:15:25 -0700355def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700356 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700357 OLD_BUG_RE = r'\nBUG=.*chromium-os'
358 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
359 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
360 'the chromium tracker in your BUG= line now.')
361 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700362
David James5c0073d2013-04-03 08:48:52 -0700363 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700364 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700365 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700366 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700367 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
368 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700369 return HookFailure(msg)
370
Ryan Cuiec4d6332011-05-02 14:15:25 -0700371
Doug Anderson42b8a052013-06-26 10:45:36 -0700372def _check_for_uprev(project, commit):
373 """Check that we're not missing a revbump of an ebuild in the given commit.
374
375 If the given commit touches files in a directory that has ebuilds somewhere
376 up the directory hierarchy, it's very likely that we need an ebuild revbump
377 in order for those changes to take effect.
378
379 It's not totally trivial to detect a revbump, so at least detect that an
380 ebuild with a revision number in it was touched. This should handle the
381 common case where we use a symlink to do the revbump.
382
383 TODO: it would be nice to enhance this hook to:
384 * Handle cases where people revbump with a slightly different syntax. I see
385 one ebuild (puppy) that revbumps with _pN. This is a false positive.
386 * Catches cases where people aren't using symlinks for revbumps. If they
387 edit a revisioned file directly (and are expected to rename it for revbump)
388 we'll miss that. Perhaps we could detect that the file touched is a
389 symlink?
390
391 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
392 still better off than without this check.
393
394 Args:
395 project: The project to look at
396 commit: The commit to look at
397
398 Returns:
399 A HookFailure or None.
400 """
401 affected_paths = _get_affected_files(commit, include_deletes=True)
402
403 # Don't yell about changes to whitelisted files...
404 whitelist = ['Manifest', 'metadata.xml']
405 affected_paths = [path for path in affected_paths
406 if os.path.basename(path) not in whitelist]
407 if not affected_paths:
408 return None
409
410 # If we've touched any file named with a -rN.ebuild then we'll say we're
411 # OK right away. See TODO above about enhancing this.
412 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
413 for path in affected_paths)
414 if touched_revved_ebuild:
415 return None
416
417 # We want to examine the current contents of all directories that are parents
418 # of files that were touched (up to the top of the project).
419 #
420 # ...note: we use the current directory contents even though it may have
421 # changed since the commit we're looking at. This is just a heuristic after
422 # all. Worst case we don't flag a missing revbump.
423 project_top = os.getcwd()
424 dirs_to_check = set([project_top])
425 for path in affected_paths:
426 path = os.path.dirname(path)
427 while os.path.exists(path) and not os.path.samefile(path, project_top):
428 dirs_to_check.add(path)
429 path = os.path.dirname(path)
430
431 # Look through each directory. If it's got an ebuild in it then we'll
432 # consider this as a case when we need a revbump.
433 for dir_path in dirs_to_check:
434 contents = os.listdir(dir_path)
435 ebuilds = [os.path.join(dir_path, path)
436 for path in contents if path.endswith('.ebuild')]
437 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
438
439 # If the -9999.ebuild file was touched the bot will uprev for us.
440 # ...we'll use a simple intersection here as a heuristic...
441 if set(ebuilds_9999) & set(affected_paths):
442 continue
443
444 if ebuilds:
445 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
446 'or a -r1.ebuild symlink if this is a new ebuild')
447
448 return None
449
450
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700451def _check_change_has_proper_changeid(project, commit):
452 """Verify that Change-ID is present in last paragraph of commit message."""
453 desc = _get_commit_desc(commit)
454 loc = desc.rfind('\nChange-Id:')
455 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700456 return HookFailure('Change-Id must be in last paragraph of description.')
457
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700458
Ryan Cuiec4d6332011-05-02 14:15:25 -0700459def _check_license(project, commit):
460 """Verifies the license header."""
461 LICENSE_HEADER = (
David Hendricks0af30eb2013-02-05 11:35:56 -0800462 r".* Copyright \(c\) 20[-0-9]{2,7} The Chromium OS Authors\. All rights "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700463 r"reserved\." "\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800464 r".* Use of this source code is governed by a BSD-style license that can "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700465 "be\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800466 r".* found in the LICENSE file\."
Ryan Cuiec4d6332011-05-02 14:15:25 -0700467 "\n"
468 )
David Hendricks35030d02013-02-04 17:49:16 -0800469 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700470
David Hendricks35030d02013-02-04 17:49:16 -0800471 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700472
473
David Hendricksa0e310d2013-02-04 17:51:55 -0800474def _check_google_copyright(project, commit):
475 """Verifies Google Inc. as copyright holder."""
476 LICENSE_HEADER = (
477 r".* Copyright 20[-0-9]{2,7} Google Inc\."
478 )
479 FAIL_MSG = "Copyright must match"
480
David Hendricks4c018e72013-02-06 13:46:38 -0800481 # Avoid blocking partners and external contributors.
482 fqdn = socket.getfqdn()
483 if not fqdn.endswith(".corp.google.com"):
484 return None
485
David Hendricksa0e310d2013-02-04 17:51:55 -0800486 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
487
488
Ryan Cuiec4d6332011-05-02 14:15:25 -0700489# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700490
Ryan Cui1562fb82011-05-09 11:01:31 -0700491
Anton Staaf815d6852011-08-22 10:08:45 -0700492def _run_checkpatch(project, commit, options=[]):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700493 """Runs checkpatch.pl on the given project"""
494 hooks_dir = _get_hooks_dir()
Anton Staaf815d6852011-08-22 10:08:45 -0700495 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700496 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700497 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700498 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700499 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700500
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700501
Anton Staaf815d6852011-08-22 10:08:45 -0700502def _run_checkpatch_no_tree(project, commit):
503 return _run_checkpatch(project, commit, ['--no-tree'])
504
Olof Johanssona96810f2012-09-04 16:20:03 -0700505def _kernel_configcheck(project, commit):
506 """Makes sure kernel config changes are not mixed with code changes"""
507 files = _get_affected_files(commit)
508 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
509 return HookFailure('Changes to chromeos/config/ and regular files must '
510 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700511
Dale Curtis2975c432011-05-03 17:25:20 -0700512def _run_json_check(project, commit):
513 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700514 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700515 try:
516 json.load(open(f))
517 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700518 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700519
520
Mike Frysinger52b537e2013-08-22 22:59:53 -0400521def _check_manifests(project, commit):
522 """Make sure Manifest files only have DIST lines"""
523 paths = []
524
525 for path in _get_affected_files(commit):
526 if os.path.basename(path) != 'Manifest':
527 continue
528 if not os.path.exists(path):
529 continue
530
531 with open(path, 'r') as f:
532 for line in f.readlines():
533 if not line.startswith('DIST '):
534 paths.append(path)
535 break
536
537 if paths:
538 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
539 ('\n'.join(paths),))
540
541
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700542def _check_change_has_branch_field(project, commit):
543 """Check for a non-empty 'BRANCH=' field in the commit message."""
544 BRANCH_RE = r'\nBRANCH=\S+'
545
546 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
547 msg = ('Changelist description needs BRANCH field (after first line)\n'
548 'E.g. BRANCH=none or BRANCH=link,snow')
549 return HookFailure(msg)
550
551
Jon Salz3ee59de2012-08-18 13:54:22 +0800552def _run_project_hook_script(script, project, commit):
553 """Runs a project hook script.
554
555 The script is run with the following environment variables set:
556 PRESUBMIT_PROJECT: The affected project
557 PRESUBMIT_COMMIT: The affected commit
558 PRESUBMIT_FILES: A newline-separated list of affected files
559
560 The script is considered to fail if the exit code is non-zero. It should
561 write an error message to stdout.
562 """
563 env = dict(os.environ)
564 env['PRESUBMIT_PROJECT'] = project
565 env['PRESUBMIT_COMMIT'] = commit
566
567 # Put affected files in an environment variable
568 files = _get_affected_files(commit)
569 env['PRESUBMIT_FILES'] = '\n'.join(files)
570
571 process = subprocess.Popen(script, env=env, shell=True,
572 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800573 stdout=subprocess.PIPE,
574 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800575 stdout, _ = process.communicate()
576 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800577 if stdout:
578 stdout = re.sub('(?m)^', ' ', stdout)
579 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800580 (script, process.returncode,
581 ':\n' + stdout if stdout else ''))
582
583
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700584# Base
585
Ryan Cui1562fb82011-05-09 11:01:31 -0700586
Ryan Cui9b651632011-05-11 11:38:58 -0700587# A list of hooks that are not project-specific
588_COMMON_HOOKS = [
589 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700590 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700591 _check_change_has_test_field,
592 _check_change_has_proper_changeid,
593 _check_no_stray_whitespace,
594 _check_no_long_lines,
595 _check_license,
596 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700597 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700598]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700599
Ryan Cui1562fb82011-05-09 11:01:31 -0700600
Ryan Cui9b651632011-05-11 11:38:58 -0700601# A dictionary of project-specific hooks(callbacks), indexed by project name.
602# dict[project] = [callback1, callback2]
603_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400604 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400605 "chromeos/overlays/chromeos-overlay": [_check_manifests],
606 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700607 "chromeos/platform/ec-private": [_run_checkpatch_no_tree,
608 _check_change_has_branch_field],
David Hendricks33f77d52013-02-04 17:53:02 -0800609 "chromeos/third_party/coreboot": [_check_change_has_branch_field,
610 _check_google_copyright],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700611 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400612 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
613 _kernel_configcheck],
614 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400615 "chromiumos/overlays/board-overlays": [_check_manifests],
616 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
617 "chromiumos/overlays/portage-stable": [_check_manifests],
Mike Frysingerdf980702013-08-22 22:25:22 -0400618 "chromiumos/platform/ec": [_run_checkpatch_no_tree,
619 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700620 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400621 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700622 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400623 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
624 "chromiumos/third_party/kernel-next": [_run_checkpatch,
625 _kernel_configcheck],
626 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
627 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700628}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700629
Ryan Cui1562fb82011-05-09 11:01:31 -0700630
Ryan Cui9b651632011-05-11 11:38:58 -0700631# A dictionary of flags (keys) that can appear in the config file, and the hook
632# that the flag disables (value)
633_DISABLE_FLAGS = {
634 'stray_whitespace_check': _check_no_stray_whitespace,
635 'long_line_check': _check_no_long_lines,
636 'cros_license_check': _check_license,
637 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700638 'branch_check': _check_change_has_branch_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700639}
640
641
Jon Salz3ee59de2012-08-18 13:54:22 +0800642def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700643 """Returns a set of hooks disabled by the current project's config file.
644
645 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800646
647 Args:
648 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700649 """
650 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800651 if not config.has_section(SECTION):
652 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700653
654 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800655 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700656 try:
657 if not config.getboolean(SECTION, flag): disable_flags.append(flag)
658 except ValueError as e:
659 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400660 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700661
662 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
663 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
664
665
Jon Salz3ee59de2012-08-18 13:54:22 +0800666def _get_project_hook_scripts(config):
667 """Returns a list of project-specific hook scripts.
668
669 Args:
670 config: A ConfigParser for the project's config file.
671 """
672 SECTION = 'Hook Scripts'
673 if not config.has_section(SECTION):
674 return []
675
676 hook_names_values = config.items(SECTION)
677 hook_names_values.sort(key=lambda x: x[0])
678 return [x[1] for x in hook_names_values]
679
680
Ryan Cui9b651632011-05-11 11:38:58 -0700681def _get_project_hooks(project):
682 """Returns a list of hooks that need to be run for a project.
683
684 Expects to be called from within the project root.
685 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800686 config = ConfigParser.RawConfigParser()
687 try:
688 config.read(_CONFIG_FILE)
689 except ConfigParser.Error:
690 # Just use an empty config file
691 config = ConfigParser.RawConfigParser()
692
693 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700694 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
695
696 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700697 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
698 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700699
Jon Salz3ee59de2012-08-18 13:54:22 +0800700 for script in _get_project_hook_scripts(config):
701 hooks.append(functools.partial(_run_project_hook_script, script))
702
Ryan Cui9b651632011-05-11 11:38:58 -0700703 return hooks
704
705
Doug Anderson14749562013-06-26 13:38:29 -0700706def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700707 """For each project run its project specific hook from the hooks dictionary.
708
709 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700710 project: The name of project to run hooks for.
711 proj_dir: If non-None, this is the directory the project is in. If None,
712 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700713 commit_list: A list of commits to run hooks against. If None or empty list
714 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700715
716 Returns:
717 Boolean value of whether any errors were ecountered while running the hooks.
718 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700719 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700720 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
721 if len(proj_dirs) == 0:
722 print('%s cannot be found.' % project, file=sys.stderr)
723 print('Please specify a valid project.', file=sys.stderr)
724 return True
725 if len(proj_dirs) > 1:
726 print('%s is associated with multiple directories.' % project,
727 file=sys.stderr)
728 print('Please specify a directory to help disambiguate.', file=sys.stderr)
729 return True
730 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700731
Ryan Cuiec4d6332011-05-02 14:15:25 -0700732 pwd = os.getcwd()
733 # hooks assume they are run from the root of the project
734 os.chdir(proj_dir)
735
Doug Anderson14749562013-06-26 13:38:29 -0700736 if not commit_list:
737 try:
738 commit_list = _get_commits()
739 except VerifyException as e:
740 PrintErrorForProject(project, HookFailure(str(e)))
741 os.chdir(pwd)
742 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700743
Ryan Cui9b651632011-05-11 11:38:58 -0700744 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -0700745 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700746 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700747 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -0700748 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -0700749 hook_error = hook(project, commit)
750 if hook_error:
751 error_list.append(hook_error)
752 error_found = True
753 if error_list:
754 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
755 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700756
Ryan Cuiec4d6332011-05-02 14:15:25 -0700757 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -0700758 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700759
760# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700761
Ryan Cui1562fb82011-05-09 11:01:31 -0700762
David James2edd9002013-10-11 14:09:19 -0700763def main(project_list, worktree_list=None, **kwargs):
Doug Anderson06456632012-01-05 11:02:14 -0800764 """Main function invoked directly by repo.
765
766 This function will exit directly upon error so that repo doesn't print some
767 obscure error message.
768
769 Args:
770 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -0700771 worktree_list: A list of directories. It should be the same length as
772 project_list, so that each entry in project_list matches with a directory
773 in worktree_list. If None, we will attempt to calculate the directories
774 automatically.
Doug Anderson06456632012-01-05 11:02:14 -0800775 kwargs: Leave this here for forward-compatibility.
776 """
Ryan Cui1562fb82011-05-09 11:01:31 -0700777 found_error = False
David James2edd9002013-10-11 14:09:19 -0700778 if not worktree_list:
779 worktree_list = [None] * len(project_list)
780 for project, worktree in zip(project_list, worktree_list):
781 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -0700782 found_error = True
783
784 if (found_error):
785 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -0700786 '- To disable some source style checks, and for other hints, see '
787 '<checkout_dir>/src/repohooks/README\n'
788 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400789 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -0700790 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -0700791
Ryan Cui1562fb82011-05-09 11:01:31 -0700792
Doug Anderson44a644f2011-11-02 10:37:37 -0700793def _identify_project(path):
794 """Identify the repo project associated with the given path.
795
796 Returns:
797 A string indicating what project is associated with the path passed in or
798 a blank string upon failure.
799 """
800 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
801 stderr=subprocess.PIPE, cwd=path).strip()
802
803
804def direct_main(args, verbose=False):
805 """Run hooks directly (outside of the context of repo).
806
807 # Setup for doctests below.
808 # ...note that some tests assume that running pre-upload on this CWD is fine.
809 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
810 >>> mydir = os.path.dirname(os.path.abspath(__file__))
811 >>> olddir = os.getcwd()
812
813 # OK to run w/ no arugments; will run with CWD.
814 >>> os.chdir(mydir)
815 >>> direct_main(['prog_name'], verbose=True)
816 Running hooks on chromiumos/repohooks
817 0
818 >>> os.chdir(olddir)
819
820 # Run specifying a dir
821 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
822 Running hooks on chromiumos/repohooks
823 0
824
825 # Not a problem to use a bogus project; we'll just get default settings.
826 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
827 Running hooks on X
828 0
829
830 # Run with project but no dir
831 >>> os.chdir(mydir)
832 >>> direct_main(['prog_name', '--project=X'], verbose=True)
833 Running hooks on X
834 0
835 >>> os.chdir(olddir)
836
837 # Try with a non-git CWD
838 >>> os.chdir('/tmp')
839 >>> direct_main(['prog_name'])
840 Traceback (most recent call last):
841 ...
842 BadInvocation: The current directory is not part of a git project.
843
844 # Check various bad arguments...
845 >>> direct_main(['prog_name', 'bogus'])
846 Traceback (most recent call last):
847 ...
848 BadInvocation: Unexpected arguments: bogus
849 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
850 Traceback (most recent call last):
851 ...
852 BadInvocation: Invalid dir: bogusdir
853 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
854 Traceback (most recent call last):
855 ...
856 BadInvocation: Not a git directory: /tmp
857
858 Args:
859 args: The value of sys.argv
860
861 Returns:
862 0 if no pre-upload failures, 1 if failures.
863
864 Raises:
865 BadInvocation: On some types of invocation errors.
866 """
867 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
868 parser = optparse.OptionParser(description=desc)
869
870 parser.add_option('--dir', default=None,
871 help='The directory that the project lives in. If not '
872 'specified, use the git project root based on the cwd.')
873 parser.add_option('--project', default=None,
874 help='The project repo path; this can affect how the hooks '
875 'get run, since some hooks are project-specific. For '
876 'chromite this is chromiumos/chromite. If not specified, '
877 'the repo tool will be used to figure this out based on '
878 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -0700879 parser.add_option('--rerun-since', default=None,
880 help='Rerun hooks on old commits since the given date. '
881 'The date should match git log\'s concept of a date. '
882 'e.g. 2012-06-20')
883
884 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -0700885
886 opts, args = parser.parse_args(args[1:])
887
Doug Anderson14749562013-06-26 13:38:29 -0700888 if opts.rerun_since:
889 if args:
890 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
891 ' '.join(args))
892
893 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
894 all_commits = _run_command(cmd).splitlines()
895 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
896
897 # Eliminate chrome-bot commits but keep ordering the same...
898 bot_commits = set(bot_commits)
899 args = [c for c in all_commits if c not in bot_commits]
900
Doug Anderson44a644f2011-11-02 10:37:37 -0700901
902 # Check/normlaize git dir; if unspecified, we'll use the root of the git
903 # project from CWD
904 if opts.dir is None:
905 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
906 stderr=subprocess.PIPE).strip()
907 if not git_dir:
908 raise BadInvocation('The current directory is not part of a git project.')
909 opts.dir = os.path.dirname(os.path.abspath(git_dir))
910 elif not os.path.isdir(opts.dir):
911 raise BadInvocation('Invalid dir: %s' % opts.dir)
912 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
913 raise BadInvocation('Not a git directory: %s' % opts.dir)
914
915 # Identify the project if it wasn't specified; this _requires_ the repo
916 # tool to be installed and for the project to be part of a repo checkout.
917 if not opts.project:
918 opts.project = _identify_project(opts.dir)
919 if not opts.project:
920 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
921
922 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400923 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -0700924
Doug Anderson14749562013-06-26 13:38:29 -0700925 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
926 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -0700927 if found_error:
928 return 1
929 return 0
930
931
932def _test():
933 """Run any built-in tests."""
934 import doctest
935 doctest.testmod()
936
937
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700938if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -0700939 if sys.argv[1:2] == ["--test"]:
940 _test()
941 exit_code = 0
942 else:
943 prog_name = os.path.basename(sys.argv[0])
944 try:
945 exit_code = direct_main(sys.argv)
946 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400947 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -0700948 exit_code = 1
949 sys.exit(exit_code)