blob: 1776bca84dfb22c521c50218f86593b2a943dab8 [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
David Hendricks4c018e72013-02-06 13:46:38 -080019import socket
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070020import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070021import subprocess
22
Ryan Cui1562fb82011-05-09 11:01:31 -070023from errors import (VerifyException, HookFailure, PrintErrorForProject,
24 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070025
David Jamesc3b68b32013-04-03 09:17:03 -070026# If repo imports us, the __name__ will be __builtin__, and the wrapper will
27# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
28# up. The same logic also happens to work if we're executed directly.
29if __name__ in ('__builtin__', '__main__'):
30 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
31
32from chromite.lib import patch
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -080033from chromite.licensing import licenses
David Jamesc3b68b32013-04-03 09:17:03 -070034
Ryan Cuiec4d6332011-05-02 14:15:25 -070035
36COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050037 # C++ and friends
38 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
39 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
40 # Scripts
41 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
42 # No extension at all, note that ALL CAPS files are black listed in
43 # COMMON_EXCLUDED_LIST below.
44 r"(^|.*[\\\/])[^.]+$",
45 # Other
46 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070047]
48
Ryan Cui1562fb82011-05-09 11:01:31 -070049
Ryan Cuiec4d6332011-05-02 14:15:25 -070050COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050051 # avoid doing source file checks for kernel
52 r"/src/third_party/kernel/",
53 r"/src/third_party/kernel-next/",
54 r"/src/third_party/ktop/",
55 r"/src/third_party/punybench/",
56 r".*\bexperimental[\\\/].*",
57 r".*\b[A-Z0-9_]{2,}$",
58 r".*[\\\/]debian[\\\/]rules$",
59 # for ebuild trees, ignore any caches and manifest data
60 r".*/Manifest$",
61 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070062
Mike Frysingerae409522014-02-01 03:16:11 -050063 # ignore profiles data (like overlay-tegra2/profiles)
64 r".*/overlay-.*/profiles/.*",
65 # ignore minified js and jquery
66 r".*\.min\.js",
67 r".*jquery.*\.js",
Ryan Cuiec4d6332011-05-02 14:15:25 -070068]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070069
Ryan Cui1562fb82011-05-09 11:01:31 -070070
Ryan Cui9b651632011-05-11 11:38:58 -070071_CONFIG_FILE = 'PRESUBMIT.cfg'
72
73
Doug Anderson44a644f2011-11-02 10:37:37 -070074# Exceptions
75
76
77class BadInvocation(Exception):
78 """An Exception indicating a bad invocation of the program."""
79 pass
80
81
Ryan Cui1562fb82011-05-09 11:01:31 -070082# General Helpers
83
Sean Paulba01d402011-05-05 11:36:23 -040084
Doug Anderson44a644f2011-11-02 10:37:37 -070085def _run_command(cmd, cwd=None, stderr=None):
86 """Executes the passed in command and returns raw stdout output.
87
88 Args:
89 cmd: The command to run; should be a list of strings.
90 cwd: The directory to switch to for running the command.
91 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
92 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
93
94 Returns:
95 The standard out from the process.
96 """
97 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
98 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -070099
Ryan Cui1562fb82011-05-09 11:01:31 -0700100
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700101def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700102 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700103 if __name__ == '__main__':
104 # Works when file is run on its own (__file__ is defined)...
105 return os.path.abspath(os.path.dirname(__file__))
106 else:
107 # We need to do this when we're run through repo. Since repo executes
108 # us with execfile(), we don't get __file__ defined.
109 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
110 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700111
Ryan Cui1562fb82011-05-09 11:01:31 -0700112
Ryan Cuiec4d6332011-05-02 14:15:25 -0700113def _match_regex_list(subject, expressions):
114 """Try to match a list of regular expressions to a string.
115
116 Args:
117 subject: The string to match regexes on
118 expressions: A list of regular expressions to check for matches with.
119
120 Returns:
121 Whether the passed in subject matches any of the passed in regexes.
122 """
123 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500124 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700125 return True
126 return False
127
Ryan Cui1562fb82011-05-09 11:01:31 -0700128
Mike Frysingerae409522014-02-01 03:16:11 -0500129def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700130 """Filter out files based on the conditions passed in.
131
132 Args:
133 files: list of filepaths to filter
134 include_list: list of regex that when matched with a file path will cause it
135 to be added to the output list unless the file is also matched with a
136 regex in the exclude_list.
137 exclude_list: list of regex that when matched with a file will prevent it
138 from being added to the output list, even if it is also matched with a
139 regex in the include_list.
140
141 Returns:
142 A list of filepaths that contain files matched in the include_list and not
143 in the exclude_list.
144 """
145 filtered = []
146 for f in files:
147 if (_match_regex_list(f, include_list) and
148 not _match_regex_list(f, exclude_list)):
149 filtered.append(f)
150 return filtered
151
Ryan Cuiec4d6332011-05-02 14:15:25 -0700152
David Hendricks35030d02013-02-04 17:49:16 -0800153def _verify_header_content(commit, content, fail_msg):
154 """Verify that file headers contain specified content.
155
156 Args:
157 commit: the affected commit.
158 content: the content of the header to be verified.
159 fail_msg: the first message to display in case of failure.
160
161 Returns:
162 The return value of HookFailure().
163 """
164 license_re = re.compile(content, re.MULTILINE)
165 bad_files = []
166 files = _filter_files(_get_affected_files(commit),
167 COMMON_INCLUDED_PATHS,
168 COMMON_EXCLUDED_PATHS)
169
170 for f in files:
Tom Wai-Hong Tam667db5d2014-02-27 06:28:14 +0800171 # Ignore non-existant files and symlinks
172 if os.path.exists(f) and not os.path.islink(f):
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800173 contents = open(f).read()
Mike Frysingerae409522014-02-01 03:16:11 -0500174 if not contents:
175 # Ignore empty files
176 continue
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800177 if not license_re.search(contents):
178 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800179 if bad_files:
Mike Frysingerae409522014-02-01 03:16:11 -0500180 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
181 "Found a bad header in these files:")
182 return HookFailure(msg, bad_files)
David Hendricks35030d02013-02-04 17:49:16 -0800183
184
Ryan Cuiec4d6332011-05-02 14:15:25 -0700185# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700186
187
Ryan Cui4725d952011-05-05 15:41:19 -0700188def _get_upstream_branch():
189 """Returns the upstream tracking branch of the current branch.
190
191 Raises:
192 Error if there is no tracking branch
193 """
194 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
195 current_branch = current_branch.replace('refs/heads/', '')
196 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700197 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700198
199 cfg_option = 'branch.' + current_branch + '.%s'
200 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
201 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
202 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700203 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700204
205 return full_upstream.replace('heads', 'remotes/' + remote)
206
Ryan Cui1562fb82011-05-09 11:01:31 -0700207
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700208def _get_patch(commit):
209 """Returns the patch for this commit."""
210 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700211
Ryan Cui1562fb82011-05-09 11:01:31 -0700212
Jon Salz98255932012-08-18 14:48:02 +0800213def _try_utf8_decode(data):
214 """Attempts to decode a string as UTF-8.
215
216 Returns:
217 The decoded Unicode object, or the original string if parsing fails.
218 """
219 try:
220 return unicode(data, 'utf-8', 'strict')
221 except UnicodeDecodeError:
222 return data
223
224
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500225def _get_file_content(path, commit):
226 """Returns the content of a file at a specific commit.
227
228 We can't rely on the file as it exists in the filesystem as people might be
229 uploading a series of changes which modifies the file multiple times.
230
231 Note: The "content" of a symlink is just the target. So if you're expecting
232 a full file, you should check that first. One way to detect is that the
233 content will not have any newlines.
234 """
235 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
236
237
Mike Frysingerae409522014-02-01 03:16:11 -0500238def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700239 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysinger5122a1f2014-02-01 02:47:35 -0500240 output = _run_command(['git', 'show', '-p', '--pretty=format:',
Mike Frysingerae409522014-02-01 03:16:11 -0500241 '--no-ext-diff', commit, path])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700242
243 new_lines = []
244 line_num = 0
245 for line in output.splitlines():
246 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
247 if m:
248 line_num = int(m.groups(1)[0])
249 continue
250 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800251 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700252 if not line.startswith('-'):
253 line_num += 1
254 return new_lines
255
Ryan Cui1562fb82011-05-09 11:01:31 -0700256
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500257def _get_affected_files(commit, include_deletes=False, relative=False):
Doug Anderson42b8a052013-06-26 10:45:36 -0700258 """Returns list of absolute filepaths that were modified/added.
259
260 Args:
261 commit: The commit
262 include_deletes: If true we'll include delete in the list.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500263 relative: Whether to return full paths to files.
Doug Anderson42b8a052013-06-26 10:45:36 -0700264
265 Returns:
266 A list of modified/added (and perhaps deleted) files
267 """
Ryan Cui72834d12011-05-05 14:51:33 -0700268 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700269 files = []
270 for statusline in output.splitlines():
271 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
272 # Ignore deleted files, and return absolute paths of files
Mike Frysingerae409522014-02-01 03:16:11 -0500273 if include_deletes or m.group(1)[0] != 'D':
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500274 f = m.group(2)
275 if not relative:
276 pwd = os.getcwd()
277 f = os.path.join(pwd, f)
278 files.append(f)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700279 return files
280
Ryan Cui1562fb82011-05-09 11:01:31 -0700281
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700282def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700283 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700284 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700285 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700286
Ryan Cui1562fb82011-05-09 11:01:31 -0700287
Ryan Cuiec4d6332011-05-02 14:15:25 -0700288def _get_commit_desc(commit):
289 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400290 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700291
292
293# Common Hooks
294
Ryan Cui1562fb82011-05-09 11:01:31 -0700295
Mike Frysingerae409522014-02-01 03:16:11 -0500296def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700297 """Checks that there aren't any lines longer than maxlen characters in any of
298 the text files to be submitted.
299 """
300 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800301 SKIP_REGEXP = re.compile('|'.join([
302 r'https?://',
303 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700304
305 errors = []
306 files = _filter_files(_get_affected_files(commit),
307 COMMON_INCLUDED_PATHS,
308 COMMON_EXCLUDED_PATHS)
309
310 for afile in files:
311 for line_num, line in _get_file_diff(afile, commit):
312 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500313 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800314 continue
315
316 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
317 if len(errors) == 5: # Just show the first 5 errors.
318 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700319
320 if errors:
321 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700322 return HookFailure(msg, errors)
323
Ryan Cuiec4d6332011-05-02 14:15:25 -0700324
Mike Frysingerae409522014-02-01 03:16:11 -0500325def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700326 """Checks that there is no stray whitespace at source lines end."""
327 errors = []
328 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500329 COMMON_INCLUDED_PATHS,
330 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700331
332 for afile in files:
333 for line_num, line in _get_file_diff(afile, commit):
334 if line.rstrip() != line:
335 errors.append('%s, line %s' % (afile, line_num))
336 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700337 return HookFailure('Found line ending with white space in:', errors)
338
Ryan Cuiec4d6332011-05-02 14:15:25 -0700339
Mike Frysingerae409522014-02-01 03:16:11 -0500340def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700341 """Checks there are no unexpanded tabs."""
342 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700343 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700344 r".*\.ebuild$",
345 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500346 r".*/[M|m]akefile$",
347 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700348 ]
349
350 errors = []
351 files = _filter_files(_get_affected_files(commit),
352 COMMON_INCLUDED_PATHS,
353 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
354
355 for afile in files:
356 for line_num, line in _get_file_diff(afile, commit):
357 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500358 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700359 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700360 return HookFailure('Found a tab character in:', errors)
361
Ryan Cuiec4d6332011-05-02 14:15:25 -0700362
Mike Frysingerae409522014-02-01 03:16:11 -0500363def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700364 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700365 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700366
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700367 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700368 msg = 'Changelist description needs TEST field (after first line)'
369 return HookFailure(msg)
370
Ryan Cuiec4d6332011-05-02 14:15:25 -0700371
Mike Frysingerae409522014-02-01 03:16:11 -0500372def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700373 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
374 msg = 'Changelist has invalid CQ-DEPEND target.'
375 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
376 try:
377 patch.GetPaladinDeps(_get_commit_desc(commit))
378 except ValueError as ex:
379 return HookFailure(msg, [example, str(ex)])
380
381
Mike Frysingerae409522014-02-01 03:16:11 -0500382def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700383 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700384 OLD_BUG_RE = r'\nBUG=.*chromium-os'
385 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
386 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
387 'the chromium tracker in your BUG= line now.')
388 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700389
David James5c0073d2013-04-03 08:48:52 -0700390 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700391 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700392 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700393 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700394 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
395 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700396 return HookFailure(msg)
397
Ryan Cuiec4d6332011-05-02 14:15:25 -0700398
Doug Anderson42b8a052013-06-26 10:45:36 -0700399def _check_for_uprev(project, commit):
400 """Check that we're not missing a revbump of an ebuild in the given commit.
401
402 If the given commit touches files in a directory that has ebuilds somewhere
403 up the directory hierarchy, it's very likely that we need an ebuild revbump
404 in order for those changes to take effect.
405
406 It's not totally trivial to detect a revbump, so at least detect that an
407 ebuild with a revision number in it was touched. This should handle the
408 common case where we use a symlink to do the revbump.
409
410 TODO: it would be nice to enhance this hook to:
411 * Handle cases where people revbump with a slightly different syntax. I see
412 one ebuild (puppy) that revbumps with _pN. This is a false positive.
413 * Catches cases where people aren't using symlinks for revbumps. If they
414 edit a revisioned file directly (and are expected to rename it for revbump)
415 we'll miss that. Perhaps we could detect that the file touched is a
416 symlink?
417
418 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
419 still better off than without this check.
420
421 Args:
422 project: The project to look at
423 commit: The commit to look at
424
425 Returns:
426 A HookFailure or None.
427 """
Mike Frysinger011af942014-01-17 16:12:22 -0500428 # If this is the portage-stable overlay, then ignore the check. It's rare
429 # that we're doing anything other than importing files from upstream, so
430 # forcing a rev bump makes no sense.
431 whitelist = (
432 'chromiumos/overlays/portage-stable',
433 )
434 if project in whitelist:
435 return None
436
Doug Anderson42b8a052013-06-26 10:45:36 -0700437 affected_paths = _get_affected_files(commit, include_deletes=True)
438
439 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500440 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700441 affected_paths = [path for path in affected_paths
442 if os.path.basename(path) not in whitelist]
443 if not affected_paths:
444 return None
445
446 # If we've touched any file named with a -rN.ebuild then we'll say we're
447 # OK right away. See TODO above about enhancing this.
448 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
449 for path in affected_paths)
450 if touched_revved_ebuild:
451 return None
452
453 # We want to examine the current contents of all directories that are parents
454 # of files that were touched (up to the top of the project).
455 #
456 # ...note: we use the current directory contents even though it may have
457 # changed since the commit we're looking at. This is just a heuristic after
458 # all. Worst case we don't flag a missing revbump.
459 project_top = os.getcwd()
460 dirs_to_check = set([project_top])
461 for path in affected_paths:
462 path = os.path.dirname(path)
463 while os.path.exists(path) and not os.path.samefile(path, project_top):
464 dirs_to_check.add(path)
465 path = os.path.dirname(path)
466
467 # Look through each directory. If it's got an ebuild in it then we'll
468 # consider this as a case when we need a revbump.
469 for dir_path in dirs_to_check:
470 contents = os.listdir(dir_path)
471 ebuilds = [os.path.join(dir_path, path)
472 for path in contents if path.endswith('.ebuild')]
473 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
474
475 # If the -9999.ebuild file was touched the bot will uprev for us.
476 # ...we'll use a simple intersection here as a heuristic...
477 if set(ebuilds_9999) & set(affected_paths):
478 continue
479
480 if ebuilds:
481 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
482 'or a -r1.ebuild symlink if this is a new ebuild')
483
484 return None
485
486
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500487def _check_ebuild_eapi(project, commit):
488 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
489
490 We want to get away from older EAPI's as it makes life confusing and they
491 have less builtin error checking.
492
493 Args:
494 project: The project to look at
495 commit: The commit to look at
496
497 Returns:
498 A HookFailure or None.
499 """
500 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500501 # that we're doing anything other than importing files from upstream, and
502 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500503 whitelist = (
504 'chromiumos/overlays/portage-stable',
505 )
506 if project in whitelist:
507 return None
508
509 BAD_EAPIS = ('0', '1', '2', '3')
510
511 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
512
513 ebuilds_re = [r'\.ebuild$']
514 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
515 ebuilds_re)
516 bad_ebuilds = []
517
518 for ebuild in ebuilds:
519 # If the ebuild does not specify an EAPI, it defaults to 0.
520 eapi = '0'
521
522 lines = _get_file_content(ebuild, commit).splitlines()
523 if len(lines) == 1:
524 # This is most likely a symlink, so skip it entirely.
525 continue
526
527 for line in lines:
528 m = get_eapi.match(line)
529 if m:
530 # Once we hit the first EAPI line in this ebuild, stop processing.
531 # The spec requires that there only be one and it be first, so
532 # checking all possible values is pointless. We also assume that
533 # it's "the" EAPI line and not something in the middle of a heredoc.
534 eapi = m.group(1)
535 break
536
537 if eapi in BAD_EAPIS:
538 bad_ebuilds.append((ebuild, eapi))
539
540 if bad_ebuilds:
541 # pylint: disable=C0301
542 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
543 # pylint: enable=C0301
544 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500545 'These ebuilds are using old EAPIs. If these are imported from\n'
546 'Gentoo, then you may ignore and upload once with the --no-verify\n'
547 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500548 '\t%s\n'
549 'See this guide for more details:\n%s\n' %
550 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
551
552
Mike Frysinger89bdb852014-02-01 05:26:26 -0500553def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500554 """Make sure we use the new style KEYWORDS when possible in ebuilds.
555
556 If an ebuild generally does not care about the arch it is running on, then
557 ebuilds should flag it with one of:
558 KEYWORDS="*" # A stable ebuild.
559 KEYWORDS="~*" # An unstable ebuild.
560 KEYWORDS="-* ..." # Is known to only work on specific arches.
561
562 Args:
563 project: The project to look at
564 commit: The commit to look at
565
566 Returns:
567 A HookFailure or None.
568 """
569 WHITELIST = set(('*', '-*', '~*'))
570
571 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
572
Mike Frysinger89bdb852014-02-01 05:26:26 -0500573 ebuilds_re = [r'\.ebuild$']
574 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
575 ebuilds_re)
576
Mike Frysingerc51ece72014-01-17 16:23:40 -0500577 for ebuild in ebuilds:
578 for _, line in _get_file_diff(ebuild, commit):
579 m = get_keywords.match(line)
580 if m:
581 keywords = set(m.group(1).split())
582 if not keywords or WHITELIST - keywords != WHITELIST:
583 continue
584
585 return HookFailure(
586 'Please update KEYWORDS to use a glob:\n'
587 'If the ebuild should be marked stable (normal for non-9999 '
588 'ebuilds):\n'
589 ' KEYWORDS="*"\n'
590 'If the ebuild should be marked unstable (normal for '
591 'cros-workon / 9999 ebuilds):\n'
592 ' KEYWORDS="~*"\n'
593 'If the ebuild needs to be marked for only specific arches,'
594 'then use -* like so:\n'
595 ' KEYWORDS="-* arm ..."\n')
596
597
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800598def _check_ebuild_licenses(_project, commit):
599 """Check if the LICENSE field in the ebuild is correct."""
600 affected_paths = _get_affected_files(commit)
601 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
602
603 # A list of licenses to ignore for now.
Yu-Ju Hongb02e98c2014-02-27 15:10:10 -0800604 LICENSES_IGNORE = ['||', '(', ')', 'Proprietary']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800605
606 for ebuild in touched_ebuilds:
607 # Skip virutal packages.
608 if ebuild.split('/')[-3] == 'virtual':
609 continue
610
611 try:
612 license_types = licenses.GetLicenseTypesFromEbuild(ebuild)
613 except ValueError as e:
614 return HookFailure(e.message, [ebuild])
615
616 # Also ignore licenses ending with '?'
617 for license_type in [x for x in license_types
618 if x not in LICENSES_IGNORE and not x.endswith('?')]:
619 try:
620 licenses.Licensing.FindLicenseType(license_type)
621 except AssertionError as e:
622 return HookFailure(e.message, [ebuild])
623
624
Mike Frysingercd363c82014-02-01 05:20:18 -0500625def _check_ebuild_virtual_pv(project, commit):
626 """Enforce the virtual PV policies."""
627 # If this is the portage-stable overlay, then ignore the check.
628 # We want to import virtuals as-is from upstream Gentoo.
629 whitelist = (
630 'chromiumos/overlays/portage-stable',
631 )
632 if project in whitelist:
633 return None
634
635 # We assume the repo name is the same as the dir name on disk.
636 # It would be dumb to not have them match though.
637 project = os.path.basename(project)
638
639 is_variant = lambda x: x.startswith('overlay-variant-')
640 is_board = lambda x: x.startswith('overlay-')
641 is_private = lambda x: x.endswith('-private')
642
643 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
644
645 ebuilds_re = [r'\.ebuild$']
646 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
647 ebuilds_re)
648 bad_ebuilds = []
649
650 for ebuild in ebuilds:
651 m = get_pv.match(ebuild)
652 if m:
653 overlay = m.group(1)
654 if not overlay or not is_board(overlay):
655 overlay = project
656
657 pv = m.group(3).split('-', 1)[0]
658
659 if is_private(overlay):
660 want_pv = '3.5' if is_variant(overlay) else '3'
661 elif is_board(overlay):
662 want_pv = '2.5' if is_variant(overlay) else '2'
663 else:
664 want_pv = '1'
665
666 if pv != want_pv:
667 bad_ebuilds.append((ebuild, pv, want_pv))
668
669 if bad_ebuilds:
670 # pylint: disable=C0301
671 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
672 # pylint: enable=C0301
673 return HookFailure(
674 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
675 '\t%s\n'
676 'If this is an upstream Gentoo virtual, then you may ignore this\n'
677 'check (and re-run w/--no-verify). Otherwise, please see this\n'
678 'page for more details:\n%s\n' %
679 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
680 for x in bad_ebuilds]), url))
681
682
Mike Frysingerae409522014-02-01 03:16:11 -0500683def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700684 """Verify that Change-ID is present in last paragraph of commit message."""
685 desc = _get_commit_desc(commit)
686 loc = desc.rfind('\nChange-Id:')
687 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700688 return HookFailure('Change-Id must be in last paragraph of description.')
689
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700690
Mike Frysingerae409522014-02-01 03:16:11 -0500691def _check_license(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700692 """Verifies the license header."""
693 LICENSE_HEADER = (
Chris Sosaed7a3fa2014-02-26 12:18:31 -0800694 r".* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. "
695 "All rights reserved\." "\n"
696 r".* Use of this source code is governed by a BSD-style license that can "
697 "be\n"
698 r".* found in the LICENSE file\."
699 "\n"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700700 )
David Hendricks35030d02013-02-04 17:49:16 -0800701 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700702
David Hendricks35030d02013-02-04 17:49:16 -0800703 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700704
705
Mike Frysingerae409522014-02-01 03:16:11 -0500706def _check_google_copyright(_project, commit):
David Hendricksa0e310d2013-02-04 17:51:55 -0800707 """Verifies Google Inc. as copyright holder."""
708 LICENSE_HEADER = (
709 r".* Copyright 20[-0-9]{2,7} Google Inc\."
710 )
711 FAIL_MSG = "Copyright must match"
712
David Hendricks4c018e72013-02-06 13:46:38 -0800713 # Avoid blocking partners and external contributors.
714 fqdn = socket.getfqdn()
715 if not fqdn.endswith(".corp.google.com"):
716 return None
717
David Hendricksa0e310d2013-02-04 17:51:55 -0800718 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
719
720
Ryan Cuiec4d6332011-05-02 14:15:25 -0700721# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700722
Ryan Cui1562fb82011-05-09 11:01:31 -0700723
Mike Frysingerae409522014-02-01 03:16:11 -0500724def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700725 """Runs checkpatch.pl on the given project"""
726 hooks_dir = _get_hooks_dir()
Mike Frysingerae409522014-02-01 03:16:11 -0500727 cmd = ['%s/checkpatch.pl' % hooks_dir] + list(options) + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700728 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700729 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700730 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700731 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700732
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700733
Anton Staaf815d6852011-08-22 10:08:45 -0700734def _run_checkpatch_no_tree(project, commit):
735 return _run_checkpatch(project, commit, ['--no-tree'])
736
Mike Frysingerae409522014-02-01 03:16:11 -0500737
Randall Spangler7318fd62013-11-21 12:16:58 -0800738def _run_checkpatch_ec(project, commit):
739 """Runs checkpatch with options for Chromium EC projects."""
740 return _run_checkpatch(project, commit, ['--no-tree',
741 '--ignore=MSLEEP,VOLATILE'])
742
Mike Frysingerae409522014-02-01 03:16:11 -0500743
744def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700745 """Makes sure kernel config changes are not mixed with code changes"""
746 files = _get_affected_files(commit)
747 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
748 return HookFailure('Changes to chromeos/config/ and regular files must '
749 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700750
Mike Frysingerae409522014-02-01 03:16:11 -0500751
752def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700753 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700754 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700755 try:
756 json.load(open(f))
757 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700758 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700759
760
Mike Frysingerae409522014-02-01 03:16:11 -0500761def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400762 """Make sure Manifest files only have DIST lines"""
763 paths = []
764
765 for path in _get_affected_files(commit):
766 if os.path.basename(path) != 'Manifest':
767 continue
768 if not os.path.exists(path):
769 continue
770
771 with open(path, 'r') as f:
772 for line in f.readlines():
773 if not line.startswith('DIST '):
774 paths.append(path)
775 break
776
777 if paths:
778 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
779 ('\n'.join(paths),))
780
781
Mike Frysingerae409522014-02-01 03:16:11 -0500782def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700783 """Check for a non-empty 'BRANCH=' field in the commit message."""
784 BRANCH_RE = r'\nBRANCH=\S+'
785
786 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
787 msg = ('Changelist description needs BRANCH field (after first line)\n'
788 'E.g. BRANCH=none or BRANCH=link,snow')
789 return HookFailure(msg)
790
791
Mike Frysingerae409522014-02-01 03:16:11 -0500792def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800793 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
794 SIGNOFF_RE = r'\nSigned-off-by: \S+'
795
796 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
797 msg = ('Changelist description needs Signed-off-by: field\n'
798 'E.g. Signed-off-by: My Name <me@chromium.org>')
799 return HookFailure(msg)
800
801
Jon Salz3ee59de2012-08-18 13:54:22 +0800802def _run_project_hook_script(script, project, commit):
803 """Runs a project hook script.
804
805 The script is run with the following environment variables set:
806 PRESUBMIT_PROJECT: The affected project
807 PRESUBMIT_COMMIT: The affected commit
808 PRESUBMIT_FILES: A newline-separated list of affected files
809
810 The script is considered to fail if the exit code is non-zero. It should
811 write an error message to stdout.
812 """
813 env = dict(os.environ)
814 env['PRESUBMIT_PROJECT'] = project
815 env['PRESUBMIT_COMMIT'] = commit
816
817 # Put affected files in an environment variable
818 files = _get_affected_files(commit)
819 env['PRESUBMIT_FILES'] = '\n'.join(files)
820
821 process = subprocess.Popen(script, env=env, shell=True,
822 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800823 stdout=subprocess.PIPE,
824 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800825 stdout, _ = process.communicate()
826 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800827 if stdout:
828 stdout = re.sub('(?m)^', ' ', stdout)
829 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800830 (script, process.returncode,
831 ':\n' + stdout if stdout else ''))
832
833
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700834# Base
835
Ryan Cui1562fb82011-05-09 11:01:31 -0700836
Ryan Cui9b651632011-05-11 11:38:58 -0700837# A list of hooks that are not project-specific
838_COMMON_HOOKS = [
839 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700840 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700841 _check_change_has_test_field,
842 _check_change_has_proper_changeid,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500843 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500844 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800845 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500846 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700847 _check_no_stray_whitespace,
848 _check_no_long_lines,
849 _check_license,
850 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700851 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700852]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700853
Ryan Cui1562fb82011-05-09 11:01:31 -0700854
Ryan Cui9b651632011-05-11 11:38:58 -0700855# A dictionary of project-specific hooks(callbacks), indexed by project name.
856# dict[project] = [callback1, callback2]
857_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400858 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400859 "chromeos/overlays/chromeos-overlay": [_check_manifests],
860 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800861 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700862 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700863 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400864 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
865 _kernel_configcheck],
866 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400867 "chromiumos/overlays/board-overlays": [_check_manifests],
868 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
869 "chromiumos/overlays/portage-stable": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800870 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400871 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700872 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400873 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Shawn Nematbakhshb6ac17a2014-01-28 16:47:13 -0800874 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
875 _check_change_has_signoff_field,
876 _check_google_copyright],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700877 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400878 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
879 "chromiumos/third_party/kernel-next": [_run_checkpatch,
880 _kernel_configcheck],
881 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
882 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700883}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700884
Ryan Cui1562fb82011-05-09 11:01:31 -0700885
Ryan Cui9b651632011-05-11 11:38:58 -0700886# A dictionary of flags (keys) that can appear in the config file, and the hook
887# that the flag disables (value)
888_DISABLE_FLAGS = {
889 'stray_whitespace_check': _check_no_stray_whitespace,
890 'long_line_check': _check_no_long_lines,
891 'cros_license_check': _check_license,
892 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700893 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800894 'signoff_check': _check_change_has_signoff_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700895}
896
897
Jon Salz3ee59de2012-08-18 13:54:22 +0800898def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700899 """Returns a set of hooks disabled by the current project's config file.
900
901 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800902
903 Args:
904 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700905 """
906 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800907 if not config.has_section(SECTION):
908 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700909
910 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800911 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700912 try:
Mike Frysingerae409522014-02-01 03:16:11 -0500913 if not config.getboolean(SECTION, flag):
914 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -0700915 except ValueError as e:
916 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400917 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700918
919 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
920 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
921
922
Jon Salz3ee59de2012-08-18 13:54:22 +0800923def _get_project_hook_scripts(config):
924 """Returns a list of project-specific hook scripts.
925
926 Args:
927 config: A ConfigParser for the project's config file.
928 """
929 SECTION = 'Hook Scripts'
930 if not config.has_section(SECTION):
931 return []
932
933 hook_names_values = config.items(SECTION)
934 hook_names_values.sort(key=lambda x: x[0])
935 return [x[1] for x in hook_names_values]
936
937
Ryan Cui9b651632011-05-11 11:38:58 -0700938def _get_project_hooks(project):
939 """Returns a list of hooks that need to be run for a project.
940
941 Expects to be called from within the project root.
942 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800943 config = ConfigParser.RawConfigParser()
944 try:
945 config.read(_CONFIG_FILE)
946 except ConfigParser.Error:
947 # Just use an empty config file
948 config = ConfigParser.RawConfigParser()
949
950 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700951 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
952
953 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700954 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
955 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700956
Jon Salz3ee59de2012-08-18 13:54:22 +0800957 for script in _get_project_hook_scripts(config):
958 hooks.append(functools.partial(_run_project_hook_script, script))
959
Ryan Cui9b651632011-05-11 11:38:58 -0700960 return hooks
961
962
Doug Anderson14749562013-06-26 13:38:29 -0700963def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700964 """For each project run its project specific hook from the hooks dictionary.
965
966 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700967 project: The name of project to run hooks for.
968 proj_dir: If non-None, this is the directory the project is in. If None,
969 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700970 commit_list: A list of commits to run hooks against. If None or empty list
971 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700972
973 Returns:
974 Boolean value of whether any errors were ecountered while running the hooks.
975 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700976 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700977 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
978 if len(proj_dirs) == 0:
979 print('%s cannot be found.' % project, file=sys.stderr)
980 print('Please specify a valid project.', file=sys.stderr)
981 return True
982 if len(proj_dirs) > 1:
983 print('%s is associated with multiple directories.' % project,
984 file=sys.stderr)
985 print('Please specify a directory to help disambiguate.', file=sys.stderr)
986 return True
987 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700988
Ryan Cuiec4d6332011-05-02 14:15:25 -0700989 pwd = os.getcwd()
990 # hooks assume they are run from the root of the project
991 os.chdir(proj_dir)
992
Doug Anderson14749562013-06-26 13:38:29 -0700993 if not commit_list:
994 try:
995 commit_list = _get_commits()
996 except VerifyException as e:
997 PrintErrorForProject(project, HookFailure(str(e)))
998 os.chdir(pwd)
999 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001000
Ryan Cui9b651632011-05-11 11:38:58 -07001001 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -07001002 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001003 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001004 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001005 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001006 hook_error = hook(project, commit)
1007 if hook_error:
1008 error_list.append(hook_error)
1009 error_found = True
1010 if error_list:
1011 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1012 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001013
Ryan Cuiec4d6332011-05-02 14:15:25 -07001014 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001015 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001016
Mike Frysingerae409522014-02-01 03:16:11 -05001017
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001018# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001019
Ryan Cui1562fb82011-05-09 11:01:31 -07001020
Mike Frysingerae409522014-02-01 03:16:11 -05001021def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001022 """Main function invoked directly by repo.
1023
1024 This function will exit directly upon error so that repo doesn't print some
1025 obscure error message.
1026
1027 Args:
1028 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001029 worktree_list: A list of directories. It should be the same length as
1030 project_list, so that each entry in project_list matches with a directory
1031 in worktree_list. If None, we will attempt to calculate the directories
1032 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001033 kwargs: Leave this here for forward-compatibility.
1034 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001035 found_error = False
David James2edd9002013-10-11 14:09:19 -07001036 if not worktree_list:
1037 worktree_list = [None] * len(project_list)
1038 for project, worktree in zip(project_list, worktree_list):
1039 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001040 found_error = True
1041
Mike Frysingerae409522014-02-01 03:16:11 -05001042 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001043 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001044 '- To disable some source style checks, and for other hints, see '
1045 '<checkout_dir>/src/repohooks/README\n'
1046 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001047 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001048 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001049
Ryan Cui1562fb82011-05-09 11:01:31 -07001050
Doug Anderson44a644f2011-11-02 10:37:37 -07001051def _identify_project(path):
1052 """Identify the repo project associated with the given path.
1053
1054 Returns:
1055 A string indicating what project is associated with the path passed in or
1056 a blank string upon failure.
1057 """
1058 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1059 stderr=subprocess.PIPE, cwd=path).strip()
1060
1061
1062def direct_main(args, verbose=False):
1063 """Run hooks directly (outside of the context of repo).
1064
1065 # Setup for doctests below.
1066 # ...note that some tests assume that running pre-upload on this CWD is fine.
1067 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1068 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1069 >>> olddir = os.getcwd()
1070
1071 # OK to run w/ no arugments; will run with CWD.
1072 >>> os.chdir(mydir)
1073 >>> direct_main(['prog_name'], verbose=True)
1074 Running hooks on chromiumos/repohooks
1075 0
1076 >>> os.chdir(olddir)
1077
1078 # Run specifying a dir
1079 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1080 Running hooks on chromiumos/repohooks
1081 0
1082
1083 # Not a problem to use a bogus project; we'll just get default settings.
1084 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1085 Running hooks on X
1086 0
1087
1088 # Run with project but no dir
1089 >>> os.chdir(mydir)
1090 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1091 Running hooks on X
1092 0
1093 >>> os.chdir(olddir)
1094
1095 # Try with a non-git CWD
1096 >>> os.chdir('/tmp')
1097 >>> direct_main(['prog_name'])
1098 Traceback (most recent call last):
1099 ...
1100 BadInvocation: The current directory is not part of a git project.
1101
1102 # Check various bad arguments...
1103 >>> direct_main(['prog_name', 'bogus'])
1104 Traceback (most recent call last):
1105 ...
1106 BadInvocation: Unexpected arguments: bogus
1107 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1108 Traceback (most recent call last):
1109 ...
1110 BadInvocation: Invalid dir: bogusdir
1111 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1112 Traceback (most recent call last):
1113 ...
1114 BadInvocation: Not a git directory: /tmp
1115
1116 Args:
1117 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001118 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001119
1120 Returns:
1121 0 if no pre-upload failures, 1 if failures.
1122
1123 Raises:
1124 BadInvocation: On some types of invocation errors.
1125 """
1126 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1127 parser = optparse.OptionParser(description=desc)
1128
1129 parser.add_option('--dir', default=None,
1130 help='The directory that the project lives in. If not '
1131 'specified, use the git project root based on the cwd.')
1132 parser.add_option('--project', default=None,
1133 help='The project repo path; this can affect how the hooks '
1134 'get run, since some hooks are project-specific. For '
1135 'chromite this is chromiumos/chromite. If not specified, '
1136 'the repo tool will be used to figure this out based on '
1137 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001138 parser.add_option('--rerun-since', default=None,
1139 help='Rerun hooks on old commits since the given date. '
1140 'The date should match git log\'s concept of a date. '
1141 'e.g. 2012-06-20')
1142
1143 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001144
1145 opts, args = parser.parse_args(args[1:])
1146
Doug Anderson14749562013-06-26 13:38:29 -07001147 if opts.rerun_since:
1148 if args:
1149 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1150 ' '.join(args))
1151
1152 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1153 all_commits = _run_command(cmd).splitlines()
1154 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1155
1156 # Eliminate chrome-bot commits but keep ordering the same...
1157 bot_commits = set(bot_commits)
1158 args = [c for c in all_commits if c not in bot_commits]
1159
Doug Anderson44a644f2011-11-02 10:37:37 -07001160
1161 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1162 # project from CWD
1163 if opts.dir is None:
1164 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1165 stderr=subprocess.PIPE).strip()
1166 if not git_dir:
1167 raise BadInvocation('The current directory is not part of a git project.')
1168 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1169 elif not os.path.isdir(opts.dir):
1170 raise BadInvocation('Invalid dir: %s' % opts.dir)
1171 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1172 raise BadInvocation('Not a git directory: %s' % opts.dir)
1173
1174 # Identify the project if it wasn't specified; this _requires_ the repo
1175 # tool to be installed and for the project to be part of a repo checkout.
1176 if not opts.project:
1177 opts.project = _identify_project(opts.dir)
1178 if not opts.project:
1179 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1180
1181 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001182 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001183
Doug Anderson14749562013-06-26 13:38:29 -07001184 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
1185 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -07001186 if found_error:
1187 return 1
1188 return 0
1189
1190
1191def _test():
1192 """Run any built-in tests."""
1193 import doctest
1194 doctest.testmod()
1195
1196
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001197if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001198 if sys.argv[1:2] == ["--test"]:
1199 _test()
1200 exit_code = 0
1201 else:
1202 prog_name = os.path.basename(sys.argv[0])
1203 try:
1204 exit_code = direct_main(sys.argv)
1205 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001206 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001207 exit_code = 1
1208 sys.exit(exit_code)