blob: 064bb8fa8d69d2429610ada1fdc21eb94a153257 [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
Daniel Erata350fd32014-09-29 14:02:34 -070032from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070033from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040034from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070035
Vadim Bendebury2b62d742014-06-22 13:14:51 -070036PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070037
38COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050039 # C++ and friends
40 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
41 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
42 # Scripts
43 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
44 # No extension at all, note that ALL CAPS files are black listed in
45 # COMMON_EXCLUDED_LIST below.
46 r"(^|.*[\\\/])[^.]+$",
47 # Other
48 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070049]
50
Ryan Cui1562fb82011-05-09 11:01:31 -070051
Ryan Cuiec4d6332011-05-02 14:15:25 -070052COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050053 # avoid doing source file checks for kernel
54 r"/src/third_party/kernel/",
55 r"/src/third_party/kernel-next/",
56 r"/src/third_party/ktop/",
57 r"/src/third_party/punybench/",
58 r".*\bexperimental[\\\/].*",
59 r".*\b[A-Z0-9_]{2,}$",
60 r".*[\\\/]debian[\\\/]rules$",
61 # for ebuild trees, ignore any caches and manifest data
62 r".*/Manifest$",
63 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070064
Mike Frysingerae409522014-02-01 03:16:11 -050065 # ignore profiles data (like overlay-tegra2/profiles)
Mike Frysinger94a670c2014-09-19 12:46:26 -040066 r"(^|.*/)overlay-.*/profiles/.*",
Mike Frysinger98638102014-08-28 00:15:08 -040067 r"^profiles/.*$",
68
Mike Frysingerae409522014-02-01 03:16:11 -050069 # ignore minified js and jquery
70 r".*\.min\.js",
71 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050072
73 # Ignore license files as the content is often taken verbatim.
74 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070075]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070076
Ryan Cui1562fb82011-05-09 11:01:31 -070077
Ryan Cui9b651632011-05-11 11:38:58 -070078_CONFIG_FILE = 'PRESUBMIT.cfg'
79
80
Doug Anderson44a644f2011-11-02 10:37:37 -070081# Exceptions
82
83
84class BadInvocation(Exception):
85 """An Exception indicating a bad invocation of the program."""
86 pass
87
88
Ryan Cui1562fb82011-05-09 11:01:31 -070089# General Helpers
90
Sean Paulba01d402011-05-05 11:36:23 -040091
Doug Anderson44a644f2011-11-02 10:37:37 -070092def _run_command(cmd, cwd=None, stderr=None):
93 """Executes the passed in command and returns raw stdout output.
94
95 Args:
96 cmd: The command to run; should be a list of strings.
97 cwd: The directory to switch to for running the command.
98 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
99 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
100
101 Returns:
102 The standard out from the process.
103 """
104 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
105 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700106
Ryan Cui1562fb82011-05-09 11:01:31 -0700107
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700108def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700109 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700110 if __name__ == '__main__':
111 # Works when file is run on its own (__file__ is defined)...
112 return os.path.abspath(os.path.dirname(__file__))
113 else:
114 # We need to do this when we're run through repo. Since repo executes
115 # us with execfile(), we don't get __file__ defined.
116 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
117 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700118
Ryan Cui1562fb82011-05-09 11:01:31 -0700119
Ryan Cuiec4d6332011-05-02 14:15:25 -0700120def _match_regex_list(subject, expressions):
121 """Try to match a list of regular expressions to a string.
122
123 Args:
124 subject: The string to match regexes on
125 expressions: A list of regular expressions to check for matches with.
126
127 Returns:
128 Whether the passed in subject matches any of the passed in regexes.
129 """
130 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500131 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700132 return True
133 return False
134
Ryan Cui1562fb82011-05-09 11:01:31 -0700135
Mike Frysingerae409522014-02-01 03:16:11 -0500136def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700137 """Filter out files based on the conditions passed in.
138
139 Args:
140 files: list of filepaths to filter
141 include_list: list of regex that when matched with a file path will cause it
142 to be added to the output list unless the file is also matched with a
143 regex in the exclude_list.
144 exclude_list: list of regex that when matched with a file will prevent it
145 from being added to the output list, even if it is also matched with a
146 regex in the include_list.
147
148 Returns:
149 A list of filepaths that contain files matched in the include_list and not
150 in the exclude_list.
151 """
152 filtered = []
153 for f in files:
154 if (_match_regex_list(f, include_list) and
155 not _match_regex_list(f, exclude_list)):
156 filtered.append(f)
157 return filtered
158
Ryan Cuiec4d6332011-05-02 14:15:25 -0700159
160# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700161
162
Ryan Cui4725d952011-05-05 15:41:19 -0700163def _get_upstream_branch():
164 """Returns the upstream tracking branch of the current branch.
165
166 Raises:
167 Error if there is no tracking branch
168 """
169 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
170 current_branch = current_branch.replace('refs/heads/', '')
171 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700172 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700173
174 cfg_option = 'branch.' + current_branch + '.%s'
175 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
176 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
177 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700178 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700179
180 return full_upstream.replace('heads', 'remotes/' + remote)
181
Ryan Cui1562fb82011-05-09 11:01:31 -0700182
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700183def _get_patch(commit):
184 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700185 if commit == PRE_SUBMIT:
186 return _run_command(['git', 'diff', '--cached', 'HEAD'])
187 else:
188 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700189
Ryan Cui1562fb82011-05-09 11:01:31 -0700190
Jon Salz98255932012-08-18 14:48:02 +0800191def _try_utf8_decode(data):
192 """Attempts to decode a string as UTF-8.
193
194 Returns:
195 The decoded Unicode object, or the original string if parsing fails.
196 """
197 try:
198 return unicode(data, 'utf-8', 'strict')
199 except UnicodeDecodeError:
200 return data
201
202
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500203def _get_file_content(path, commit):
204 """Returns the content of a file at a specific commit.
205
206 We can't rely on the file as it exists in the filesystem as people might be
207 uploading a series of changes which modifies the file multiple times.
208
209 Note: The "content" of a symlink is just the target. So if you're expecting
210 a full file, you should check that first. One way to detect is that the
211 content will not have any newlines.
212 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700213 if commit == PRE_SUBMIT:
214 return _run_command(['git', 'diff', 'HEAD', path])
215 else:
216 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500217
218
Mike Frysingerae409522014-02-01 03:16:11 -0500219def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700220 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700221 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff']
222 if commit == PRE_SUBMIT:
223 command += ['HEAD', path]
224 else:
225 command += [commit, path]
226 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700227
228 new_lines = []
229 line_num = 0
230 for line in output.splitlines():
231 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
232 if m:
233 line_num = int(m.groups(1)[0])
234 continue
235 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800236 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700237 if not line.startswith('-'):
238 line_num += 1
239 return new_lines
240
Ryan Cui1562fb82011-05-09 11:01:31 -0700241
Peter Ammon811f6702014-06-12 15:45:38 -0700242def _parse_affected_files(output, include_deletes=False, relative=False):
243 """Parses git diff's 'raw' format, returning a list of modified file paths.
244
245 This excludes directories and symlinks, and optionally includes files that
246 were deleted.
Doug Anderson42b8a052013-06-26 10:45:36 -0700247
248 Args:
Peter Ammon811f6702014-06-12 15:45:38 -0700249 output: The result of the 'git diff --raw' command
250 include_deletes: If true, we'll include deleted files in the result
251 relative: Whether to return relative or full paths to files
Doug Anderson42b8a052013-06-26 10:45:36 -0700252
253 Returns:
254 A list of modified/added (and perhaps deleted) files
255 """
Ryan Cuiec4d6332011-05-02 14:15:25 -0700256 files = []
Peter Ammon811f6702014-06-12 15:45:38 -0700257 # See the documentation for 'git diff --raw' for the relevant format.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700258 for statusline in output.splitlines():
Peter Ammon811f6702014-06-12 15:45:38 -0700259 attributes, paths = statusline.split('\t', 1)
260 _, mode, _, _, status = attributes.split(' ')
261
262 # Ignore symlinks and directories.
263 imode = int(mode, 8)
264 if stat.S_ISDIR(imode) or stat.S_ISLNK(imode):
265 continue
266
267 # Ignore deleted files, and optionally return absolute paths of files.
268 if include_deletes or status != 'D':
269 # If a file was merely modified, we will have a single file path.
270 # If it was moved, we will have two paths (source and destination).
271 # In either case, we want the last path.
272 f = paths.split('\t')[-1]
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500273 if not relative:
274 pwd = os.getcwd()
275 f = os.path.join(pwd, f)
276 files.append(f)
Peter Ammon811f6702014-06-12 15:45:38 -0700277
Ryan Cuiec4d6332011-05-02 14:15:25 -0700278 return files
279
Ryan Cui1562fb82011-05-09 11:01:31 -0700280
Peter Ammon811f6702014-06-12 15:45:38 -0700281def _get_affected_files(commit, include_deletes=False, relative=False):
282 """Returns list of file paths that were modified/added, excluding symlinks.
283
284 Args:
285 commit: The commit
286 include_deletes: If true, we'll include deleted files in the result
287 relative: Whether to return relative or full paths to files
288
289 Returns:
290 A list of modified/added (and perhaps deleted) files
291 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700292 if commit == PRE_SUBMIT:
293 return _run_command(['git', 'diff-index', '--cached',
294 '--name-only', 'HEAD']).split()
Peter Ammon811f6702014-06-12 15:45:38 -0700295 output = _run_command(['git', 'diff', '--raw', commit + '^!'])
296 return _parse_affected_files(output, include_deletes, relative)
297
298
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700299def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700300 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700301 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700302 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700303
Ryan Cui1562fb82011-05-09 11:01:31 -0700304
Ryan Cuiec4d6332011-05-02 14:15:25 -0700305def _get_commit_desc(commit):
306 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700307 if commit == PRE_SUBMIT:
308 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400309 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700310
311
312# Common Hooks
313
Ryan Cui1562fb82011-05-09 11:01:31 -0700314
Mike Frysingerae409522014-02-01 03:16:11 -0500315def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700316 """Checks that there aren't any lines longer than maxlen characters in any of
317 the text files to be submitted.
318 """
319 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800320 SKIP_REGEXP = re.compile('|'.join([
321 r'https?://',
322 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700323
324 errors = []
325 files = _filter_files(_get_affected_files(commit),
326 COMMON_INCLUDED_PATHS,
327 COMMON_EXCLUDED_PATHS)
328
329 for afile in files:
330 for line_num, line in _get_file_diff(afile, commit):
331 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500332 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800333 continue
334
335 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
336 if len(errors) == 5: # Just show the first 5 errors.
337 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700338
339 if errors:
340 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700341 return HookFailure(msg, errors)
342
Ryan Cuiec4d6332011-05-02 14:15:25 -0700343
Mike Frysingerae409522014-02-01 03:16:11 -0500344def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700345 """Checks that there is no stray whitespace at source lines end."""
346 errors = []
347 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500348 COMMON_INCLUDED_PATHS,
349 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700350 for afile in files:
351 for line_num, line in _get_file_diff(afile, commit):
352 if line.rstrip() != line:
353 errors.append('%s, line %s' % (afile, line_num))
354 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700355 return HookFailure('Found line ending with white space in:', errors)
356
Ryan Cuiec4d6332011-05-02 14:15:25 -0700357
Mike Frysingerae409522014-02-01 03:16:11 -0500358def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700359 """Checks there are no unexpanded tabs."""
360 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700361 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700362 r".*\.ebuild$",
363 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500364 r".*/[M|m]akefile$",
365 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700366 ]
367
368 errors = []
369 files = _filter_files(_get_affected_files(commit),
370 COMMON_INCLUDED_PATHS,
371 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
372
373 for afile in files:
374 for line_num, line in _get_file_diff(afile, commit):
375 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500376 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700377 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700378 return HookFailure('Found a tab character in:', errors)
379
Ryan Cuiec4d6332011-05-02 14:15:25 -0700380
Mike Frysingerae409522014-02-01 03:16:11 -0500381def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700382 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700383 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700384
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700385 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700386 msg = 'Changelist description needs TEST field (after first line)'
387 return HookFailure(msg)
388
Ryan Cuiec4d6332011-05-02 14:15:25 -0700389
Mike Frysingerae409522014-02-01 03:16:11 -0500390def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700391 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
392 msg = 'Changelist has invalid CQ-DEPEND target.'
393 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
394 try:
395 patch.GetPaladinDeps(_get_commit_desc(commit))
396 except ValueError as ex:
397 return HookFailure(msg, [example, str(ex)])
398
399
Mike Frysingerae409522014-02-01 03:16:11 -0500400def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700401 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700402 OLD_BUG_RE = r'\nBUG=.*chromium-os'
403 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
404 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
405 'the chromium tracker in your BUG= line now.')
406 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700407
David James5c0073d2013-04-03 08:48:52 -0700408 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700409 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700410 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700411 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700412 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
413 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700414 return HookFailure(msg)
415
Ryan Cuiec4d6332011-05-02 14:15:25 -0700416
Doug Anderson42b8a052013-06-26 10:45:36 -0700417def _check_for_uprev(project, commit):
418 """Check that we're not missing a revbump of an ebuild in the given commit.
419
420 If the given commit touches files in a directory that has ebuilds somewhere
421 up the directory hierarchy, it's very likely that we need an ebuild revbump
422 in order for those changes to take effect.
423
424 It's not totally trivial to detect a revbump, so at least detect that an
425 ebuild with a revision number in it was touched. This should handle the
426 common case where we use a symlink to do the revbump.
427
428 TODO: it would be nice to enhance this hook to:
429 * Handle cases where people revbump with a slightly different syntax. I see
430 one ebuild (puppy) that revbumps with _pN. This is a false positive.
431 * Catches cases where people aren't using symlinks for revbumps. If they
432 edit a revisioned file directly (and are expected to rename it for revbump)
433 we'll miss that. Perhaps we could detect that the file touched is a
434 symlink?
435
436 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
437 still better off than without this check.
438
439 Args:
440 project: The project to look at
441 commit: The commit to look at
442
443 Returns:
444 A HookFailure or None.
445 """
Mike Frysinger011af942014-01-17 16:12:22 -0500446 # If this is the portage-stable overlay, then ignore the check. It's rare
447 # that we're doing anything other than importing files from upstream, so
448 # forcing a rev bump makes no sense.
449 whitelist = (
450 'chromiumos/overlays/portage-stable',
451 )
452 if project in whitelist:
453 return None
454
Doug Anderson42b8a052013-06-26 10:45:36 -0700455 affected_paths = _get_affected_files(commit, include_deletes=True)
456
457 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500458 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700459 affected_paths = [path for path in affected_paths
460 if os.path.basename(path) not in whitelist]
461 if not affected_paths:
462 return None
463
464 # If we've touched any file named with a -rN.ebuild then we'll say we're
465 # OK right away. See TODO above about enhancing this.
466 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
467 for path in affected_paths)
468 if touched_revved_ebuild:
469 return None
470
471 # We want to examine the current contents of all directories that are parents
472 # of files that were touched (up to the top of the project).
473 #
474 # ...note: we use the current directory contents even though it may have
475 # changed since the commit we're looking at. This is just a heuristic after
476 # all. Worst case we don't flag a missing revbump.
477 project_top = os.getcwd()
478 dirs_to_check = set([project_top])
479 for path in affected_paths:
480 path = os.path.dirname(path)
481 while os.path.exists(path) and not os.path.samefile(path, project_top):
482 dirs_to_check.add(path)
483 path = os.path.dirname(path)
484
485 # Look through each directory. If it's got an ebuild in it then we'll
486 # consider this as a case when we need a revbump.
487 for dir_path in dirs_to_check:
488 contents = os.listdir(dir_path)
489 ebuilds = [os.path.join(dir_path, path)
490 for path in contents if path.endswith('.ebuild')]
491 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
492
493 # If the -9999.ebuild file was touched the bot will uprev for us.
494 # ...we'll use a simple intersection here as a heuristic...
495 if set(ebuilds_9999) & set(affected_paths):
496 continue
497
498 if ebuilds:
499 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
500 'or a -r1.ebuild symlink if this is a new ebuild')
501
502 return None
503
504
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500505def _check_ebuild_eapi(project, commit):
506 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
507
508 We want to get away from older EAPI's as it makes life confusing and they
509 have less builtin error checking.
510
511 Args:
512 project: The project to look at
513 commit: The commit to look at
514
515 Returns:
516 A HookFailure or None.
517 """
518 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500519 # that we're doing anything other than importing files from upstream, and
520 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500521 whitelist = (
522 'chromiumos/overlays/portage-stable',
523 )
524 if project in whitelist:
525 return None
526
527 BAD_EAPIS = ('0', '1', '2', '3')
528
529 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
530
531 ebuilds_re = [r'\.ebuild$']
532 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
533 ebuilds_re)
534 bad_ebuilds = []
535
536 for ebuild in ebuilds:
537 # If the ebuild does not specify an EAPI, it defaults to 0.
538 eapi = '0'
539
540 lines = _get_file_content(ebuild, commit).splitlines()
541 if len(lines) == 1:
542 # This is most likely a symlink, so skip it entirely.
543 continue
544
545 for line in lines:
546 m = get_eapi.match(line)
547 if m:
548 # Once we hit the first EAPI line in this ebuild, stop processing.
549 # The spec requires that there only be one and it be first, so
550 # checking all possible values is pointless. We also assume that
551 # it's "the" EAPI line and not something in the middle of a heredoc.
552 eapi = m.group(1)
553 break
554
555 if eapi in BAD_EAPIS:
556 bad_ebuilds.append((ebuild, eapi))
557
558 if bad_ebuilds:
559 # pylint: disable=C0301
560 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
561 # pylint: enable=C0301
562 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500563 'These ebuilds are using old EAPIs. If these are imported from\n'
564 'Gentoo, then you may ignore and upload once with the --no-verify\n'
565 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500566 '\t%s\n'
567 'See this guide for more details:\n%s\n' %
568 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
569
570
Mike Frysinger89bdb852014-02-01 05:26:26 -0500571def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500572 """Make sure we use the new style KEYWORDS when possible in ebuilds.
573
574 If an ebuild generally does not care about the arch it is running on, then
575 ebuilds should flag it with one of:
576 KEYWORDS="*" # A stable ebuild.
577 KEYWORDS="~*" # An unstable ebuild.
578 KEYWORDS="-* ..." # Is known to only work on specific arches.
579
580 Args:
581 project: The project to look at
582 commit: The commit to look at
583
584 Returns:
585 A HookFailure or None.
586 """
587 WHITELIST = set(('*', '-*', '~*'))
588
589 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
590
Mike Frysinger89bdb852014-02-01 05:26:26 -0500591 ebuilds_re = [r'\.ebuild$']
592 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
593 ebuilds_re)
594
Mike Frysinger8d42d742014-09-22 15:50:21 -0400595 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500596 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400597 # We get the full content rather than a diff as the latter does not work
598 # on new files (like when adding new ebuilds).
599 lines = _get_file_content(ebuild, commit).splitlines()
600 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500601 m = get_keywords.match(line)
602 if m:
603 keywords = set(m.group(1).split())
604 if not keywords or WHITELIST - keywords != WHITELIST:
605 continue
606
Mike Frysinger8d42d742014-09-22 15:50:21 -0400607 bad_ebuilds.append(ebuild)
608
609 if bad_ebuilds:
610 return HookFailure(
611 '%s\n'
612 'Please update KEYWORDS to use a glob:\n'
613 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
614 ' KEYWORDS="*"\n'
615 'If the ebuild should be marked unstable (normal for '
616 'cros-workon / 9999 ebuilds):\n'
617 ' KEYWORDS="~*"\n'
618 'If the ebuild needs to be marked for only specific arches,'
619 'then use -* like so:\n'
620 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500621
622
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800623def _check_ebuild_licenses(_project, commit):
624 """Check if the LICENSE field in the ebuild is correct."""
625 affected_paths = _get_affected_files(commit)
626 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
627
628 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800629 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800630
631 for ebuild in touched_ebuilds:
632 # Skip virutal packages.
633 if ebuild.split('/')[-3] == 'virtual':
634 continue
635
636 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400637 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800638 except ValueError as e:
639 return HookFailure(e.message, [ebuild])
640
641 # Also ignore licenses ending with '?'
642 for license_type in [x for x in license_types
643 if x not in LICENSES_IGNORE and not x.endswith('?')]:
644 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400645 licenses_lib.Licensing.FindLicenseType(license_type)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800646 except AssertionError as e:
647 return HookFailure(e.message, [ebuild])
648
649
Mike Frysingercd363c82014-02-01 05:20:18 -0500650def _check_ebuild_virtual_pv(project, commit):
651 """Enforce the virtual PV policies."""
652 # If this is the portage-stable overlay, then ignore the check.
653 # We want to import virtuals as-is from upstream Gentoo.
654 whitelist = (
655 'chromiumos/overlays/portage-stable',
656 )
657 if project in whitelist:
658 return None
659
660 # We assume the repo name is the same as the dir name on disk.
661 # It would be dumb to not have them match though.
662 project = os.path.basename(project)
663
664 is_variant = lambda x: x.startswith('overlay-variant-')
665 is_board = lambda x: x.startswith('overlay-')
666 is_private = lambda x: x.endswith('-private')
667
668 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
669
670 ebuilds_re = [r'\.ebuild$']
671 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
672 ebuilds_re)
673 bad_ebuilds = []
674
675 for ebuild in ebuilds:
676 m = get_pv.match(ebuild)
677 if m:
678 overlay = m.group(1)
679 if not overlay or not is_board(overlay):
680 overlay = project
681
682 pv = m.group(3).split('-', 1)[0]
683
684 if is_private(overlay):
685 want_pv = '3.5' if is_variant(overlay) else '3'
686 elif is_board(overlay):
687 want_pv = '2.5' if is_variant(overlay) else '2'
688 else:
689 want_pv = '1'
690
691 if pv != want_pv:
692 bad_ebuilds.append((ebuild, pv, want_pv))
693
694 if bad_ebuilds:
695 # pylint: disable=C0301
696 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
697 # pylint: enable=C0301
698 return HookFailure(
699 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
700 '\t%s\n'
701 'If this is an upstream Gentoo virtual, then you may ignore this\n'
702 'check (and re-run w/--no-verify). Otherwise, please see this\n'
703 'page for more details:\n%s\n' %
704 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
705 for x in bad_ebuilds]), url))
706
707
Mike Frysingerae409522014-02-01 03:16:11 -0500708def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700709 """Verify that Change-ID is present in last paragraph of commit message."""
710 desc = _get_commit_desc(commit)
711 loc = desc.rfind('\nChange-Id:')
712 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700713 return HookFailure('Change-Id must be in last paragraph of description.')
714
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700715
Mike Frysingerae409522014-02-01 03:16:11 -0500716def _check_license(_project, commit):
Mike Frysinger98638102014-08-28 00:15:08 -0400717 """Verifies the license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700718
Mike Frysinger98638102014-08-28 00:15:08 -0400719 Should be following the spec:
720 http://dev.chromium.org/developers/coding-style#TOC-File-headers
721 """
722 # For older years, be a bit more flexible as our policy says leave them be.
723 LICENSE_HEADER = (
724 r'.* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
725 'All rights reserved\.' '\n'
726 r'.* Use of this source code is governed by a BSD-style license that can '
727 'be\n'
728 r'.* found in the LICENSE file\.'
729 '\n'
730 )
731 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
732
733 # For newer years, be stricter.
734 COPYRIGHT_LINE = (
735 r'.* Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
736 'All rights reserved\.' '\n'
737 )
738 copyright_re = re.compile(COPYRIGHT_LINE)
739
740 bad_files = []
741 bad_copyright_files = []
742 files = _filter_files(_get_affected_files(commit, relative=True),
743 COMMON_INCLUDED_PATHS,
744 COMMON_EXCLUDED_PATHS)
745
746 for f in files:
747 contents = _get_file_content(f, commit)
748 if not contents:
749 # Ignore empty files.
750 continue
751
752 if not license_re.search(contents):
753 bad_files.append(f)
754 elif copyright_re.search(contents):
755 bad_copyright_files.append(f)
756
757 if bad_files:
758 msg = '%s:\n%s\n%s' % (
759 'License must match', license_re.pattern,
760 'Found a bad header in these files:')
761 return HookFailure(msg, bad_files)
762
763 if bad_copyright_files:
764 msg = 'Do not use (c) in copyright headers in new files:'
765 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700766
767
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400768def _check_layout_conf(_project, commit):
769 """Verifies the metadata/layout.conf file."""
770 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400771 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400772 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400773 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400774
Mike Frysinger94a670c2014-09-19 12:46:26 -0400775 # Handle multiple overlays in a single commit (like the public tree).
776 for f in _get_affected_files(commit, relative=True):
777 if f.endswith(repo_name):
778 repo_names.append(f)
779 elif f.endswith(layout_path):
780 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400781
782 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -0400783 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400784 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -0400785 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400786
Mike Frysinger94a670c2014-09-19 12:46:26 -0400787 # Gather all the errors in one pass so we show one full message.
788 all_errors = {}
789 for layout_path in layout_paths:
790 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400791
Mike Frysinger94a670c2014-09-19 12:46:26 -0400792 # Make sure the config file is sorted.
793 data = [x for x in _get_file_content(layout_path, commit).splitlines()
794 if x and x[0] != '#']
795 if sorted(data) != data:
796 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400797
Mike Frysinger94a670c2014-09-19 12:46:26 -0400798 # Require people to set specific values all the time.
799 settings = (
800 # TODO: Enable this for everyone. http://crbug.com/408038
801 #('fast caching', 'cache-format = md5-dict'),
802 ('fast manifests', 'thin-manifests = true'),
803 ('extra features', 'profile-formats = portage-2'),
804 )
805 for reason, line in settings:
806 if line not in data:
807 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400808
Mike Frysinger94a670c2014-09-19 12:46:26 -0400809 # Require one of these settings.
810 if ('use-manifests = true' not in data and
811 'use-manifests = strict' not in data):
812 errors += ['enable file checking with: use-manifests = true']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400813
Mike Frysinger94a670c2014-09-19 12:46:26 -0400814 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -0400815 for line in data:
816 if line.startswith('repo-name = '):
817 break
818 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -0400819 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400820
Mike Frysinger94a670c2014-09-19 12:46:26 -0400821 # Summarize all the errors we saw (if any).
822 lines = ''
823 for layout_path, errors in all_errors.items():
824 if errors:
825 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
826 if lines:
827 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
828 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400829
830
Ryan Cuiec4d6332011-05-02 14:15:25 -0700831# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700832
Ryan Cui1562fb82011-05-09 11:01:31 -0700833
Mike Frysingerae409522014-02-01 03:16:11 -0500834def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700835 """Runs checkpatch.pl on the given project"""
836 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700837 options = list(options)
838 if commit == PRE_SUBMIT:
839 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
840 # this case.
841 options.append('--ignore=MISSING_SIGN_OFF')
842 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700843 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700844 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700845 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700846 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700847
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700848
Anton Staaf815d6852011-08-22 10:08:45 -0700849def _run_checkpatch_no_tree(project, commit):
850 return _run_checkpatch(project, commit, ['--no-tree'])
851
Mike Frysingerae409522014-02-01 03:16:11 -0500852
Randall Spangler7318fd62013-11-21 12:16:58 -0800853def _run_checkpatch_ec(project, commit):
854 """Runs checkpatch with options for Chromium EC projects."""
855 return _run_checkpatch(project, commit, ['--no-tree',
856 '--ignore=MSLEEP,VOLATILE'])
857
Mike Frysingerae409522014-02-01 03:16:11 -0500858
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700859def _run_checkpatch_depthcharge(project, commit):
860 """Runs checkpatch with options for depthcharge."""
861 return _run_checkpatch(project, commit, [
862 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700863 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
864 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
865 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700866
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700867def _run_checkpatch_coreboot(project, commit):
868 """Runs checkpatch with options for coreboot."""
869 return _run_checkpatch(project, commit, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700870 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700871 '--no-tree',
872 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
Shawn Nematbakhsh6442c582014-08-22 14:32:06 -0700873 'GLOBAL_INITIALISERS,INITIALISED_STATIC,C99_COMMENTS'])
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700874
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700875
Mike Frysingerae409522014-02-01 03:16:11 -0500876def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700877 """Makes sure kernel config changes are not mixed with code changes"""
878 files = _get_affected_files(commit)
879 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
880 return HookFailure('Changes to chromeos/config/ and regular files must '
881 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700882
Mike Frysingerae409522014-02-01 03:16:11 -0500883
884def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700885 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700886 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700887 try:
888 json.load(open(f))
889 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700890 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700891
892
Mike Frysingerae409522014-02-01 03:16:11 -0500893def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400894 """Make sure Manifest files only have DIST lines"""
895 paths = []
896
897 for path in _get_affected_files(commit):
898 if os.path.basename(path) != 'Manifest':
899 continue
900 if not os.path.exists(path):
901 continue
902
903 with open(path, 'r') as f:
904 for line in f.readlines():
905 if not line.startswith('DIST '):
906 paths.append(path)
907 break
908
909 if paths:
910 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
911 ('\n'.join(paths),))
912
913
Mike Frysingerae409522014-02-01 03:16:11 -0500914def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700915 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700916 if commit == PRE_SUBMIT:
917 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700918 BRANCH_RE = r'\nBRANCH=\S+'
919
920 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
921 msg = ('Changelist description needs BRANCH field (after first line)\n'
922 'E.g. BRANCH=none or BRANCH=link,snow')
923 return HookFailure(msg)
924
925
Mike Frysingerae409522014-02-01 03:16:11 -0500926def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800927 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700928 if commit == PRE_SUBMIT:
929 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800930 SIGNOFF_RE = r'\nSigned-off-by: \S+'
931
932 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
933 msg = ('Changelist description needs Signed-off-by: field\n'
934 'E.g. Signed-off-by: My Name <me@chromium.org>')
935 return HookFailure(msg)
936
937
Jon Salz3ee59de2012-08-18 13:54:22 +0800938def _run_project_hook_script(script, project, commit):
939 """Runs a project hook script.
940
941 The script is run with the following environment variables set:
942 PRESUBMIT_PROJECT: The affected project
943 PRESUBMIT_COMMIT: The affected commit
944 PRESUBMIT_FILES: A newline-separated list of affected files
945
946 The script is considered to fail if the exit code is non-zero. It should
947 write an error message to stdout.
948 """
949 env = dict(os.environ)
950 env['PRESUBMIT_PROJECT'] = project
951 env['PRESUBMIT_COMMIT'] = commit
952
953 # Put affected files in an environment variable
954 files = _get_affected_files(commit)
955 env['PRESUBMIT_FILES'] = '\n'.join(files)
956
957 process = subprocess.Popen(script, env=env, shell=True,
958 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800959 stdout=subprocess.PIPE,
960 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800961 stdout, _ = process.communicate()
962 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800963 if stdout:
964 stdout = re.sub('(?m)^', ' ', stdout)
965 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800966 (script, process.returncode,
967 ':\n' + stdout if stdout else ''))
968
969
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700970def _moved_to_platform2(project, _commit):
971 """Forbids commits to legacy repo in src/platform."""
972 return HookFailure('%s has been moved to platform2. This change should be '
973 'made there.' % project)
974
975
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700976def _check_project_prefix(_project, commit):
977 """Fails if the change is project specific and the commit message is not
978 prefixed by the project_name.
979 """
980
981 files = _get_affected_files(commit, relative=True)
982 prefix = os.path.commonprefix(files)
983 prefix = os.path.dirname(prefix)
984
985 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -0700986 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700987 return
988
989 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -0700990
991 # The common files may all be within a subdirectory of the main project
992 # directory, so walk up the tree until we find an alias file.
993 # _get_affected_files() should return relative paths, but check against '/' to
994 # ensure that this loop terminates even if it receives an absolute path.
995 while prefix and prefix != '/':
996 alias_file = os.path.join(prefix, '.project_alias')
997
998 # If an alias exists, use it.
999 if os.path.isfile(alias_file):
1000 project_name = osutils.ReadFile(alias_file).strip()
1001
1002 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001003
1004 if not _get_commit_desc(commit).startswith(project_name + ': '):
1005 return HookFailure('The commit title for changes affecting only %s'
1006 ' should start with \"%s: \"'
1007 % (project_name, project_name))
1008
1009
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001010# Base
1011
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001012# A list of hooks which are not project specific and check patch description
1013# (as opposed to patch body).
1014_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001015 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001016 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001017 _check_change_has_test_field,
1018 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001019]
1020
1021
1022# A list of hooks that are not project-specific
1023_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001024 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001025 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001026 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001027 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -07001028 _check_no_stray_whitespace,
1029 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001030 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001031 _check_license,
1032 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001033 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001034]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001035
Ryan Cui1562fb82011-05-09 11:01:31 -07001036
Ryan Cui9b651632011-05-11 11:38:58 -07001037# A dictionary of project-specific hooks(callbacks), indexed by project name.
1038# dict[project] = [callback1, callback2]
1039_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001040 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001041 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1042 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001043 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001044 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001045 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001046 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1047 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001048 "chromiumos/overlays/board-overlays": [_check_manifests],
1049 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1050 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001051 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury42052852014-10-06 16:02:38 -07001052 "chromiumos/platform/depthcharge": [_check_change_has_branch_field,
1053 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001054 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001055 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001056 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001057 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001058 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury42052852014-10-06 16:02:38 -07001059 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
1060 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001061 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001062 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001063 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1064 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1065 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001066 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001067}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001068
Ryan Cui1562fb82011-05-09 11:01:31 -07001069
Ryan Cui9b651632011-05-11 11:38:58 -07001070# A dictionary of flags (keys) that can appear in the config file, and the hook
1071# that the flag disables (value)
1072_DISABLE_FLAGS = {
1073 'stray_whitespace_check': _check_no_stray_whitespace,
1074 'long_line_check': _check_no_long_lines,
1075 'cros_license_check': _check_license,
1076 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001077 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001078 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001079 'bug_field_check': _check_change_has_bug_field,
1080 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001081}
1082
1083
Jon Salz3ee59de2012-08-18 13:54:22 +08001084def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001085 """Returns a set of hooks disabled by the current project's config file.
1086
1087 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001088
1089 Args:
1090 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001091 """
1092 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001093 if not config.has_section(SECTION):
1094 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001095
1096 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001097 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001098 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001099 if not config.getboolean(SECTION, flag):
1100 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001101 except ValueError as e:
1102 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001103 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001104
1105 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1106 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1107
1108
Jon Salz3ee59de2012-08-18 13:54:22 +08001109def _get_project_hook_scripts(config):
1110 """Returns a list of project-specific hook scripts.
1111
1112 Args:
1113 config: A ConfigParser for the project's config file.
1114 """
1115 SECTION = 'Hook Scripts'
1116 if not config.has_section(SECTION):
1117 return []
1118
1119 hook_names_values = config.items(SECTION)
1120 hook_names_values.sort(key=lambda x: x[0])
1121 return [x[1] for x in hook_names_values]
1122
1123
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001124def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001125 """Returns a list of hooks that need to be run for a project.
1126
1127 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001128
1129 Args:
1130 project: A string, name of the project.
1131 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001132 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001133 config = ConfigParser.RawConfigParser()
1134 try:
1135 config.read(_CONFIG_FILE)
1136 except ConfigParser.Error:
1137 # Just use an empty config file
1138 config = ConfigParser.RawConfigParser()
1139
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001140 if presubmit:
1141 hook_list = _COMMON_HOOKS
1142 else:
1143 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1144
Jon Salz3ee59de2012-08-18 13:54:22 +08001145 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001146 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001147
1148 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001149 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1150 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001151
Jon Salz3ee59de2012-08-18 13:54:22 +08001152 for script in _get_project_hook_scripts(config):
1153 hooks.append(functools.partial(_run_project_hook_script, script))
1154
Ryan Cui9b651632011-05-11 11:38:58 -07001155 return hooks
1156
1157
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001158def _run_project_hooks(project, proj_dir=None,
1159 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001160 """For each project run its project specific hook from the hooks dictionary.
1161
1162 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001163 project: The name of project to run hooks for.
1164 proj_dir: If non-None, this is the directory the project is in. If None,
1165 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001166 commit_list: A list of commits to run hooks against. If None or empty list
1167 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001168 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001169
1170 Returns:
1171 Boolean value of whether any errors were ecountered while running the hooks.
1172 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001173 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001174 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1175 if len(proj_dirs) == 0:
1176 print('%s cannot be found.' % project, file=sys.stderr)
1177 print('Please specify a valid project.', file=sys.stderr)
1178 return True
1179 if len(proj_dirs) > 1:
1180 print('%s is associated with multiple directories.' % project,
1181 file=sys.stderr)
1182 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1183 return True
1184 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001185
Ryan Cuiec4d6332011-05-02 14:15:25 -07001186 pwd = os.getcwd()
1187 # hooks assume they are run from the root of the project
1188 os.chdir(proj_dir)
1189
Doug Anderson14749562013-06-26 13:38:29 -07001190 if not commit_list:
1191 try:
1192 commit_list = _get_commits()
1193 except VerifyException as e:
1194 PrintErrorForProject(project, HookFailure(str(e)))
1195 os.chdir(pwd)
1196 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001197
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001198 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001199 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001200 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001201 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001202 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001203 hook_error = hook(project, commit)
1204 if hook_error:
1205 error_list.append(hook_error)
1206 error_found = True
1207 if error_list:
1208 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1209 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001210
Ryan Cuiec4d6332011-05-02 14:15:25 -07001211 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001212 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001213
Mike Frysingerae409522014-02-01 03:16:11 -05001214
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001215# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001216
Ryan Cui1562fb82011-05-09 11:01:31 -07001217
Mike Frysingerae409522014-02-01 03:16:11 -05001218def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001219 """Main function invoked directly by repo.
1220
1221 This function will exit directly upon error so that repo doesn't print some
1222 obscure error message.
1223
1224 Args:
1225 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001226 worktree_list: A list of directories. It should be the same length as
1227 project_list, so that each entry in project_list matches with a directory
1228 in worktree_list. If None, we will attempt to calculate the directories
1229 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001230 kwargs: Leave this here for forward-compatibility.
1231 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001232 found_error = False
David James2edd9002013-10-11 14:09:19 -07001233 if not worktree_list:
1234 worktree_list = [None] * len(project_list)
1235 for project, worktree in zip(project_list, worktree_list):
1236 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001237 found_error = True
1238
Mike Frysingerae409522014-02-01 03:16:11 -05001239 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001240 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001241 '- To disable some source style checks, and for other hints, see '
1242 '<checkout_dir>/src/repohooks/README\n'
1243 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001244 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001245 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001246
Ryan Cui1562fb82011-05-09 11:01:31 -07001247
Doug Anderson44a644f2011-11-02 10:37:37 -07001248def _identify_project(path):
1249 """Identify the repo project associated with the given path.
1250
1251 Returns:
1252 A string indicating what project is associated with the path passed in or
1253 a blank string upon failure.
1254 """
1255 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1256 stderr=subprocess.PIPE, cwd=path).strip()
1257
1258
1259def direct_main(args, verbose=False):
1260 """Run hooks directly (outside of the context of repo).
1261
1262 # Setup for doctests below.
1263 # ...note that some tests assume that running pre-upload on this CWD is fine.
1264 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1265 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1266 >>> olddir = os.getcwd()
1267
1268 # OK to run w/ no arugments; will run with CWD.
1269 >>> os.chdir(mydir)
1270 >>> direct_main(['prog_name'], verbose=True)
1271 Running hooks on chromiumos/repohooks
1272 0
1273 >>> os.chdir(olddir)
1274
1275 # Run specifying a dir
1276 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1277 Running hooks on chromiumos/repohooks
1278 0
1279
1280 # Not a problem to use a bogus project; we'll just get default settings.
1281 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1282 Running hooks on X
1283 0
1284
1285 # Run with project but no dir
1286 >>> os.chdir(mydir)
1287 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1288 Running hooks on X
1289 0
1290 >>> os.chdir(olddir)
1291
1292 # Try with a non-git CWD
1293 >>> os.chdir('/tmp')
1294 >>> direct_main(['prog_name'])
1295 Traceback (most recent call last):
1296 ...
1297 BadInvocation: The current directory is not part of a git project.
1298
1299 # Check various bad arguments...
1300 >>> direct_main(['prog_name', 'bogus'])
1301 Traceback (most recent call last):
1302 ...
1303 BadInvocation: Unexpected arguments: bogus
1304 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1305 Traceback (most recent call last):
1306 ...
1307 BadInvocation: Invalid dir: bogusdir
1308 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1309 Traceback (most recent call last):
1310 ...
1311 BadInvocation: Not a git directory: /tmp
1312
1313 Args:
1314 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001315 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001316
1317 Returns:
1318 0 if no pre-upload failures, 1 if failures.
1319
1320 Raises:
1321 BadInvocation: On some types of invocation errors.
1322 """
1323 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1324 parser = optparse.OptionParser(description=desc)
1325
1326 parser.add_option('--dir', default=None,
1327 help='The directory that the project lives in. If not '
1328 'specified, use the git project root based on the cwd.')
1329 parser.add_option('--project', default=None,
1330 help='The project repo path; this can affect how the hooks '
1331 'get run, since some hooks are project-specific. For '
1332 'chromite this is chromiumos/chromite. If not specified, '
1333 'the repo tool will be used to figure this out based on '
1334 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001335 parser.add_option('--rerun-since', default=None,
1336 help='Rerun hooks on old commits since the given date. '
1337 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001338 'e.g. 2012-06-20. This option is mutually exclusive '
1339 'with --pre-submit.')
1340 parser.add_option('--pre-submit', action="store_true",
1341 help='Run the check against the pending commit. '
1342 'This option should be used at the \'git commit\' '
1343 'phase as opposed to \'repo upload\'. This option '
1344 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001345
1346 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001347
1348 opts, args = parser.parse_args(args[1:])
1349
Doug Anderson14749562013-06-26 13:38:29 -07001350 if opts.rerun_since:
1351 if args:
1352 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1353 ' '.join(args))
1354
1355 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1356 all_commits = _run_command(cmd).splitlines()
1357 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1358
1359 # Eliminate chrome-bot commits but keep ordering the same...
1360 bot_commits = set(bot_commits)
1361 args = [c for c in all_commits if c not in bot_commits]
1362
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001363 if opts.pre_submit:
1364 raise BadInvocation('rerun-since and pre-submit can not be '
1365 'used together')
1366 if opts.pre_submit:
1367 if args:
1368 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1369 ' '.join(args))
1370 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001371
1372 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1373 # project from CWD
1374 if opts.dir is None:
1375 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1376 stderr=subprocess.PIPE).strip()
1377 if not git_dir:
1378 raise BadInvocation('The current directory is not part of a git project.')
1379 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1380 elif not os.path.isdir(opts.dir):
1381 raise BadInvocation('Invalid dir: %s' % opts.dir)
1382 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1383 raise BadInvocation('Not a git directory: %s' % opts.dir)
1384
1385 # Identify the project if it wasn't specified; this _requires_ the repo
1386 # tool to be installed and for the project to be part of a repo checkout.
1387 if not opts.project:
1388 opts.project = _identify_project(opts.dir)
1389 if not opts.project:
1390 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1391
1392 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001393 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001394
Doug Anderson14749562013-06-26 13:38:29 -07001395 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001396 commit_list=args,
1397 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001398 if found_error:
1399 return 1
1400 return 0
1401
1402
1403def _test():
1404 """Run any built-in tests."""
1405 import doctest
1406 doctest.testmod()
1407
1408
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001409if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001410 if sys.argv[1:2] == ["--test"]:
1411 _test()
1412 exit_code = 0
1413 else:
1414 prog_name = os.path.basename(sys.argv[0])
1415 try:
1416 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001417 except BadInvocation, err:
1418 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001419 exit_code = 1
1420 sys.exit(exit_code)