blob: 07ca533ba270fe796f44d02c426026a3c1ae8a83 [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
Randall Spangler7318fd62013-11-21 12:16:58 -0800505def _run_checkpatch_ec(project, commit):
506 """Runs checkpatch with options for Chromium EC projects."""
507 return _run_checkpatch(project, commit, ['--no-tree',
508 '--ignore=MSLEEP,VOLATILE'])
509
Olof Johanssona96810f2012-09-04 16:20:03 -0700510def _kernel_configcheck(project, commit):
511 """Makes sure kernel config changes are not mixed with code changes"""
512 files = _get_affected_files(commit)
513 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
514 return HookFailure('Changes to chromeos/config/ and regular files must '
515 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700516
Dale Curtis2975c432011-05-03 17:25:20 -0700517def _run_json_check(project, commit):
518 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700519 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700520 try:
521 json.load(open(f))
522 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700523 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700524
525
Mike Frysinger52b537e2013-08-22 22:59:53 -0400526def _check_manifests(project, commit):
527 """Make sure Manifest files only have DIST lines"""
528 paths = []
529
530 for path in _get_affected_files(commit):
531 if os.path.basename(path) != 'Manifest':
532 continue
533 if not os.path.exists(path):
534 continue
535
536 with open(path, 'r') as f:
537 for line in f.readlines():
538 if not line.startswith('DIST '):
539 paths.append(path)
540 break
541
542 if paths:
543 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
544 ('\n'.join(paths),))
545
546
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700547def _check_change_has_branch_field(project, commit):
548 """Check for a non-empty 'BRANCH=' field in the commit message."""
549 BRANCH_RE = r'\nBRANCH=\S+'
550
551 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
552 msg = ('Changelist description needs BRANCH field (after first line)\n'
553 'E.g. BRANCH=none or BRANCH=link,snow')
554 return HookFailure(msg)
555
556
Jon Salz3ee59de2012-08-18 13:54:22 +0800557def _run_project_hook_script(script, project, commit):
558 """Runs a project hook script.
559
560 The script is run with the following environment variables set:
561 PRESUBMIT_PROJECT: The affected project
562 PRESUBMIT_COMMIT: The affected commit
563 PRESUBMIT_FILES: A newline-separated list of affected files
564
565 The script is considered to fail if the exit code is non-zero. It should
566 write an error message to stdout.
567 """
568 env = dict(os.environ)
569 env['PRESUBMIT_PROJECT'] = project
570 env['PRESUBMIT_COMMIT'] = commit
571
572 # Put affected files in an environment variable
573 files = _get_affected_files(commit)
574 env['PRESUBMIT_FILES'] = '\n'.join(files)
575
576 process = subprocess.Popen(script, env=env, shell=True,
577 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800578 stdout=subprocess.PIPE,
579 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800580 stdout, _ = process.communicate()
581 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800582 if stdout:
583 stdout = re.sub('(?m)^', ' ', stdout)
584 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800585 (script, process.returncode,
586 ':\n' + stdout if stdout else ''))
587
588
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700589# Base
590
Ryan Cui1562fb82011-05-09 11:01:31 -0700591
Ryan Cui9b651632011-05-11 11:38:58 -0700592# A list of hooks that are not project-specific
593_COMMON_HOOKS = [
594 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700595 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700596 _check_change_has_test_field,
597 _check_change_has_proper_changeid,
598 _check_no_stray_whitespace,
599 _check_no_long_lines,
600 _check_license,
601 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700602 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700603]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700604
Ryan Cui1562fb82011-05-09 11:01:31 -0700605
Ryan Cui9b651632011-05-11 11:38:58 -0700606# A dictionary of project-specific hooks(callbacks), indexed by project name.
607# dict[project] = [callback1, callback2]
608_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400609 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400610 "chromeos/overlays/chromeos-overlay": [_check_manifests],
611 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800612 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700613 _check_change_has_branch_field],
David Hendricks33f77d52013-02-04 17:53:02 -0800614 "chromeos/third_party/coreboot": [_check_change_has_branch_field,
615 _check_google_copyright],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700616 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400617 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
618 _kernel_configcheck],
619 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400620 "chromiumos/overlays/board-overlays": [_check_manifests],
621 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
622 "chromiumos/overlays/portage-stable": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800623 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400624 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700625 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400626 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700627 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400628 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
629 "chromiumos/third_party/kernel-next": [_run_checkpatch,
630 _kernel_configcheck],
631 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
632 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700633}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700634
Ryan Cui1562fb82011-05-09 11:01:31 -0700635
Ryan Cui9b651632011-05-11 11:38:58 -0700636# A dictionary of flags (keys) that can appear in the config file, and the hook
637# that the flag disables (value)
638_DISABLE_FLAGS = {
639 'stray_whitespace_check': _check_no_stray_whitespace,
640 'long_line_check': _check_no_long_lines,
641 'cros_license_check': _check_license,
642 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700643 'branch_check': _check_change_has_branch_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700644}
645
646
Jon Salz3ee59de2012-08-18 13:54:22 +0800647def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700648 """Returns a set of hooks disabled by the current project's config file.
649
650 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800651
652 Args:
653 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700654 """
655 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800656 if not config.has_section(SECTION):
657 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700658
659 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800660 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700661 try:
662 if not config.getboolean(SECTION, flag): disable_flags.append(flag)
663 except ValueError as e:
664 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400665 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700666
667 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
668 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
669
670
Jon Salz3ee59de2012-08-18 13:54:22 +0800671def _get_project_hook_scripts(config):
672 """Returns a list of project-specific hook scripts.
673
674 Args:
675 config: A ConfigParser for the project's config file.
676 """
677 SECTION = 'Hook Scripts'
678 if not config.has_section(SECTION):
679 return []
680
681 hook_names_values = config.items(SECTION)
682 hook_names_values.sort(key=lambda x: x[0])
683 return [x[1] for x in hook_names_values]
684
685
Ryan Cui9b651632011-05-11 11:38:58 -0700686def _get_project_hooks(project):
687 """Returns a list of hooks that need to be run for a project.
688
689 Expects to be called from within the project root.
690 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800691 config = ConfigParser.RawConfigParser()
692 try:
693 config.read(_CONFIG_FILE)
694 except ConfigParser.Error:
695 # Just use an empty config file
696 config = ConfigParser.RawConfigParser()
697
698 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700699 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
700
701 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700702 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
703 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700704
Jon Salz3ee59de2012-08-18 13:54:22 +0800705 for script in _get_project_hook_scripts(config):
706 hooks.append(functools.partial(_run_project_hook_script, script))
707
Ryan Cui9b651632011-05-11 11:38:58 -0700708 return hooks
709
710
Doug Anderson14749562013-06-26 13:38:29 -0700711def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700712 """For each project run its project specific hook from the hooks dictionary.
713
714 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700715 project: The name of project to run hooks for.
716 proj_dir: If non-None, this is the directory the project is in. If None,
717 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700718 commit_list: A list of commits to run hooks against. If None or empty list
719 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700720
721 Returns:
722 Boolean value of whether any errors were ecountered while running the hooks.
723 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700724 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700725 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
726 if len(proj_dirs) == 0:
727 print('%s cannot be found.' % project, file=sys.stderr)
728 print('Please specify a valid project.', file=sys.stderr)
729 return True
730 if len(proj_dirs) > 1:
731 print('%s is associated with multiple directories.' % project,
732 file=sys.stderr)
733 print('Please specify a directory to help disambiguate.', file=sys.stderr)
734 return True
735 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700736
Ryan Cuiec4d6332011-05-02 14:15:25 -0700737 pwd = os.getcwd()
738 # hooks assume they are run from the root of the project
739 os.chdir(proj_dir)
740
Doug Anderson14749562013-06-26 13:38:29 -0700741 if not commit_list:
742 try:
743 commit_list = _get_commits()
744 except VerifyException as e:
745 PrintErrorForProject(project, HookFailure(str(e)))
746 os.chdir(pwd)
747 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700748
Ryan Cui9b651632011-05-11 11:38:58 -0700749 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -0700750 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700751 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700752 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -0700753 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -0700754 hook_error = hook(project, commit)
755 if hook_error:
756 error_list.append(hook_error)
757 error_found = True
758 if error_list:
759 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
760 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700761
Ryan Cuiec4d6332011-05-02 14:15:25 -0700762 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -0700763 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700764
765# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700766
Ryan Cui1562fb82011-05-09 11:01:31 -0700767
David James2edd9002013-10-11 14:09:19 -0700768def main(project_list, worktree_list=None, **kwargs):
Doug Anderson06456632012-01-05 11:02:14 -0800769 """Main function invoked directly by repo.
770
771 This function will exit directly upon error so that repo doesn't print some
772 obscure error message.
773
774 Args:
775 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -0700776 worktree_list: A list of directories. It should be the same length as
777 project_list, so that each entry in project_list matches with a directory
778 in worktree_list. If None, we will attempt to calculate the directories
779 automatically.
Doug Anderson06456632012-01-05 11:02:14 -0800780 kwargs: Leave this here for forward-compatibility.
781 """
Ryan Cui1562fb82011-05-09 11:01:31 -0700782 found_error = False
David James2edd9002013-10-11 14:09:19 -0700783 if not worktree_list:
784 worktree_list = [None] * len(project_list)
785 for project, worktree in zip(project_list, worktree_list):
786 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -0700787 found_error = True
788
789 if (found_error):
790 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -0700791 '- To disable some source style checks, and for other hints, see '
792 '<checkout_dir>/src/repohooks/README\n'
793 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400794 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -0700795 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -0700796
Ryan Cui1562fb82011-05-09 11:01:31 -0700797
Doug Anderson44a644f2011-11-02 10:37:37 -0700798def _identify_project(path):
799 """Identify the repo project associated with the given path.
800
801 Returns:
802 A string indicating what project is associated with the path passed in or
803 a blank string upon failure.
804 """
805 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
806 stderr=subprocess.PIPE, cwd=path).strip()
807
808
809def direct_main(args, verbose=False):
810 """Run hooks directly (outside of the context of repo).
811
812 # Setup for doctests below.
813 # ...note that some tests assume that running pre-upload on this CWD is fine.
814 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
815 >>> mydir = os.path.dirname(os.path.abspath(__file__))
816 >>> olddir = os.getcwd()
817
818 # OK to run w/ no arugments; will run with CWD.
819 >>> os.chdir(mydir)
820 >>> direct_main(['prog_name'], verbose=True)
821 Running hooks on chromiumos/repohooks
822 0
823 >>> os.chdir(olddir)
824
825 # Run specifying a dir
826 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
827 Running hooks on chromiumos/repohooks
828 0
829
830 # Not a problem to use a bogus project; we'll just get default settings.
831 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
832 Running hooks on X
833 0
834
835 # Run with project but no dir
836 >>> os.chdir(mydir)
837 >>> direct_main(['prog_name', '--project=X'], verbose=True)
838 Running hooks on X
839 0
840 >>> os.chdir(olddir)
841
842 # Try with a non-git CWD
843 >>> os.chdir('/tmp')
844 >>> direct_main(['prog_name'])
845 Traceback (most recent call last):
846 ...
847 BadInvocation: The current directory is not part of a git project.
848
849 # Check various bad arguments...
850 >>> direct_main(['prog_name', 'bogus'])
851 Traceback (most recent call last):
852 ...
853 BadInvocation: Unexpected arguments: bogus
854 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
855 Traceback (most recent call last):
856 ...
857 BadInvocation: Invalid dir: bogusdir
858 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
859 Traceback (most recent call last):
860 ...
861 BadInvocation: Not a git directory: /tmp
862
863 Args:
864 args: The value of sys.argv
865
866 Returns:
867 0 if no pre-upload failures, 1 if failures.
868
869 Raises:
870 BadInvocation: On some types of invocation errors.
871 """
872 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
873 parser = optparse.OptionParser(description=desc)
874
875 parser.add_option('--dir', default=None,
876 help='The directory that the project lives in. If not '
877 'specified, use the git project root based on the cwd.')
878 parser.add_option('--project', default=None,
879 help='The project repo path; this can affect how the hooks '
880 'get run, since some hooks are project-specific. For '
881 'chromite this is chromiumos/chromite. If not specified, '
882 'the repo tool will be used to figure this out based on '
883 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -0700884 parser.add_option('--rerun-since', default=None,
885 help='Rerun hooks on old commits since the given date. '
886 'The date should match git log\'s concept of a date. '
887 'e.g. 2012-06-20')
888
889 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -0700890
891 opts, args = parser.parse_args(args[1:])
892
Doug Anderson14749562013-06-26 13:38:29 -0700893 if opts.rerun_since:
894 if args:
895 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
896 ' '.join(args))
897
898 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
899 all_commits = _run_command(cmd).splitlines()
900 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
901
902 # Eliminate chrome-bot commits but keep ordering the same...
903 bot_commits = set(bot_commits)
904 args = [c for c in all_commits if c not in bot_commits]
905
Doug Anderson44a644f2011-11-02 10:37:37 -0700906
907 # Check/normlaize git dir; if unspecified, we'll use the root of the git
908 # project from CWD
909 if opts.dir is None:
910 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
911 stderr=subprocess.PIPE).strip()
912 if not git_dir:
913 raise BadInvocation('The current directory is not part of a git project.')
914 opts.dir = os.path.dirname(os.path.abspath(git_dir))
915 elif not os.path.isdir(opts.dir):
916 raise BadInvocation('Invalid dir: %s' % opts.dir)
917 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
918 raise BadInvocation('Not a git directory: %s' % opts.dir)
919
920 # Identify the project if it wasn't specified; this _requires_ the repo
921 # tool to be installed and for the project to be part of a repo checkout.
922 if not opts.project:
923 opts.project = _identify_project(opts.dir)
924 if not opts.project:
925 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
926
927 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400928 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -0700929
Doug Anderson14749562013-06-26 13:38:29 -0700930 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
931 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -0700932 if found_error:
933 return 1
934 return 0
935
936
937def _test():
938 """Run any built-in tests."""
939 import doctest
940 doctest.testmod()
941
942
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700943if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -0700944 if sys.argv[1:2] == ["--test"]:
945 _test()
946 exit_code = 0
947 else:
948 prog_name = os.path.basename(sys.argv[0])
949 try:
950 exit_code = direct_main(sys.argv)
951 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400952 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -0700953 exit_code = 1
954 sys.exit(exit_code)