blob: 17ceb82ac724b3762c374bfec153d561a9c0021d [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.
Mike Frysinger324cf682014-09-22 15:52:50 -0400810 for line in data:
811 if line.startswith('repo-name = '):
812 break
813 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -0400814 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400815
Mike Frysinger94a670c2014-09-19 12:46:26 -0400816 # Summarize all the errors we saw (if any).
817 lines = ''
818 for layout_path, errors in all_errors.items():
819 if errors:
820 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
821 if lines:
822 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
823 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400824
825
Ryan Cuiec4d6332011-05-02 14:15:25 -0700826# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700827
Ryan Cui1562fb82011-05-09 11:01:31 -0700828
Mike Frysingerae409522014-02-01 03:16:11 -0500829def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700830 """Runs checkpatch.pl on the given project"""
831 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700832 options = list(options)
833 if commit == PRE_SUBMIT:
834 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
835 # this case.
836 options.append('--ignore=MISSING_SIGN_OFF')
837 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700838 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700839 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700840 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700841 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700842
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700843
Anton Staaf815d6852011-08-22 10:08:45 -0700844def _run_checkpatch_no_tree(project, commit):
845 return _run_checkpatch(project, commit, ['--no-tree'])
846
Mike Frysingerae409522014-02-01 03:16:11 -0500847
Randall Spangler7318fd62013-11-21 12:16:58 -0800848def _run_checkpatch_ec(project, commit):
849 """Runs checkpatch with options for Chromium EC projects."""
850 return _run_checkpatch(project, commit, ['--no-tree',
851 '--ignore=MSLEEP,VOLATILE'])
852
Mike Frysingerae409522014-02-01 03:16:11 -0500853
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700854def _run_checkpatch_depthcharge(project, commit):
855 """Runs checkpatch with options for depthcharge."""
856 return _run_checkpatch(project, commit, [
857 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700858 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
859 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
860 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700861
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700862def _run_checkpatch_coreboot(project, commit):
863 """Runs checkpatch with options for coreboot."""
864 return _run_checkpatch(project, commit, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700865 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700866 '--no-tree',
867 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
868 'GLOBAL_INITIALISERS,INITIALISED_STATIC'])
869
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700870
Mike Frysingerae409522014-02-01 03:16:11 -0500871def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700872 """Makes sure kernel config changes are not mixed with code changes"""
873 files = _get_affected_files(commit)
874 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
875 return HookFailure('Changes to chromeos/config/ and regular files must '
876 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700877
Mike Frysingerae409522014-02-01 03:16:11 -0500878
879def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700880 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700881 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700882 try:
883 json.load(open(f))
884 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700885 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700886
887
Mike Frysingerae409522014-02-01 03:16:11 -0500888def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400889 """Make sure Manifest files only have DIST lines"""
890 paths = []
891
892 for path in _get_affected_files(commit):
893 if os.path.basename(path) != 'Manifest':
894 continue
895 if not os.path.exists(path):
896 continue
897
898 with open(path, 'r') as f:
899 for line in f.readlines():
900 if not line.startswith('DIST '):
901 paths.append(path)
902 break
903
904 if paths:
905 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
906 ('\n'.join(paths),))
907
908
Mike Frysingerae409522014-02-01 03:16:11 -0500909def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700910 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700911 if commit == PRE_SUBMIT:
912 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700913 BRANCH_RE = r'\nBRANCH=\S+'
914
915 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
916 msg = ('Changelist description needs BRANCH field (after first line)\n'
917 'E.g. BRANCH=none or BRANCH=link,snow')
918 return HookFailure(msg)
919
920
Mike Frysingerae409522014-02-01 03:16:11 -0500921def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800922 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700923 if commit == PRE_SUBMIT:
924 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800925 SIGNOFF_RE = r'\nSigned-off-by: \S+'
926
927 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
928 msg = ('Changelist description needs Signed-off-by: field\n'
929 'E.g. Signed-off-by: My Name <me@chromium.org>')
930 return HookFailure(msg)
931
932
Jon Salz3ee59de2012-08-18 13:54:22 +0800933def _run_project_hook_script(script, project, commit):
934 """Runs a project hook script.
935
936 The script is run with the following environment variables set:
937 PRESUBMIT_PROJECT: The affected project
938 PRESUBMIT_COMMIT: The affected commit
939 PRESUBMIT_FILES: A newline-separated list of affected files
940
941 The script is considered to fail if the exit code is non-zero. It should
942 write an error message to stdout.
943 """
944 env = dict(os.environ)
945 env['PRESUBMIT_PROJECT'] = project
946 env['PRESUBMIT_COMMIT'] = commit
947
948 # Put affected files in an environment variable
949 files = _get_affected_files(commit)
950 env['PRESUBMIT_FILES'] = '\n'.join(files)
951
952 process = subprocess.Popen(script, env=env, shell=True,
953 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800954 stdout=subprocess.PIPE,
955 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800956 stdout, _ = process.communicate()
957 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800958 if stdout:
959 stdout = re.sub('(?m)^', ' ', stdout)
960 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800961 (script, process.returncode,
962 ':\n' + stdout if stdout else ''))
963
964
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700965def _moved_to_platform2(project, _commit):
966 """Forbids commits to legacy repo in src/platform."""
967 return HookFailure('%s has been moved to platform2. This change should be '
968 'made there.' % project)
969
970
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700971def _check_project_prefix(_project, commit):
972 """Fails if the change is project specific and the commit message is not
973 prefixed by the project_name.
974 """
975
976 files = _get_affected_files(commit, relative=True)
977 prefix = os.path.commonprefix(files)
978 prefix = os.path.dirname(prefix)
979
980 # If there is no common prefix, the CL span multiple projects.
981 if prefix == '':
982 return
983
984 project_name = prefix.split('/')[0]
985 alias_file = os.path.join(prefix, '.project_alias')
986 # If an alias exists, use it.
987 if os.path.isfile(alias_file):
988 with open(alias_file, 'r') as f:
989 project_name = f.read().strip()
990
991 if not _get_commit_desc(commit).startswith(project_name + ': '):
992 return HookFailure('The commit title for changes affecting only %s'
993 ' should start with \"%s: \"'
994 % (project_name, project_name))
995
996
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700997# Base
998
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700999# A list of hooks which are not project specific and check patch description
1000# (as opposed to patch body).
1001_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001002 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001003 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001004 _check_change_has_test_field,
1005 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001006]
1007
1008
1009# A list of hooks that are not project-specific
1010_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001011 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001012 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001013 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001014 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -07001015 _check_no_stray_whitespace,
1016 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001017 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001018 _check_license,
1019 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001020 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001021]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001022
Ryan Cui1562fb82011-05-09 11:01:31 -07001023
Ryan Cui9b651632011-05-11 11:38:58 -07001024# A dictionary of project-specific hooks(callbacks), indexed by project name.
1025# dict[project] = [callback1, callback2]
1026_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001027 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001028 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1029 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001030 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001031 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001032 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001033 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1034 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001035 "chromiumos/overlays/board-overlays": [_check_manifests],
1036 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1037 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001038 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001039 "chromiumos/platform/depthcharge": [_check_change_has_signoff_field,
1040 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001041 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001042 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001043 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001044 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001045 "chromiumos/third_party/coreboot": [_check_change_has_signoff_field,
1046 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001047 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001048 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1049 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1050 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001051 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001052}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001053
Ryan Cui1562fb82011-05-09 11:01:31 -07001054
Ryan Cui9b651632011-05-11 11:38:58 -07001055# A dictionary of flags (keys) that can appear in the config file, and the hook
1056# that the flag disables (value)
1057_DISABLE_FLAGS = {
1058 'stray_whitespace_check': _check_no_stray_whitespace,
1059 'long_line_check': _check_no_long_lines,
1060 'cros_license_check': _check_license,
1061 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001062 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001063 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001064 'bug_field_check': _check_change_has_bug_field,
1065 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001066}
1067
1068
Jon Salz3ee59de2012-08-18 13:54:22 +08001069def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001070 """Returns a set of hooks disabled by the current project's config file.
1071
1072 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001073
1074 Args:
1075 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001076 """
1077 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001078 if not config.has_section(SECTION):
1079 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001080
1081 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001082 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001083 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001084 if not config.getboolean(SECTION, flag):
1085 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001086 except ValueError as e:
1087 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001088 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001089
1090 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1091 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1092
1093
Jon Salz3ee59de2012-08-18 13:54:22 +08001094def _get_project_hook_scripts(config):
1095 """Returns a list of project-specific hook scripts.
1096
1097 Args:
1098 config: A ConfigParser for the project's config file.
1099 """
1100 SECTION = 'Hook Scripts'
1101 if not config.has_section(SECTION):
1102 return []
1103
1104 hook_names_values = config.items(SECTION)
1105 hook_names_values.sort(key=lambda x: x[0])
1106 return [x[1] for x in hook_names_values]
1107
1108
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001109def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001110 """Returns a list of hooks that need to be run for a project.
1111
1112 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001113
1114 Args:
1115 project: A string, name of the project.
1116 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001117 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001118 config = ConfigParser.RawConfigParser()
1119 try:
1120 config.read(_CONFIG_FILE)
1121 except ConfigParser.Error:
1122 # Just use an empty config file
1123 config = ConfigParser.RawConfigParser()
1124
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001125 if presubmit:
1126 hook_list = _COMMON_HOOKS
1127 else:
1128 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1129
Jon Salz3ee59de2012-08-18 13:54:22 +08001130 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001131 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001132
1133 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001134 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1135 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001136
Jon Salz3ee59de2012-08-18 13:54:22 +08001137 for script in _get_project_hook_scripts(config):
1138 hooks.append(functools.partial(_run_project_hook_script, script))
1139
Ryan Cui9b651632011-05-11 11:38:58 -07001140 return hooks
1141
1142
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001143def _run_project_hooks(project, proj_dir=None,
1144 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001145 """For each project run its project specific hook from the hooks dictionary.
1146
1147 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001148 project: The name of project to run hooks for.
1149 proj_dir: If non-None, this is the directory the project is in. If None,
1150 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001151 commit_list: A list of commits to run hooks against. If None or empty list
1152 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001153 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001154
1155 Returns:
1156 Boolean value of whether any errors were ecountered while running the hooks.
1157 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001158 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001159 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1160 if len(proj_dirs) == 0:
1161 print('%s cannot be found.' % project, file=sys.stderr)
1162 print('Please specify a valid project.', file=sys.stderr)
1163 return True
1164 if len(proj_dirs) > 1:
1165 print('%s is associated with multiple directories.' % project,
1166 file=sys.stderr)
1167 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1168 return True
1169 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001170
Ryan Cuiec4d6332011-05-02 14:15:25 -07001171 pwd = os.getcwd()
1172 # hooks assume they are run from the root of the project
1173 os.chdir(proj_dir)
1174
Doug Anderson14749562013-06-26 13:38:29 -07001175 if not commit_list:
1176 try:
1177 commit_list = _get_commits()
1178 except VerifyException as e:
1179 PrintErrorForProject(project, HookFailure(str(e)))
1180 os.chdir(pwd)
1181 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001182
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001183 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001184 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001185 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001186 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001187 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001188 hook_error = hook(project, commit)
1189 if hook_error:
1190 error_list.append(hook_error)
1191 error_found = True
1192 if error_list:
1193 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1194 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001195
Ryan Cuiec4d6332011-05-02 14:15:25 -07001196 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001197 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001198
Mike Frysingerae409522014-02-01 03:16:11 -05001199
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001200# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001201
Ryan Cui1562fb82011-05-09 11:01:31 -07001202
Mike Frysingerae409522014-02-01 03:16:11 -05001203def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001204 """Main function invoked directly by repo.
1205
1206 This function will exit directly upon error so that repo doesn't print some
1207 obscure error message.
1208
1209 Args:
1210 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001211 worktree_list: A list of directories. It should be the same length as
1212 project_list, so that each entry in project_list matches with a directory
1213 in worktree_list. If None, we will attempt to calculate the directories
1214 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001215 kwargs: Leave this here for forward-compatibility.
1216 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001217 found_error = False
David James2edd9002013-10-11 14:09:19 -07001218 if not worktree_list:
1219 worktree_list = [None] * len(project_list)
1220 for project, worktree in zip(project_list, worktree_list):
1221 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001222 found_error = True
1223
Mike Frysingerae409522014-02-01 03:16:11 -05001224 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001225 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001226 '- To disable some source style checks, and for other hints, see '
1227 '<checkout_dir>/src/repohooks/README\n'
1228 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001229 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001230 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001231
Ryan Cui1562fb82011-05-09 11:01:31 -07001232
Doug Anderson44a644f2011-11-02 10:37:37 -07001233def _identify_project(path):
1234 """Identify the repo project associated with the given path.
1235
1236 Returns:
1237 A string indicating what project is associated with the path passed in or
1238 a blank string upon failure.
1239 """
1240 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1241 stderr=subprocess.PIPE, cwd=path).strip()
1242
1243
1244def direct_main(args, verbose=False):
1245 """Run hooks directly (outside of the context of repo).
1246
1247 # Setup for doctests below.
1248 # ...note that some tests assume that running pre-upload on this CWD is fine.
1249 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1250 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1251 >>> olddir = os.getcwd()
1252
1253 # OK to run w/ no arugments; will run with CWD.
1254 >>> os.chdir(mydir)
1255 >>> direct_main(['prog_name'], verbose=True)
1256 Running hooks on chromiumos/repohooks
1257 0
1258 >>> os.chdir(olddir)
1259
1260 # Run specifying a dir
1261 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1262 Running hooks on chromiumos/repohooks
1263 0
1264
1265 # Not a problem to use a bogus project; we'll just get default settings.
1266 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1267 Running hooks on X
1268 0
1269
1270 # Run with project but no dir
1271 >>> os.chdir(mydir)
1272 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1273 Running hooks on X
1274 0
1275 >>> os.chdir(olddir)
1276
1277 # Try with a non-git CWD
1278 >>> os.chdir('/tmp')
1279 >>> direct_main(['prog_name'])
1280 Traceback (most recent call last):
1281 ...
1282 BadInvocation: The current directory is not part of a git project.
1283
1284 # Check various bad arguments...
1285 >>> direct_main(['prog_name', 'bogus'])
1286 Traceback (most recent call last):
1287 ...
1288 BadInvocation: Unexpected arguments: bogus
1289 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1290 Traceback (most recent call last):
1291 ...
1292 BadInvocation: Invalid dir: bogusdir
1293 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1294 Traceback (most recent call last):
1295 ...
1296 BadInvocation: Not a git directory: /tmp
1297
1298 Args:
1299 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001300 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001301
1302 Returns:
1303 0 if no pre-upload failures, 1 if failures.
1304
1305 Raises:
1306 BadInvocation: On some types of invocation errors.
1307 """
1308 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1309 parser = optparse.OptionParser(description=desc)
1310
1311 parser.add_option('--dir', default=None,
1312 help='The directory that the project lives in. If not '
1313 'specified, use the git project root based on the cwd.')
1314 parser.add_option('--project', default=None,
1315 help='The project repo path; this can affect how the hooks '
1316 'get run, since some hooks are project-specific. For '
1317 'chromite this is chromiumos/chromite. If not specified, '
1318 'the repo tool will be used to figure this out based on '
1319 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001320 parser.add_option('--rerun-since', default=None,
1321 help='Rerun hooks on old commits since the given date. '
1322 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001323 'e.g. 2012-06-20. This option is mutually exclusive '
1324 'with --pre-submit.')
1325 parser.add_option('--pre-submit', action="store_true",
1326 help='Run the check against the pending commit. '
1327 'This option should be used at the \'git commit\' '
1328 'phase as opposed to \'repo upload\'. This option '
1329 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001330
1331 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001332
1333 opts, args = parser.parse_args(args[1:])
1334
Doug Anderson14749562013-06-26 13:38:29 -07001335 if opts.rerun_since:
1336 if args:
1337 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1338 ' '.join(args))
1339
1340 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1341 all_commits = _run_command(cmd).splitlines()
1342 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1343
1344 # Eliminate chrome-bot commits but keep ordering the same...
1345 bot_commits = set(bot_commits)
1346 args = [c for c in all_commits if c not in bot_commits]
1347
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001348 if opts.pre_submit:
1349 raise BadInvocation('rerun-since and pre-submit can not be '
1350 'used together')
1351 if opts.pre_submit:
1352 if args:
1353 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1354 ' '.join(args))
1355 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001356
1357 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1358 # project from CWD
1359 if opts.dir is None:
1360 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1361 stderr=subprocess.PIPE).strip()
1362 if not git_dir:
1363 raise BadInvocation('The current directory is not part of a git project.')
1364 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1365 elif not os.path.isdir(opts.dir):
1366 raise BadInvocation('Invalid dir: %s' % opts.dir)
1367 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1368 raise BadInvocation('Not a git directory: %s' % opts.dir)
1369
1370 # Identify the project if it wasn't specified; this _requires_ the repo
1371 # tool to be installed and for the project to be part of a repo checkout.
1372 if not opts.project:
1373 opts.project = _identify_project(opts.dir)
1374 if not opts.project:
1375 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1376
1377 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001378 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001379
Doug Anderson14749562013-06-26 13:38:29 -07001380 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001381 commit_list=args,
1382 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001383 if found_error:
1384 return 1
1385 return 0
1386
1387
1388def _test():
1389 """Run any built-in tests."""
1390 import doctest
1391 doctest.testmod()
1392
1393
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001394if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001395 if sys.argv[1:2] == ["--test"]:
1396 _test()
1397 exit_code = 0
1398 else:
1399 prog_name = os.path.basename(sys.argv[0])
1400 try:
1401 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001402 except BadInvocation, err:
1403 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001404 exit_code = 1
1405 sys.exit(exit_code)