blob: 5393872d1b1121de9a06caf870a3be4ba1918290 [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)
Mike Frysinger94a670c2014-09-19 12:46:26 -040065 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'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400766 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400767 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400768 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400769
Mike Frysinger94a670c2014-09-19 12:46:26 -0400770 # Handle multiple overlays in a single commit (like the public tree).
771 for f in _get_affected_files(commit, relative=True):
772 if f.endswith(repo_name):
773 repo_names.append(f)
774 elif f.endswith(layout_path):
775 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400776
777 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -0400778 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400779 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -0400780 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400781
Mike Frysinger94a670c2014-09-19 12:46:26 -0400782 # Gather all the errors in one pass so we show one full message.
783 all_errors = {}
784 for layout_path in layout_paths:
785 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400786
Mike Frysinger94a670c2014-09-19 12:46:26 -0400787 # Make sure the config file is sorted.
788 data = [x for x in _get_file_content(layout_path, commit).splitlines()
789 if x and x[0] != '#']
790 if sorted(data) != data:
791 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400792
Mike Frysinger94a670c2014-09-19 12:46:26 -0400793 # Require people to set specific values all the time.
794 settings = (
795 # TODO: Enable this for everyone. http://crbug.com/408038
796 #('fast caching', 'cache-format = md5-dict'),
797 ('fast manifests', 'thin-manifests = true'),
798 ('extra features', 'profile-formats = portage-2'),
799 )
800 for reason, line in settings:
801 if line not in data:
802 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400803
Mike Frysinger94a670c2014-09-19 12:46:26 -0400804 # Require one of these settings.
805 if ('use-manifests = true' not in data and
806 'use-manifests = strict' not in data):
807 errors += ['enable file checking with: use-manifests = true']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400808
Mike Frysinger94a670c2014-09-19 12:46:26 -0400809 # Require repo-name to be set.
810 if 'repo-name = ' not in data:
811 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400812
Mike Frysinger94a670c2014-09-19 12:46:26 -0400813 # Summarize all the errors we saw (if any).
814 lines = ''
815 for layout_path, errors in all_errors.items():
816 if errors:
817 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
818 if lines:
819 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
820 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400821
822
Ryan Cuiec4d6332011-05-02 14:15:25 -0700823# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700824
Ryan Cui1562fb82011-05-09 11:01:31 -0700825
Mike Frysingerae409522014-02-01 03:16:11 -0500826def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700827 """Runs checkpatch.pl on the given project"""
828 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700829 options = list(options)
830 if commit == PRE_SUBMIT:
831 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
832 # this case.
833 options.append('--ignore=MISSING_SIGN_OFF')
834 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700835 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700836 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700837 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700838 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700839
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700840
Anton Staaf815d6852011-08-22 10:08:45 -0700841def _run_checkpatch_no_tree(project, commit):
842 return _run_checkpatch(project, commit, ['--no-tree'])
843
Mike Frysingerae409522014-02-01 03:16:11 -0500844
Randall Spangler7318fd62013-11-21 12:16:58 -0800845def _run_checkpatch_ec(project, commit):
846 """Runs checkpatch with options for Chromium EC projects."""
847 return _run_checkpatch(project, commit, ['--no-tree',
848 '--ignore=MSLEEP,VOLATILE'])
849
Mike Frysingerae409522014-02-01 03:16:11 -0500850
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700851def _run_checkpatch_depthcharge(project, commit):
852 """Runs checkpatch with options for depthcharge."""
853 return _run_checkpatch(project, commit, [
854 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700855 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
856 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
857 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700858
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700859def _run_checkpatch_coreboot(project, commit):
860 """Runs checkpatch with options for coreboot."""
861 return _run_checkpatch(project, commit, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700862 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700863 '--no-tree',
864 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
865 'GLOBAL_INITIALISERS,INITIALISED_STATIC'])
866
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700867
Mike Frysingerae409522014-02-01 03:16:11 -0500868def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700869 """Makes sure kernel config changes are not mixed with code changes"""
870 files = _get_affected_files(commit)
871 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
872 return HookFailure('Changes to chromeos/config/ and regular files must '
873 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700874
Mike Frysingerae409522014-02-01 03:16:11 -0500875
876def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700877 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700878 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700879 try:
880 json.load(open(f))
881 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700882 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700883
884
Mike Frysingerae409522014-02-01 03:16:11 -0500885def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400886 """Make sure Manifest files only have DIST lines"""
887 paths = []
888
889 for path in _get_affected_files(commit):
890 if os.path.basename(path) != 'Manifest':
891 continue
892 if not os.path.exists(path):
893 continue
894
895 with open(path, 'r') as f:
896 for line in f.readlines():
897 if not line.startswith('DIST '):
898 paths.append(path)
899 break
900
901 if paths:
902 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
903 ('\n'.join(paths),))
904
905
Mike Frysingerae409522014-02-01 03:16:11 -0500906def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700907 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700908 if commit == PRE_SUBMIT:
909 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700910 BRANCH_RE = r'\nBRANCH=\S+'
911
912 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
913 msg = ('Changelist description needs BRANCH field (after first line)\n'
914 'E.g. BRANCH=none or BRANCH=link,snow')
915 return HookFailure(msg)
916
917
Mike Frysingerae409522014-02-01 03:16:11 -0500918def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800919 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700920 if commit == PRE_SUBMIT:
921 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800922 SIGNOFF_RE = r'\nSigned-off-by: \S+'
923
924 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
925 msg = ('Changelist description needs Signed-off-by: field\n'
926 'E.g. Signed-off-by: My Name <me@chromium.org>')
927 return HookFailure(msg)
928
929
Jon Salz3ee59de2012-08-18 13:54:22 +0800930def _run_project_hook_script(script, project, commit):
931 """Runs a project hook script.
932
933 The script is run with the following environment variables set:
934 PRESUBMIT_PROJECT: The affected project
935 PRESUBMIT_COMMIT: The affected commit
936 PRESUBMIT_FILES: A newline-separated list of affected files
937
938 The script is considered to fail if the exit code is non-zero. It should
939 write an error message to stdout.
940 """
941 env = dict(os.environ)
942 env['PRESUBMIT_PROJECT'] = project
943 env['PRESUBMIT_COMMIT'] = commit
944
945 # Put affected files in an environment variable
946 files = _get_affected_files(commit)
947 env['PRESUBMIT_FILES'] = '\n'.join(files)
948
949 process = subprocess.Popen(script, env=env, shell=True,
950 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800951 stdout=subprocess.PIPE,
952 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800953 stdout, _ = process.communicate()
954 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800955 if stdout:
956 stdout = re.sub('(?m)^', ' ', stdout)
957 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800958 (script, process.returncode,
959 ':\n' + stdout if stdout else ''))
960
961
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700962def _moved_to_platform2(project, _commit):
963 """Forbids commits to legacy repo in src/platform."""
964 return HookFailure('%s has been moved to platform2. This change should be '
965 'made there.' % project)
966
967
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700968def _check_project_prefix(_project, commit):
969 """Fails if the change is project specific and the commit message is not
970 prefixed by the project_name.
971 """
972
973 files = _get_affected_files(commit, relative=True)
974 prefix = os.path.commonprefix(files)
975 prefix = os.path.dirname(prefix)
976
977 # If there is no common prefix, the CL span multiple projects.
978 if prefix == '':
979 return
980
981 project_name = prefix.split('/')[0]
982 alias_file = os.path.join(prefix, '.project_alias')
983 # If an alias exists, use it.
984 if os.path.isfile(alias_file):
985 with open(alias_file, 'r') as f:
986 project_name = f.read().strip()
987
988 if not _get_commit_desc(commit).startswith(project_name + ': '):
989 return HookFailure('The commit title for changes affecting only %s'
990 ' should start with \"%s: \"'
991 % (project_name, project_name))
992
993
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700994# Base
995
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700996# A list of hooks which are not project specific and check patch description
997# (as opposed to patch body).
998_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -0700999 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001000 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001001 _check_change_has_test_field,
1002 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001003]
1004
1005
1006# A list of hooks that are not project-specific
1007_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001008 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001009 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001010 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001011 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -07001012 _check_no_stray_whitespace,
1013 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001014 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001015 _check_license,
1016 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001017 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001018]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001019
Ryan Cui1562fb82011-05-09 11:01:31 -07001020
Ryan Cui9b651632011-05-11 11:38:58 -07001021# A dictionary of project-specific hooks(callbacks), indexed by project name.
1022# dict[project] = [callback1, callback2]
1023_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001024 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001025 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1026 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001027 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001028 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001029 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001030 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1031 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001032 "chromiumos/overlays/board-overlays": [_check_manifests],
1033 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1034 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001035 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001036 "chromiumos/platform/depthcharge": [_check_change_has_signoff_field,
1037 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001038 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001039 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001040 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001041 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001042 "chromiumos/third_party/coreboot": [_check_change_has_signoff_field,
1043 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001044 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001045 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1046 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1047 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001048 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001049}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001050
Ryan Cui1562fb82011-05-09 11:01:31 -07001051
Ryan Cui9b651632011-05-11 11:38:58 -07001052# A dictionary of flags (keys) that can appear in the config file, and the hook
1053# that the flag disables (value)
1054_DISABLE_FLAGS = {
1055 'stray_whitespace_check': _check_no_stray_whitespace,
1056 'long_line_check': _check_no_long_lines,
1057 'cros_license_check': _check_license,
1058 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001059 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001060 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001061 'bug_field_check': _check_change_has_bug_field,
1062 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001063}
1064
1065
Jon Salz3ee59de2012-08-18 13:54:22 +08001066def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001067 """Returns a set of hooks disabled by the current project's config file.
1068
1069 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001070
1071 Args:
1072 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001073 """
1074 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001075 if not config.has_section(SECTION):
1076 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001077
1078 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001079 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001080 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001081 if not config.getboolean(SECTION, flag):
1082 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001083 except ValueError as e:
1084 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001085 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001086
1087 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1088 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1089
1090
Jon Salz3ee59de2012-08-18 13:54:22 +08001091def _get_project_hook_scripts(config):
1092 """Returns a list of project-specific hook scripts.
1093
1094 Args:
1095 config: A ConfigParser for the project's config file.
1096 """
1097 SECTION = 'Hook Scripts'
1098 if not config.has_section(SECTION):
1099 return []
1100
1101 hook_names_values = config.items(SECTION)
1102 hook_names_values.sort(key=lambda x: x[0])
1103 return [x[1] for x in hook_names_values]
1104
1105
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001106def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001107 """Returns a list of hooks that need to be run for a project.
1108
1109 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001110
1111 Args:
1112 project: A string, name of the project.
1113 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001114 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001115 config = ConfigParser.RawConfigParser()
1116 try:
1117 config.read(_CONFIG_FILE)
1118 except ConfigParser.Error:
1119 # Just use an empty config file
1120 config = ConfigParser.RawConfigParser()
1121
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001122 if presubmit:
1123 hook_list = _COMMON_HOOKS
1124 else:
1125 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1126
Jon Salz3ee59de2012-08-18 13:54:22 +08001127 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001128 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001129
1130 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001131 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1132 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001133
Jon Salz3ee59de2012-08-18 13:54:22 +08001134 for script in _get_project_hook_scripts(config):
1135 hooks.append(functools.partial(_run_project_hook_script, script))
1136
Ryan Cui9b651632011-05-11 11:38:58 -07001137 return hooks
1138
1139
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001140def _run_project_hooks(project, proj_dir=None,
1141 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001142 """For each project run its project specific hook from the hooks dictionary.
1143
1144 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001145 project: The name of project to run hooks for.
1146 proj_dir: If non-None, this is the directory the project is in. If None,
1147 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001148 commit_list: A list of commits to run hooks against. If None or empty list
1149 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001150 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001151
1152 Returns:
1153 Boolean value of whether any errors were ecountered while running the hooks.
1154 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001155 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001156 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1157 if len(proj_dirs) == 0:
1158 print('%s cannot be found.' % project, file=sys.stderr)
1159 print('Please specify a valid project.', file=sys.stderr)
1160 return True
1161 if len(proj_dirs) > 1:
1162 print('%s is associated with multiple directories.' % project,
1163 file=sys.stderr)
1164 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1165 return True
1166 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001167
Ryan Cuiec4d6332011-05-02 14:15:25 -07001168 pwd = os.getcwd()
1169 # hooks assume they are run from the root of the project
1170 os.chdir(proj_dir)
1171
Doug Anderson14749562013-06-26 13:38:29 -07001172 if not commit_list:
1173 try:
1174 commit_list = _get_commits()
1175 except VerifyException as e:
1176 PrintErrorForProject(project, HookFailure(str(e)))
1177 os.chdir(pwd)
1178 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001179
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001180 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001181 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001182 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001183 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001184 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001185 hook_error = hook(project, commit)
1186 if hook_error:
1187 error_list.append(hook_error)
1188 error_found = True
1189 if error_list:
1190 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1191 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001192
Ryan Cuiec4d6332011-05-02 14:15:25 -07001193 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001194 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001195
Mike Frysingerae409522014-02-01 03:16:11 -05001196
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001197# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001198
Ryan Cui1562fb82011-05-09 11:01:31 -07001199
Mike Frysingerae409522014-02-01 03:16:11 -05001200def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001201 """Main function invoked directly by repo.
1202
1203 This function will exit directly upon error so that repo doesn't print some
1204 obscure error message.
1205
1206 Args:
1207 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001208 worktree_list: A list of directories. It should be the same length as
1209 project_list, so that each entry in project_list matches with a directory
1210 in worktree_list. If None, we will attempt to calculate the directories
1211 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001212 kwargs: Leave this here for forward-compatibility.
1213 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001214 found_error = False
David James2edd9002013-10-11 14:09:19 -07001215 if not worktree_list:
1216 worktree_list = [None] * len(project_list)
1217 for project, worktree in zip(project_list, worktree_list):
1218 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001219 found_error = True
1220
Mike Frysingerae409522014-02-01 03:16:11 -05001221 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001222 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001223 '- To disable some source style checks, and for other hints, see '
1224 '<checkout_dir>/src/repohooks/README\n'
1225 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001226 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001227 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001228
Ryan Cui1562fb82011-05-09 11:01:31 -07001229
Doug Anderson44a644f2011-11-02 10:37:37 -07001230def _identify_project(path):
1231 """Identify the repo project associated with the given path.
1232
1233 Returns:
1234 A string indicating what project is associated with the path passed in or
1235 a blank string upon failure.
1236 """
1237 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1238 stderr=subprocess.PIPE, cwd=path).strip()
1239
1240
1241def direct_main(args, verbose=False):
1242 """Run hooks directly (outside of the context of repo).
1243
1244 # Setup for doctests below.
1245 # ...note that some tests assume that running pre-upload on this CWD is fine.
1246 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1247 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1248 >>> olddir = os.getcwd()
1249
1250 # OK to run w/ no arugments; will run with CWD.
1251 >>> os.chdir(mydir)
1252 >>> direct_main(['prog_name'], verbose=True)
1253 Running hooks on chromiumos/repohooks
1254 0
1255 >>> os.chdir(olddir)
1256
1257 # Run specifying a dir
1258 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1259 Running hooks on chromiumos/repohooks
1260 0
1261
1262 # Not a problem to use a bogus project; we'll just get default settings.
1263 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1264 Running hooks on X
1265 0
1266
1267 # Run with project but no dir
1268 >>> os.chdir(mydir)
1269 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1270 Running hooks on X
1271 0
1272 >>> os.chdir(olddir)
1273
1274 # Try with a non-git CWD
1275 >>> os.chdir('/tmp')
1276 >>> direct_main(['prog_name'])
1277 Traceback (most recent call last):
1278 ...
1279 BadInvocation: The current directory is not part of a git project.
1280
1281 # Check various bad arguments...
1282 >>> direct_main(['prog_name', 'bogus'])
1283 Traceback (most recent call last):
1284 ...
1285 BadInvocation: Unexpected arguments: bogus
1286 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1287 Traceback (most recent call last):
1288 ...
1289 BadInvocation: Invalid dir: bogusdir
1290 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1291 Traceback (most recent call last):
1292 ...
1293 BadInvocation: Not a git directory: /tmp
1294
1295 Args:
1296 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001297 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001298
1299 Returns:
1300 0 if no pre-upload failures, 1 if failures.
1301
1302 Raises:
1303 BadInvocation: On some types of invocation errors.
1304 """
1305 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1306 parser = optparse.OptionParser(description=desc)
1307
1308 parser.add_option('--dir', default=None,
1309 help='The directory that the project lives in. If not '
1310 'specified, use the git project root based on the cwd.')
1311 parser.add_option('--project', default=None,
1312 help='The project repo path; this can affect how the hooks '
1313 'get run, since some hooks are project-specific. For '
1314 'chromite this is chromiumos/chromite. If not specified, '
1315 'the repo tool will be used to figure this out based on '
1316 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001317 parser.add_option('--rerun-since', default=None,
1318 help='Rerun hooks on old commits since the given date. '
1319 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001320 'e.g. 2012-06-20. This option is mutually exclusive '
1321 'with --pre-submit.')
1322 parser.add_option('--pre-submit', action="store_true",
1323 help='Run the check against the pending commit. '
1324 'This option should be used at the \'git commit\' '
1325 'phase as opposed to \'repo upload\'. This option '
1326 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001327
1328 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001329
1330 opts, args = parser.parse_args(args[1:])
1331
Doug Anderson14749562013-06-26 13:38:29 -07001332 if opts.rerun_since:
1333 if args:
1334 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1335 ' '.join(args))
1336
1337 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1338 all_commits = _run_command(cmd).splitlines()
1339 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1340
1341 # Eliminate chrome-bot commits but keep ordering the same...
1342 bot_commits = set(bot_commits)
1343 args = [c for c in all_commits if c not in bot_commits]
1344
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001345 if opts.pre_submit:
1346 raise BadInvocation('rerun-since and pre-submit can not be '
1347 'used together')
1348 if opts.pre_submit:
1349 if args:
1350 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1351 ' '.join(args))
1352 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001353
1354 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1355 # project from CWD
1356 if opts.dir is None:
1357 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1358 stderr=subprocess.PIPE).strip()
1359 if not git_dir:
1360 raise BadInvocation('The current directory is not part of a git project.')
1361 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1362 elif not os.path.isdir(opts.dir):
1363 raise BadInvocation('Invalid dir: %s' % opts.dir)
1364 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1365 raise BadInvocation('Not a git directory: %s' % opts.dir)
1366
1367 # Identify the project if it wasn't specified; this _requires_ the repo
1368 # tool to be installed and for the project to be part of a repo checkout.
1369 if not opts.project:
1370 opts.project = _identify_project(opts.dir)
1371 if not opts.project:
1372 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1373
1374 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001375 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001376
Doug Anderson14749562013-06-26 13:38:29 -07001377 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001378 commit_list=args,
1379 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001380 if found_error:
1381 return 1
1382 return 0
1383
1384
1385def _test():
1386 """Run any built-in tests."""
1387 import doctest
1388 doctest.testmod()
1389
1390
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001391if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001392 if sys.argv[1:2] == ["--test"]:
1393 _test()
1394 exit_code = 0
1395 else:
1396 prog_name = os.path.basename(sys.argv[0])
1397 try:
1398 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001399 except BadInvocation, err:
1400 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001401 exit_code = 1
1402 sys.exit(exit_code)