blob: a619caca029b2a2f38dabe65a8d2d8100e84d8b9 [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
Peter Ammon811f6702014-06-12 15:45:38 -070020import stat
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050021import subprocess
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
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050032from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070033from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070034from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040035from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070036
Vadim Bendebury2b62d742014-06-22 13:14:51 -070037PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070038
39COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050040 # C++ and friends
41 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
42 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
43 # Scripts
44 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
45 # No extension at all, note that ALL CAPS files are black listed in
46 # COMMON_EXCLUDED_LIST below.
47 r"(^|.*[\\\/])[^.]+$",
48 # Other
49 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070050]
51
Ryan Cui1562fb82011-05-09 11:01:31 -070052
Ryan Cuiec4d6332011-05-02 14:15:25 -070053COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050054 # avoid doing source file checks for kernel
55 r"/src/third_party/kernel/",
56 r"/src/third_party/kernel-next/",
57 r"/src/third_party/ktop/",
58 r"/src/third_party/punybench/",
59 r".*\bexperimental[\\\/].*",
60 r".*\b[A-Z0-9_]{2,}$",
61 r".*[\\\/]debian[\\\/]rules$",
62 # for ebuild trees, ignore any caches and manifest data
63 r".*/Manifest$",
64 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070065
Mike Frysingerae409522014-02-01 03:16:11 -050066 # ignore profiles data (like overlay-tegra2/profiles)
Mike Frysinger94a670c2014-09-19 12:46:26 -040067 r"(^|.*/)overlay-.*/profiles/.*",
Mike Frysinger98638102014-08-28 00:15:08 -040068 r"^profiles/.*$",
69
Mike Frysingerae409522014-02-01 03:16:11 -050070 # ignore minified js and jquery
71 r".*\.min\.js",
72 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050073
74 # Ignore license files as the content is often taken verbatim.
75 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070076]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070077
Ryan Cui1562fb82011-05-09 11:01:31 -070078
Ryan Cui9b651632011-05-11 11:38:58 -070079_CONFIG_FILE = 'PRESUBMIT.cfg'
80
81
Doug Anderson44a644f2011-11-02 10:37:37 -070082# Exceptions
83
84
85class BadInvocation(Exception):
86 """An Exception indicating a bad invocation of the program."""
87 pass
88
89
Ryan Cui1562fb82011-05-09 11:01:31 -070090# General Helpers
91
Sean Paulba01d402011-05-05 11:36:23 -040092
Doug Anderson44a644f2011-11-02 10:37:37 -070093def _run_command(cmd, cwd=None, stderr=None):
94 """Executes the passed in command and returns raw stdout output.
95
96 Args:
97 cmd: The command to run; should be a list of strings.
98 cwd: The directory to switch to for running the command.
99 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
100 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
101
102 Returns:
103 The standard out from the process.
104 """
105 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
106 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700107
Ryan Cui1562fb82011-05-09 11:01:31 -0700108
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700109def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700110 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700111 if __name__ == '__main__':
112 # Works when file is run on its own (__file__ is defined)...
113 return os.path.abspath(os.path.dirname(__file__))
114 else:
115 # We need to do this when we're run through repo. Since repo executes
116 # us with execfile(), we don't get __file__ defined.
117 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
118 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700119
Ryan Cui1562fb82011-05-09 11:01:31 -0700120
Ryan Cuiec4d6332011-05-02 14:15:25 -0700121def _match_regex_list(subject, expressions):
122 """Try to match a list of regular expressions to a string.
123
124 Args:
125 subject: The string to match regexes on
126 expressions: A list of regular expressions to check for matches with.
127
128 Returns:
129 Whether the passed in subject matches any of the passed in regexes.
130 """
131 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500132 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700133 return True
134 return False
135
Ryan Cui1562fb82011-05-09 11:01:31 -0700136
Mike Frysingerae409522014-02-01 03:16:11 -0500137def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700138 """Filter out files based on the conditions passed in.
139
140 Args:
141 files: list of filepaths to filter
142 include_list: list of regex that when matched with a file path will cause it
143 to be added to the output list unless the file is also matched with a
144 regex in the exclude_list.
145 exclude_list: list of regex that when matched with a file will prevent it
146 from being added to the output list, even if it is also matched with a
147 regex in the include_list.
148
149 Returns:
150 A list of filepaths that contain files matched in the include_list and not
151 in the exclude_list.
152 """
153 filtered = []
154 for f in files:
155 if (_match_regex_list(f, include_list) and
156 not _match_regex_list(f, exclude_list)):
157 filtered.append(f)
158 return filtered
159
Ryan Cuiec4d6332011-05-02 14:15:25 -0700160
161# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700162
163
Ryan Cui4725d952011-05-05 15:41:19 -0700164def _get_upstream_branch():
165 """Returns the upstream tracking branch of the current branch.
166
167 Raises:
168 Error if there is no tracking branch
169 """
170 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
171 current_branch = current_branch.replace('refs/heads/', '')
172 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700173 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700174
175 cfg_option = 'branch.' + current_branch + '.%s'
176 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
177 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
178 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700179 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700180
181 return full_upstream.replace('heads', 'remotes/' + remote)
182
Ryan Cui1562fb82011-05-09 11:01:31 -0700183
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700184def _get_patch(commit):
185 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700186 if commit == PRE_SUBMIT:
187 return _run_command(['git', 'diff', '--cached', 'HEAD'])
188 else:
189 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700190
Ryan Cui1562fb82011-05-09 11:01:31 -0700191
Jon Salz98255932012-08-18 14:48:02 +0800192def _try_utf8_decode(data):
193 """Attempts to decode a string as UTF-8.
194
195 Returns:
196 The decoded Unicode object, or the original string if parsing fails.
197 """
198 try:
199 return unicode(data, 'utf-8', 'strict')
200 except UnicodeDecodeError:
201 return data
202
203
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500204def _get_file_content(path, commit):
205 """Returns the content of a file at a specific commit.
206
207 We can't rely on the file as it exists in the filesystem as people might be
208 uploading a series of changes which modifies the file multiple times.
209
210 Note: The "content" of a symlink is just the target. So if you're expecting
211 a full file, you should check that first. One way to detect is that the
212 content will not have any newlines.
213 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700214 if commit == PRE_SUBMIT:
215 return _run_command(['git', 'diff', 'HEAD', path])
216 else:
217 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500218
219
Mike Frysingerae409522014-02-01 03:16:11 -0500220def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700221 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700222 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff']
223 if commit == PRE_SUBMIT:
224 command += ['HEAD', path]
225 else:
226 command += [commit, path]
227 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700228
229 new_lines = []
230 line_num = 0
231 for line in output.splitlines():
232 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
233 if m:
234 line_num = int(m.groups(1)[0])
235 continue
236 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800237 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700238 if not line.startswith('-'):
239 line_num += 1
240 return new_lines
241
Ryan Cui1562fb82011-05-09 11:01:31 -0700242
Peter Ammon811f6702014-06-12 15:45:38 -0700243def _get_affected_files(commit, include_deletes=False, relative=False):
244 """Returns list of file paths that were modified/added, excluding symlinks.
245
246 Args:
247 commit: The commit
248 include_deletes: If true, we'll include deleted files in the result
249 relative: Whether to return relative or full paths to files
250
251 Returns:
252 A list of modified/added (and perhaps deleted) files
253 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700254 if commit == PRE_SUBMIT:
255 return _run_command(['git', 'diff-index', '--cached',
256 '--name-only', 'HEAD']).split()
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500257
258 path = os.getcwd()
259 files = git.RawDiff(path, '%s^!' % commit)
260
261 # Filter out symlinks.
262 files = [x for x in files if not stat.S_ISLNK(int(x.dst_mode, 8))]
263
264 if not include_deletes:
265 files = [x for x in files if x.status != 'D']
266
267 # Caller only cares about filenames.
268 files = [x.dst_file if x.dst_file else x.src_file for x in files]
269 if relative:
270 return files
271 else:
272 return [os.path.join(path, x) for x in files]
Peter Ammon811f6702014-06-12 15:45:38 -0700273
274
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700275def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700276 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700277 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700278 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700279
Ryan Cui1562fb82011-05-09 11:01:31 -0700280
Ryan Cuiec4d6332011-05-02 14:15:25 -0700281def _get_commit_desc(commit):
282 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700283 if commit == PRE_SUBMIT:
284 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400285 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700286
287
288# Common Hooks
289
Ryan Cui1562fb82011-05-09 11:01:31 -0700290
Mike Frysingerae409522014-02-01 03:16:11 -0500291def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700292 """Checks that there aren't any lines longer than maxlen characters in any of
293 the text files to be submitted.
294 """
295 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800296 SKIP_REGEXP = re.compile('|'.join([
297 r'https?://',
298 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700299
300 errors = []
301 files = _filter_files(_get_affected_files(commit),
302 COMMON_INCLUDED_PATHS,
303 COMMON_EXCLUDED_PATHS)
304
305 for afile in files:
306 for line_num, line in _get_file_diff(afile, commit):
307 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500308 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800309 continue
310
311 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
312 if len(errors) == 5: # Just show the first 5 errors.
313 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700314
315 if errors:
316 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700317 return HookFailure(msg, errors)
318
Ryan Cuiec4d6332011-05-02 14:15:25 -0700319
Mike Frysingerae409522014-02-01 03:16:11 -0500320def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700321 """Checks that there is no stray whitespace at source lines end."""
322 errors = []
323 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500324 COMMON_INCLUDED_PATHS,
325 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700326 for afile in files:
327 for line_num, line in _get_file_diff(afile, commit):
328 if line.rstrip() != line:
329 errors.append('%s, line %s' % (afile, line_num))
330 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700331 return HookFailure('Found line ending with white space in:', errors)
332
Ryan Cuiec4d6332011-05-02 14:15:25 -0700333
Mike Frysingerae409522014-02-01 03:16:11 -0500334def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700335 """Checks there are no unexpanded tabs."""
336 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700337 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700338 r".*\.ebuild$",
339 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500340 r".*/[M|m]akefile$",
341 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700342 ]
343
344 errors = []
345 files = _filter_files(_get_affected_files(commit),
346 COMMON_INCLUDED_PATHS,
347 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
348
349 for afile in files:
350 for line_num, line in _get_file_diff(afile, commit):
351 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500352 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700353 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700354 return HookFailure('Found a tab character in:', errors)
355
Ryan Cuiec4d6332011-05-02 14:15:25 -0700356
Mike Frysingerae409522014-02-01 03:16:11 -0500357def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700358 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700359 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700360
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700361 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700362 msg = 'Changelist description needs TEST field (after first line)'
363 return HookFailure(msg)
364
Ryan Cuiec4d6332011-05-02 14:15:25 -0700365
Mike Frysingerae409522014-02-01 03:16:11 -0500366def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700367 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
368 msg = 'Changelist has invalid CQ-DEPEND target.'
369 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
370 try:
371 patch.GetPaladinDeps(_get_commit_desc(commit))
372 except ValueError as ex:
373 return HookFailure(msg, [example, str(ex)])
374
375
Mike Frysingerae409522014-02-01 03:16:11 -0500376def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700377 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700378 OLD_BUG_RE = r'\nBUG=.*chromium-os'
379 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
380 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
381 'the chromium tracker in your BUG= line now.')
382 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700383
David James5c0073d2013-04-03 08:48:52 -0700384 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700385 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700386 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700387 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700388 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
389 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700390 return HookFailure(msg)
391
Ryan Cuiec4d6332011-05-02 14:15:25 -0700392
Doug Anderson42b8a052013-06-26 10:45:36 -0700393def _check_for_uprev(project, commit):
394 """Check that we're not missing a revbump of an ebuild in the given commit.
395
396 If the given commit touches files in a directory that has ebuilds somewhere
397 up the directory hierarchy, it's very likely that we need an ebuild revbump
398 in order for those changes to take effect.
399
400 It's not totally trivial to detect a revbump, so at least detect that an
401 ebuild with a revision number in it was touched. This should handle the
402 common case where we use a symlink to do the revbump.
403
404 TODO: it would be nice to enhance this hook to:
405 * Handle cases where people revbump with a slightly different syntax. I see
406 one ebuild (puppy) that revbumps with _pN. This is a false positive.
407 * Catches cases where people aren't using symlinks for revbumps. If they
408 edit a revisioned file directly (and are expected to rename it for revbump)
409 we'll miss that. Perhaps we could detect that the file touched is a
410 symlink?
411
412 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
413 still better off than without this check.
414
415 Args:
416 project: The project to look at
417 commit: The commit to look at
418
419 Returns:
420 A HookFailure or None.
421 """
Mike Frysinger011af942014-01-17 16:12:22 -0500422 # If this is the portage-stable overlay, then ignore the check. It's rare
423 # that we're doing anything other than importing files from upstream, so
424 # forcing a rev bump makes no sense.
425 whitelist = (
426 'chromiumos/overlays/portage-stable',
427 )
428 if project in whitelist:
429 return None
430
Doug Anderson42b8a052013-06-26 10:45:36 -0700431 affected_paths = _get_affected_files(commit, include_deletes=True)
432
433 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500434 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700435 affected_paths = [path for path in affected_paths
436 if os.path.basename(path) not in whitelist]
437 if not affected_paths:
438 return None
439
440 # If we've touched any file named with a -rN.ebuild then we'll say we're
441 # OK right away. See TODO above about enhancing this.
442 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
443 for path in affected_paths)
444 if touched_revved_ebuild:
445 return None
446
447 # We want to examine the current contents of all directories that are parents
448 # of files that were touched (up to the top of the project).
449 #
450 # ...note: we use the current directory contents even though it may have
451 # changed since the commit we're looking at. This is just a heuristic after
452 # all. Worst case we don't flag a missing revbump.
453 project_top = os.getcwd()
454 dirs_to_check = set([project_top])
455 for path in affected_paths:
456 path = os.path.dirname(path)
457 while os.path.exists(path) and not os.path.samefile(path, project_top):
458 dirs_to_check.add(path)
459 path = os.path.dirname(path)
460
461 # Look through each directory. If it's got an ebuild in it then we'll
462 # consider this as a case when we need a revbump.
463 for dir_path in dirs_to_check:
464 contents = os.listdir(dir_path)
465 ebuilds = [os.path.join(dir_path, path)
466 for path in contents if path.endswith('.ebuild')]
467 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
468
469 # If the -9999.ebuild file was touched the bot will uprev for us.
470 # ...we'll use a simple intersection here as a heuristic...
471 if set(ebuilds_9999) & set(affected_paths):
472 continue
473
474 if ebuilds:
475 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
476 'or a -r1.ebuild symlink if this is a new ebuild')
477
478 return None
479
480
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500481def _check_ebuild_eapi(project, commit):
482 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
483
484 We want to get away from older EAPI's as it makes life confusing and they
485 have less builtin error checking.
486
487 Args:
488 project: The project to look at
489 commit: The commit to look at
490
491 Returns:
492 A HookFailure or None.
493 """
494 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500495 # that we're doing anything other than importing files from upstream, and
496 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500497 whitelist = (
498 'chromiumos/overlays/portage-stable',
499 )
500 if project in whitelist:
501 return None
502
503 BAD_EAPIS = ('0', '1', '2', '3')
504
505 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
506
507 ebuilds_re = [r'\.ebuild$']
508 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
509 ebuilds_re)
510 bad_ebuilds = []
511
512 for ebuild in ebuilds:
513 # If the ebuild does not specify an EAPI, it defaults to 0.
514 eapi = '0'
515
516 lines = _get_file_content(ebuild, commit).splitlines()
517 if len(lines) == 1:
518 # This is most likely a symlink, so skip it entirely.
519 continue
520
521 for line in lines:
522 m = get_eapi.match(line)
523 if m:
524 # Once we hit the first EAPI line in this ebuild, stop processing.
525 # The spec requires that there only be one and it be first, so
526 # checking all possible values is pointless. We also assume that
527 # it's "the" EAPI line and not something in the middle of a heredoc.
528 eapi = m.group(1)
529 break
530
531 if eapi in BAD_EAPIS:
532 bad_ebuilds.append((ebuild, eapi))
533
534 if bad_ebuilds:
535 # pylint: disable=C0301
536 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
537 # pylint: enable=C0301
538 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500539 'These ebuilds are using old EAPIs. If these are imported from\n'
540 'Gentoo, then you may ignore and upload once with the --no-verify\n'
541 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500542 '\t%s\n'
543 'See this guide for more details:\n%s\n' %
544 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
545
546
Mike Frysinger89bdb852014-02-01 05:26:26 -0500547def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500548 """Make sure we use the new style KEYWORDS when possible in ebuilds.
549
550 If an ebuild generally does not care about the arch it is running on, then
551 ebuilds should flag it with one of:
552 KEYWORDS="*" # A stable ebuild.
553 KEYWORDS="~*" # An unstable ebuild.
554 KEYWORDS="-* ..." # Is known to only work on specific arches.
555
556 Args:
557 project: The project to look at
558 commit: The commit to look at
559
560 Returns:
561 A HookFailure or None.
562 """
563 WHITELIST = set(('*', '-*', '~*'))
564
565 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
566
Mike Frysinger89bdb852014-02-01 05:26:26 -0500567 ebuilds_re = [r'\.ebuild$']
568 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
569 ebuilds_re)
570
Mike Frysinger8d42d742014-09-22 15:50:21 -0400571 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500572 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400573 # We get the full content rather than a diff as the latter does not work
574 # on new files (like when adding new ebuilds).
575 lines = _get_file_content(ebuild, commit).splitlines()
576 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500577 m = get_keywords.match(line)
578 if m:
579 keywords = set(m.group(1).split())
580 if not keywords or WHITELIST - keywords != WHITELIST:
581 continue
582
Mike Frysinger8d42d742014-09-22 15:50:21 -0400583 bad_ebuilds.append(ebuild)
584
585 if bad_ebuilds:
586 return HookFailure(
587 '%s\n'
588 'Please update KEYWORDS to use a glob:\n'
589 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
590 ' KEYWORDS="*"\n'
591 'If the ebuild should be marked unstable (normal for '
592 'cros-workon / 9999 ebuilds):\n'
593 ' KEYWORDS="~*"\n'
594 'If the ebuild needs to be marked for only specific arches,'
595 'then use -* like so:\n'
596 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500597
598
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800599def _check_ebuild_licenses(_project, commit):
600 """Check if the LICENSE field in the ebuild is correct."""
601 affected_paths = _get_affected_files(commit)
602 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
603
604 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800605 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800606
607 for ebuild in touched_ebuilds:
608 # Skip virutal packages.
609 if ebuild.split('/')[-3] == 'virtual':
610 continue
611
612 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400613 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800614 except ValueError as e:
615 return HookFailure(e.message, [ebuild])
616
617 # Also ignore licenses ending with '?'
618 for license_type in [x for x in license_types
619 if x not in LICENSES_IGNORE and not x.endswith('?')]:
620 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400621 licenses_lib.Licensing.FindLicenseType(license_type)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800622 except AssertionError as e:
623 return HookFailure(e.message, [ebuild])
624
625
Mike Frysingercd363c82014-02-01 05:20:18 -0500626def _check_ebuild_virtual_pv(project, commit):
627 """Enforce the virtual PV policies."""
628 # If this is the portage-stable overlay, then ignore the check.
629 # We want to import virtuals as-is from upstream Gentoo.
630 whitelist = (
631 'chromiumos/overlays/portage-stable',
632 )
633 if project in whitelist:
634 return None
635
636 # We assume the repo name is the same as the dir name on disk.
637 # It would be dumb to not have them match though.
638 project = os.path.basename(project)
639
640 is_variant = lambda x: x.startswith('overlay-variant-')
641 is_board = lambda x: x.startswith('overlay-')
642 is_private = lambda x: x.endswith('-private')
643
644 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
645
646 ebuilds_re = [r'\.ebuild$']
647 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
648 ebuilds_re)
649 bad_ebuilds = []
650
651 for ebuild in ebuilds:
652 m = get_pv.match(ebuild)
653 if m:
654 overlay = m.group(1)
655 if not overlay or not is_board(overlay):
656 overlay = project
657
658 pv = m.group(3).split('-', 1)[0]
659
660 if is_private(overlay):
661 want_pv = '3.5' if is_variant(overlay) else '3'
662 elif is_board(overlay):
663 want_pv = '2.5' if is_variant(overlay) else '2'
664 else:
665 want_pv = '1'
666
667 if pv != want_pv:
668 bad_ebuilds.append((ebuild, pv, want_pv))
669
670 if bad_ebuilds:
671 # pylint: disable=C0301
672 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
673 # pylint: enable=C0301
674 return HookFailure(
675 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
676 '\t%s\n'
677 'If this is an upstream Gentoo virtual, then you may ignore this\n'
678 'check (and re-run w/--no-verify). Otherwise, please see this\n'
679 'page for more details:\n%s\n' %
680 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
681 for x in bad_ebuilds]), url))
682
683
Mike Frysingerae409522014-02-01 03:16:11 -0500684def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700685 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -0400686 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700687 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -0400688 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -0500689 if not m:
Ryan Cui1562fb82011-05-09 11:01:31 -0700690 return HookFailure('Change-Id must be in last paragraph of description.')
691
Mike Frysinger02b88bd2014-11-21 00:29:38 -0500692 # Allow s-o-b tags to follow it, but only those.
693 end = desc[m.end():].strip().splitlines()
694 if [x for x in end if not x.startswith('Signed-off-by: ')]:
695 return HookFailure('Only "Signed-off-by:" tags may follow the Change-Id.')
696
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700697
Mike Frysinger36b2ebc2014-10-31 14:02:03 -0400698def _check_commit_message_style(_project, commit):
699 """Verify that the commit message matches our style.
700
701 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
702 commit hooks.
703 """
704 desc = _get_commit_desc(commit)
705
706 # The first line should be by itself.
707 lines = desc.splitlines()
708 if len(lines) > 1 and lines[1]:
709 return HookFailure('The second line of the commit message must be blank.')
710
711 # The first line should be one sentence.
712 if '. ' in lines[0]:
713 return HookFailure('The first line cannot be more than one sentence.')
714
715 # The first line cannot be too long.
716 MAX_FIRST_LINE_LEN = 100
717 if len(lines[0]) > MAX_FIRST_LINE_LEN:
718 return HookFailure('The first line must be less than %i chars.' %
719 MAX_FIRST_LINE_LEN)
720
721
Mike Frysingerae409522014-02-01 03:16:11 -0500722def _check_license(_project, commit):
Mike Frysinger98638102014-08-28 00:15:08 -0400723 """Verifies the license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700724
Mike Frysinger98638102014-08-28 00:15:08 -0400725 Should be following the spec:
726 http://dev.chromium.org/developers/coding-style#TOC-File-headers
727 """
728 # For older years, be a bit more flexible as our policy says leave them be.
729 LICENSE_HEADER = (
730 r'.* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -0500731 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400732 r'.* Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -0500733 r'be\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400734 r'.* found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -0500735 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400736 )
737 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
738
739 # For newer years, be stricter.
740 COPYRIGHT_LINE = (
741 r'.* Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -0500742 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400743 )
744 copyright_re = re.compile(COPYRIGHT_LINE)
745
746 bad_files = []
747 bad_copyright_files = []
748 files = _filter_files(_get_affected_files(commit, relative=True),
749 COMMON_INCLUDED_PATHS,
750 COMMON_EXCLUDED_PATHS)
751
752 for f in files:
753 contents = _get_file_content(f, commit)
754 if not contents:
755 # Ignore empty files.
756 continue
757
758 if not license_re.search(contents):
759 bad_files.append(f)
760 elif copyright_re.search(contents):
761 bad_copyright_files.append(f)
762
763 if bad_files:
764 msg = '%s:\n%s\n%s' % (
765 'License must match', license_re.pattern,
766 'Found a bad header in these files:')
767 return HookFailure(msg, bad_files)
768
769 if bad_copyright_files:
770 msg = 'Do not use (c) in copyright headers in new files:'
771 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700772
773
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400774def _check_layout_conf(_project, commit):
775 """Verifies the metadata/layout.conf file."""
776 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400777 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400778 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400779 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400780
Mike Frysinger94a670c2014-09-19 12:46:26 -0400781 # Handle multiple overlays in a single commit (like the public tree).
782 for f in _get_affected_files(commit, relative=True):
783 if f.endswith(repo_name):
784 repo_names.append(f)
785 elif f.endswith(layout_path):
786 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400787
788 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -0400789 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400790 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -0400791 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400792
Mike Frysinger94a670c2014-09-19 12:46:26 -0400793 # Gather all the errors in one pass so we show one full message.
794 all_errors = {}
795 for layout_path in layout_paths:
796 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400797
Mike Frysinger94a670c2014-09-19 12:46:26 -0400798 # Make sure the config file is sorted.
799 data = [x for x in _get_file_content(layout_path, commit).splitlines()
800 if x and x[0] != '#']
801 if sorted(data) != data:
802 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400803
Mike Frysinger94a670c2014-09-19 12:46:26 -0400804 # Require people to set specific values all the time.
805 settings = (
806 # TODO: Enable this for everyone. http://crbug.com/408038
807 #('fast caching', 'cache-format = md5-dict'),
808 ('fast manifests', 'thin-manifests = true'),
809 ('extra features', 'profile-formats = portage-2'),
810 )
811 for reason, line in settings:
812 if line not in data:
813 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400814
Mike Frysinger94a670c2014-09-19 12:46:26 -0400815 # Require one of these settings.
816 if ('use-manifests = true' not in data and
817 'use-manifests = strict' not in data):
818 errors += ['enable file checking with: use-manifests = true']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400819
Mike Frysinger94a670c2014-09-19 12:46:26 -0400820 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -0400821 for line in data:
822 if line.startswith('repo-name = '):
823 break
824 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -0400825 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400826
Mike Frysinger94a670c2014-09-19 12:46:26 -0400827 # Summarize all the errors we saw (if any).
828 lines = ''
829 for layout_path, errors in all_errors.items():
830 if errors:
831 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
832 if lines:
833 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
834 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400835
836
Ryan Cuiec4d6332011-05-02 14:15:25 -0700837# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700838
Ryan Cui1562fb82011-05-09 11:01:31 -0700839
Mike Frysingerae409522014-02-01 03:16:11 -0500840def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700841 """Runs checkpatch.pl on the given project"""
842 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700843 options = list(options)
844 if commit == PRE_SUBMIT:
845 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
846 # this case.
847 options.append('--ignore=MISSING_SIGN_OFF')
848 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700849 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700850 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700851 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700852 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700853
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700854
Anton Staaf815d6852011-08-22 10:08:45 -0700855def _run_checkpatch_no_tree(project, commit):
856 return _run_checkpatch(project, commit, ['--no-tree'])
857
Mike Frysingerae409522014-02-01 03:16:11 -0500858
Randall Spangler7318fd62013-11-21 12:16:58 -0800859def _run_checkpatch_ec(project, commit):
860 """Runs checkpatch with options for Chromium EC projects."""
861 return _run_checkpatch(project, commit, ['--no-tree',
862 '--ignore=MSLEEP,VOLATILE'])
863
Mike Frysingerae409522014-02-01 03:16:11 -0500864
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700865def _run_checkpatch_depthcharge(project, commit):
866 """Runs checkpatch with options for depthcharge."""
867 return _run_checkpatch(project, commit, [
868 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700869 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
870 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
871 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700872
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700873def _run_checkpatch_coreboot(project, commit):
874 """Runs checkpatch with options for coreboot."""
875 return _run_checkpatch(project, commit, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700876 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700877 '--no-tree',
878 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
Shawn Nematbakhsh6442c582014-08-22 14:32:06 -0700879 'GLOBAL_INITIALISERS,INITIALISED_STATIC,C99_COMMENTS'])
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700880
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700881
Mike Frysingerae409522014-02-01 03:16:11 -0500882def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700883 """Makes sure kernel config changes are not mixed with code changes"""
884 files = _get_affected_files(commit)
885 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
886 return HookFailure('Changes to chromeos/config/ and regular files must '
887 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700888
Mike Frysingerae409522014-02-01 03:16:11 -0500889
890def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700891 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700892 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700893 try:
894 json.load(open(f))
895 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700896 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700897
898
Mike Frysingerae409522014-02-01 03:16:11 -0500899def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400900 """Make sure Manifest files only have DIST lines"""
901 paths = []
902
903 for path in _get_affected_files(commit):
904 if os.path.basename(path) != 'Manifest':
905 continue
906 if not os.path.exists(path):
907 continue
908
909 with open(path, 'r') as f:
910 for line in f.readlines():
911 if not line.startswith('DIST '):
912 paths.append(path)
913 break
914
915 if paths:
916 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
917 ('\n'.join(paths),))
918
919
Mike Frysingerae409522014-02-01 03:16:11 -0500920def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700921 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700922 if commit == PRE_SUBMIT:
923 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700924 BRANCH_RE = r'\nBRANCH=\S+'
925
926 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
927 msg = ('Changelist description needs BRANCH field (after first line)\n'
928 'E.g. BRANCH=none or BRANCH=link,snow')
929 return HookFailure(msg)
930
931
Mike Frysingerae409522014-02-01 03:16:11 -0500932def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800933 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700934 if commit == PRE_SUBMIT:
935 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800936 SIGNOFF_RE = r'\nSigned-off-by: \S+'
937
938 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
939 msg = ('Changelist description needs Signed-off-by: field\n'
940 'E.g. Signed-off-by: My Name <me@chromium.org>')
941 return HookFailure(msg)
942
943
Jon Salz3ee59de2012-08-18 13:54:22 +0800944def _run_project_hook_script(script, project, commit):
945 """Runs a project hook script.
946
947 The script is run with the following environment variables set:
948 PRESUBMIT_PROJECT: The affected project
949 PRESUBMIT_COMMIT: The affected commit
950 PRESUBMIT_FILES: A newline-separated list of affected files
951
952 The script is considered to fail if the exit code is non-zero. It should
953 write an error message to stdout.
954 """
955 env = dict(os.environ)
956 env['PRESUBMIT_PROJECT'] = project
957 env['PRESUBMIT_COMMIT'] = commit
958
959 # Put affected files in an environment variable
960 files = _get_affected_files(commit)
961 env['PRESUBMIT_FILES'] = '\n'.join(files)
962
963 process = subprocess.Popen(script, env=env, shell=True,
964 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800965 stdout=subprocess.PIPE,
966 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800967 stdout, _ = process.communicate()
968 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800969 if stdout:
970 stdout = re.sub('(?m)^', ' ', stdout)
971 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800972 (script, process.returncode,
973 ':\n' + stdout if stdout else ''))
974
975
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700976def _moved_to_platform2(project, _commit):
977 """Forbids commits to legacy repo in src/platform."""
978 return HookFailure('%s has been moved to platform2. This change should be '
979 'made there.' % project)
980
981
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700982def _check_project_prefix(_project, commit):
983 """Fails if the change is project specific and the commit message is not
984 prefixed by the project_name.
985 """
986
987 files = _get_affected_files(commit, relative=True)
988 prefix = os.path.commonprefix(files)
989 prefix = os.path.dirname(prefix)
990
991 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -0700992 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700993 return
994
995 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -0700996
997 # The common files may all be within a subdirectory of the main project
998 # directory, so walk up the tree until we find an alias file.
999 # _get_affected_files() should return relative paths, but check against '/' to
1000 # ensure that this loop terminates even if it receives an absolute path.
1001 while prefix and prefix != '/':
1002 alias_file = os.path.join(prefix, '.project_alias')
1003
1004 # If an alias exists, use it.
1005 if os.path.isfile(alias_file):
1006 project_name = osutils.ReadFile(alias_file).strip()
1007
1008 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001009
1010 if not _get_commit_desc(commit).startswith(project_name + ': '):
1011 return HookFailure('The commit title for changes affecting only %s'
1012 ' should start with \"%s: \"'
1013 % (project_name, project_name))
1014
1015
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001016# Base
1017
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001018# A list of hooks which are not project specific and check patch description
1019# (as opposed to patch body).
1020_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001021 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001022 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001023 _check_change_has_test_field,
1024 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001025 _check_commit_message_style,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001026]
1027
1028
1029# A list of hooks that are not project-specific
1030_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001031 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001032 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001033 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001034 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -07001035 _check_no_stray_whitespace,
1036 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001037 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001038 _check_license,
1039 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001040 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001041]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001042
Ryan Cui1562fb82011-05-09 11:01:31 -07001043
Ryan Cui9b651632011-05-11 11:38:58 -07001044# A dictionary of project-specific hooks(callbacks), indexed by project name.
1045# dict[project] = [callback1, callback2]
1046_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001047 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001048 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1049 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001050 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001051 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001052 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001053 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1054 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001055 "chromiumos/overlays/board-overlays": [_check_manifests],
1056 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1057 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001058 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury42052852014-10-06 16:02:38 -07001059 "chromiumos/platform/depthcharge": [_check_change_has_branch_field,
1060 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001061 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001062 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001063 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001064 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001065 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury42052852014-10-06 16:02:38 -07001066 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
1067 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001068 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001069 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001070 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1071 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1072 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001073 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001074}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001075
Ryan Cui1562fb82011-05-09 11:01:31 -07001076
Ryan Cui9b651632011-05-11 11:38:58 -07001077# A dictionary of flags (keys) that can appear in the config file, and the hook
1078# that the flag disables (value)
1079_DISABLE_FLAGS = {
1080 'stray_whitespace_check': _check_no_stray_whitespace,
1081 'long_line_check': _check_no_long_lines,
1082 'cros_license_check': _check_license,
1083 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001084 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001085 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001086 'bug_field_check': _check_change_has_bug_field,
1087 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001088}
1089
1090
Jon Salz3ee59de2012-08-18 13:54:22 +08001091def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001092 """Returns a set of hooks disabled by the current project's config file.
1093
1094 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001095
1096 Args:
1097 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001098 """
1099 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001100 if not config.has_section(SECTION):
1101 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001102
1103 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001104 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001105 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001106 if not config.getboolean(SECTION, flag):
1107 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001108 except ValueError as e:
1109 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001110 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001111
1112 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1113 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1114
1115
Jon Salz3ee59de2012-08-18 13:54:22 +08001116def _get_project_hook_scripts(config):
1117 """Returns a list of project-specific hook scripts.
1118
1119 Args:
1120 config: A ConfigParser for the project's config file.
1121 """
1122 SECTION = 'Hook Scripts'
1123 if not config.has_section(SECTION):
1124 return []
1125
1126 hook_names_values = config.items(SECTION)
1127 hook_names_values.sort(key=lambda x: x[0])
1128 return [x[1] for x in hook_names_values]
1129
1130
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001131def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001132 """Returns a list of hooks that need to be run for a project.
1133
1134 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001135
1136 Args:
1137 project: A string, name of the project.
1138 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001139 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001140 config = ConfigParser.RawConfigParser()
1141 try:
1142 config.read(_CONFIG_FILE)
1143 except ConfigParser.Error:
1144 # Just use an empty config file
1145 config = ConfigParser.RawConfigParser()
1146
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001147 if presubmit:
1148 hook_list = _COMMON_HOOKS
1149 else:
1150 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1151
Jon Salz3ee59de2012-08-18 13:54:22 +08001152 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001153 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001154
1155 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001156 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1157 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001158
Jon Salz3ee59de2012-08-18 13:54:22 +08001159 for script in _get_project_hook_scripts(config):
1160 hooks.append(functools.partial(_run_project_hook_script, script))
1161
Ryan Cui9b651632011-05-11 11:38:58 -07001162 return hooks
1163
1164
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001165def _run_project_hooks(project, proj_dir=None,
1166 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001167 """For each project run its project specific hook from the hooks dictionary.
1168
1169 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001170 project: The name of project to run hooks for.
1171 proj_dir: If non-None, this is the directory the project is in. If None,
1172 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001173 commit_list: A list of commits to run hooks against. If None or empty list
1174 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001175 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001176
1177 Returns:
1178 Boolean value of whether any errors were ecountered while running the hooks.
1179 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001180 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001181 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1182 if len(proj_dirs) == 0:
1183 print('%s cannot be found.' % project, file=sys.stderr)
1184 print('Please specify a valid project.', file=sys.stderr)
1185 return True
1186 if len(proj_dirs) > 1:
1187 print('%s is associated with multiple directories.' % project,
1188 file=sys.stderr)
1189 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1190 return True
1191 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001192
Ryan Cuiec4d6332011-05-02 14:15:25 -07001193 pwd = os.getcwd()
1194 # hooks assume they are run from the root of the project
1195 os.chdir(proj_dir)
1196
Doug Anderson14749562013-06-26 13:38:29 -07001197 if not commit_list:
1198 try:
1199 commit_list = _get_commits()
1200 except VerifyException as e:
1201 PrintErrorForProject(project, HookFailure(str(e)))
1202 os.chdir(pwd)
1203 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001204
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001205 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001206 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001207 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001208 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001209 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001210 hook_error = hook(project, commit)
1211 if hook_error:
1212 error_list.append(hook_error)
1213 error_found = True
1214 if error_list:
1215 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1216 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001217
Ryan Cuiec4d6332011-05-02 14:15:25 -07001218 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001219 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001220
Mike Frysingerae409522014-02-01 03:16:11 -05001221
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001222# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001223
Ryan Cui1562fb82011-05-09 11:01:31 -07001224
Mike Frysingerae409522014-02-01 03:16:11 -05001225def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001226 """Main function invoked directly by repo.
1227
1228 This function will exit directly upon error so that repo doesn't print some
1229 obscure error message.
1230
1231 Args:
1232 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001233 worktree_list: A list of directories. It should be the same length as
1234 project_list, so that each entry in project_list matches with a directory
1235 in worktree_list. If None, we will attempt to calculate the directories
1236 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001237 kwargs: Leave this here for forward-compatibility.
1238 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001239 found_error = False
David James2edd9002013-10-11 14:09:19 -07001240 if not worktree_list:
1241 worktree_list = [None] * len(project_list)
1242 for project, worktree in zip(project_list, worktree_list):
1243 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001244 found_error = True
1245
Mike Frysingerae409522014-02-01 03:16:11 -05001246 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001247 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001248 '- To disable some source style checks, and for other hints, see '
1249 '<checkout_dir>/src/repohooks/README\n'
1250 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001251 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001252 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001253
Ryan Cui1562fb82011-05-09 11:01:31 -07001254
Doug Anderson44a644f2011-11-02 10:37:37 -07001255def _identify_project(path):
1256 """Identify the repo project associated with the given path.
1257
1258 Returns:
1259 A string indicating what project is associated with the path passed in or
1260 a blank string upon failure.
1261 """
1262 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Mike Frysingerb81102f2014-11-21 00:33:35 -05001263 stderr=subprocess.PIPE, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07001264
1265
1266def direct_main(args, verbose=False):
1267 """Run hooks directly (outside of the context of repo).
1268
1269 # Setup for doctests below.
1270 # ...note that some tests assume that running pre-upload on this CWD is fine.
1271 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1272 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1273 >>> olddir = os.getcwd()
1274
1275 # OK to run w/ no arugments; will run with CWD.
1276 >>> os.chdir(mydir)
1277 >>> direct_main(['prog_name'], verbose=True)
1278 Running hooks on chromiumos/repohooks
1279 0
1280 >>> os.chdir(olddir)
1281
1282 # Run specifying a dir
1283 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1284 Running hooks on chromiumos/repohooks
1285 0
1286
1287 # Not a problem to use a bogus project; we'll just get default settings.
1288 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1289 Running hooks on X
1290 0
1291
1292 # Run with project but no dir
1293 >>> os.chdir(mydir)
1294 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1295 Running hooks on X
1296 0
1297 >>> os.chdir(olddir)
1298
1299 # Try with a non-git CWD
1300 >>> os.chdir('/tmp')
1301 >>> direct_main(['prog_name'])
1302 Traceback (most recent call last):
1303 ...
1304 BadInvocation: The current directory is not part of a git project.
1305
1306 # Check various bad arguments...
1307 >>> direct_main(['prog_name', 'bogus'])
1308 Traceback (most recent call last):
1309 ...
1310 BadInvocation: Unexpected arguments: bogus
1311 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1312 Traceback (most recent call last):
1313 ...
1314 BadInvocation: Invalid dir: bogusdir
1315 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1316 Traceback (most recent call last):
1317 ...
1318 BadInvocation: Not a git directory: /tmp
1319
1320 Args:
1321 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001322 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001323
1324 Returns:
1325 0 if no pre-upload failures, 1 if failures.
1326
1327 Raises:
1328 BadInvocation: On some types of invocation errors.
1329 """
1330 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1331 parser = optparse.OptionParser(description=desc)
1332
1333 parser.add_option('--dir', default=None,
1334 help='The directory that the project lives in. If not '
1335 'specified, use the git project root based on the cwd.')
1336 parser.add_option('--project', default=None,
1337 help='The project repo path; this can affect how the hooks '
1338 'get run, since some hooks are project-specific. For '
1339 'chromite this is chromiumos/chromite. If not specified, '
1340 'the repo tool will be used to figure this out based on '
1341 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001342 parser.add_option('--rerun-since', default=None,
1343 help='Rerun hooks on old commits since the given date. '
1344 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001345 'e.g. 2012-06-20. This option is mutually exclusive '
1346 'with --pre-submit.')
1347 parser.add_option('--pre-submit', action="store_true",
1348 help='Run the check against the pending commit. '
1349 'This option should be used at the \'git commit\' '
1350 'phase as opposed to \'repo upload\'. This option '
1351 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001352
1353 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001354
1355 opts, args = parser.parse_args(args[1:])
1356
Doug Anderson14749562013-06-26 13:38:29 -07001357 if opts.rerun_since:
1358 if args:
1359 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1360 ' '.join(args))
1361
1362 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1363 all_commits = _run_command(cmd).splitlines()
1364 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1365
1366 # Eliminate chrome-bot commits but keep ordering the same...
1367 bot_commits = set(bot_commits)
1368 args = [c for c in all_commits if c not in bot_commits]
1369
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001370 if opts.pre_submit:
1371 raise BadInvocation('rerun-since and pre-submit can not be '
1372 'used together')
1373 if opts.pre_submit:
1374 if args:
1375 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1376 ' '.join(args))
1377 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001378
1379 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1380 # project from CWD
1381 if opts.dir is None:
1382 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1383 stderr=subprocess.PIPE).strip()
1384 if not git_dir:
1385 raise BadInvocation('The current directory is not part of a git project.')
1386 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1387 elif not os.path.isdir(opts.dir):
1388 raise BadInvocation('Invalid dir: %s' % opts.dir)
1389 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1390 raise BadInvocation('Not a git directory: %s' % opts.dir)
1391
1392 # Identify the project if it wasn't specified; this _requires_ the repo
1393 # tool to be installed and for the project to be part of a repo checkout.
1394 if not opts.project:
1395 opts.project = _identify_project(opts.dir)
1396 if not opts.project:
1397 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1398
1399 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001400 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001401
Doug Anderson14749562013-06-26 13:38:29 -07001402 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001403 commit_list=args,
1404 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001405 if found_error:
1406 return 1
1407 return 0
1408
1409
1410def _test():
1411 """Run any built-in tests."""
1412 import doctest
1413 doctest.testmod()
1414
1415
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001416if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001417 if sys.argv[1:2] == ["--test"]:
1418 _test()
1419 exit_code = 0
1420 else:
1421 prog_name = os.path.basename(sys.argv[0])
1422 try:
1423 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001424 except BadInvocation, err:
1425 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001426 exit_code = 1
1427 sys.exit(exit_code)