blob: 028d25668874d17bee6fdfbb7c2d1e553fbaca00 [file] [log] [blame]
Doug Anderson44a644f2011-11-02 10:37:37 -07001#!/usr/bin/env python
Jon Salz98255932012-08-18 14:48:02 +08002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysingerae409522014-02-01 03:16:11 -05006"""Presubmit checks to run when doing `repo upload`.
7
8You can add new checks by adding a functions to the HOOKS constants.
9"""
10
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040011from __future__ import print_function
12
Ryan Cui9b651632011-05-11 11:38:58 -070013import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +080014import functools
Dale Curtis2975c432011-05-03 17:25:20 -070015import json
Doug Anderson44a644f2011-11-02 10:37:37 -070016import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070017import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070018import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070019import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070020import subprocess
Peter Ammon811f6702014-06-12 15:45:38 -070021import stat
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070022
Ryan Cui1562fb82011-05-09 11:01:31 -070023from errors import (VerifyException, HookFailure, PrintErrorForProject,
24 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070025
David Jamesc3b68b32013-04-03 09:17:03 -070026# If repo imports us, the __name__ will be __builtin__, and the wrapper will
27# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
28# up. The same logic also happens to work if we're executed directly.
29if __name__ in ('__builtin__', '__main__'):
30 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
31
32from chromite.lib import patch
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",
Mike Frysinger33a458d2014-03-03 17:00:51 -050068
69 # Ignore license files as the content is often taken verbatim.
70 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070071]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070072
Ryan Cui1562fb82011-05-09 11:01:31 -070073
Ryan Cui9b651632011-05-11 11:38:58 -070074_CONFIG_FILE = 'PRESUBMIT.cfg'
75
76
Doug Anderson44a644f2011-11-02 10:37:37 -070077# Exceptions
78
79
80class BadInvocation(Exception):
81 """An Exception indicating a bad invocation of the program."""
82 pass
83
84
Ryan Cui1562fb82011-05-09 11:01:31 -070085# General Helpers
86
Sean Paulba01d402011-05-05 11:36:23 -040087
Doug Anderson44a644f2011-11-02 10:37:37 -070088def _run_command(cmd, cwd=None, stderr=None):
89 """Executes the passed in command and returns raw stdout output.
90
91 Args:
92 cmd: The command to run; should be a list of strings.
93 cwd: The directory to switch to for running the command.
94 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
95 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
96
97 Returns:
98 The standard out from the process.
99 """
100 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
101 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700102
Ryan Cui1562fb82011-05-09 11:01:31 -0700103
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700104def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700105 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700106 if __name__ == '__main__':
107 # Works when file is run on its own (__file__ is defined)...
108 return os.path.abspath(os.path.dirname(__file__))
109 else:
110 # We need to do this when we're run through repo. Since repo executes
111 # us with execfile(), we don't get __file__ defined.
112 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
113 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700114
Ryan Cui1562fb82011-05-09 11:01:31 -0700115
Ryan Cuiec4d6332011-05-02 14:15:25 -0700116def _match_regex_list(subject, expressions):
117 """Try to match a list of regular expressions to a string.
118
119 Args:
120 subject: The string to match regexes on
121 expressions: A list of regular expressions to check for matches with.
122
123 Returns:
124 Whether the passed in subject matches any of the passed in regexes.
125 """
126 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500127 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700128 return True
129 return False
130
Ryan Cui1562fb82011-05-09 11:01:31 -0700131
Mike Frysingerae409522014-02-01 03:16:11 -0500132def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700133 """Filter out files based on the conditions passed in.
134
135 Args:
136 files: list of filepaths to filter
137 include_list: list of regex that when matched with a file path will cause it
138 to be added to the output list unless the file is also matched with a
139 regex in the exclude_list.
140 exclude_list: list of regex that when matched with a file will prevent it
141 from being added to the output list, even if it is also matched with a
142 regex in the include_list.
143
144 Returns:
145 A list of filepaths that contain files matched in the include_list and not
146 in the exclude_list.
147 """
148 filtered = []
149 for f in files:
150 if (_match_regex_list(f, include_list) and
151 not _match_regex_list(f, exclude_list)):
152 filtered.append(f)
153 return filtered
154
Ryan Cuiec4d6332011-05-02 14:15:25 -0700155
David Hendricks35030d02013-02-04 17:49:16 -0800156def _verify_header_content(commit, content, fail_msg):
157 """Verify that file headers contain specified content.
158
159 Args:
160 commit: the affected commit.
161 content: the content of the header to be verified.
162 fail_msg: the first message to display in case of failure.
163
Mike Frysinger33a458d2014-03-03 17:00:51 -0500164 Returns:
165 The return value of HookFailure().
David Hendricks35030d02013-02-04 17:49:16 -0800166 """
167 license_re = re.compile(content, re.MULTILINE)
168 bad_files = []
169 files = _filter_files(_get_affected_files(commit),
170 COMMON_INCLUDED_PATHS,
171 COMMON_EXCLUDED_PATHS)
172
173 for f in files:
Tom Wai-Hong Tam667db5d2014-02-27 06:28:14 +0800174 # Ignore non-existant files and symlinks
175 if os.path.exists(f) and not os.path.islink(f):
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800176 contents = open(f).read()
Mike Frysingerae409522014-02-01 03:16:11 -0500177 if not contents:
178 # Ignore empty files
179 continue
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800180 if not license_re.search(contents):
181 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800182 if bad_files:
Mike Frysingerae409522014-02-01 03:16:11 -0500183 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
184 "Found a bad header in these files:")
185 return HookFailure(msg, bad_files)
David Hendricks35030d02013-02-04 17:49:16 -0800186
187
Ryan Cuiec4d6332011-05-02 14:15:25 -0700188# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700189
190
Ryan Cui4725d952011-05-05 15:41:19 -0700191def _get_upstream_branch():
192 """Returns the upstream tracking branch of the current branch.
193
194 Raises:
195 Error if there is no tracking branch
196 """
197 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
198 current_branch = current_branch.replace('refs/heads/', '')
199 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700200 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700201
202 cfg_option = 'branch.' + current_branch + '.%s'
203 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
204 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
205 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700206 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700207
208 return full_upstream.replace('heads', 'remotes/' + remote)
209
Ryan Cui1562fb82011-05-09 11:01:31 -0700210
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700211def _get_patch(commit):
212 """Returns the patch for this commit."""
213 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700214
Ryan Cui1562fb82011-05-09 11:01:31 -0700215
Jon Salz98255932012-08-18 14:48:02 +0800216def _try_utf8_decode(data):
217 """Attempts to decode a string as UTF-8.
218
219 Returns:
220 The decoded Unicode object, or the original string if parsing fails.
221 """
222 try:
223 return unicode(data, 'utf-8', 'strict')
224 except UnicodeDecodeError:
225 return data
226
227
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500228def _get_file_content(path, commit):
229 """Returns the content of a file at a specific commit.
230
231 We can't rely on the file as it exists in the filesystem as people might be
232 uploading a series of changes which modifies the file multiple times.
233
234 Note: The "content" of a symlink is just the target. So if you're expecting
235 a full file, you should check that first. One way to detect is that the
236 content will not have any newlines.
237 """
238 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
239
240
Mike Frysingerae409522014-02-01 03:16:11 -0500241def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700242 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysinger5122a1f2014-02-01 02:47:35 -0500243 output = _run_command(['git', 'show', '-p', '--pretty=format:',
Mike Frysingerae409522014-02-01 03:16:11 -0500244 '--no-ext-diff', commit, path])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700245
246 new_lines = []
247 line_num = 0
248 for line in output.splitlines():
249 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
250 if m:
251 line_num = int(m.groups(1)[0])
252 continue
253 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800254 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700255 if not line.startswith('-'):
256 line_num += 1
257 return new_lines
258
Ryan Cui1562fb82011-05-09 11:01:31 -0700259
Peter Ammon811f6702014-06-12 15:45:38 -0700260def _parse_affected_files(output, include_deletes=False, relative=False):
261 """Parses git diff's 'raw' format, returning a list of modified file paths.
262
263 This excludes directories and symlinks, and optionally includes files that
264 were deleted.
Doug Anderson42b8a052013-06-26 10:45:36 -0700265
266 Args:
Peter Ammon811f6702014-06-12 15:45:38 -0700267 output: The result of the 'git diff --raw' command
268 include_deletes: If true, we'll include deleted files in the result
269 relative: Whether to return relative or full paths to files
Doug Anderson42b8a052013-06-26 10:45:36 -0700270
271 Returns:
272 A list of modified/added (and perhaps deleted) files
273 """
Ryan Cuiec4d6332011-05-02 14:15:25 -0700274 files = []
Peter Ammon811f6702014-06-12 15:45:38 -0700275 # See the documentation for 'git diff --raw' for the relevant format.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700276 for statusline in output.splitlines():
Peter Ammon811f6702014-06-12 15:45:38 -0700277 attributes, paths = statusline.split('\t', 1)
278 _, mode, _, _, status = attributes.split(' ')
279
280 # Ignore symlinks and directories.
281 imode = int(mode, 8)
282 if stat.S_ISDIR(imode) or stat.S_ISLNK(imode):
283 continue
284
285 # Ignore deleted files, and optionally return absolute paths of files.
286 if include_deletes or status != 'D':
287 # If a file was merely modified, we will have a single file path.
288 # If it was moved, we will have two paths (source and destination).
289 # In either case, we want the last path.
290 f = paths.split('\t')[-1]
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500291 if not relative:
292 pwd = os.getcwd()
293 f = os.path.join(pwd, f)
294 files.append(f)
Peter Ammon811f6702014-06-12 15:45:38 -0700295
Ryan Cuiec4d6332011-05-02 14:15:25 -0700296 return files
297
Ryan Cui1562fb82011-05-09 11:01:31 -0700298
Peter Ammon811f6702014-06-12 15:45:38 -0700299def _get_affected_files(commit, include_deletes=False, relative=False):
300 """Returns list of file paths that were modified/added, excluding symlinks.
301
302 Args:
303 commit: The commit
304 include_deletes: If true, we'll include deleted files in the result
305 relative: Whether to return relative or full paths to files
306
307 Returns:
308 A list of modified/added (and perhaps deleted) files
309 """
310 output = _run_command(['git', 'diff', '--raw', commit + '^!'])
311 return _parse_affected_files(output, include_deletes, relative)
312
313
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700314def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700315 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700316 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700317 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700318
Ryan Cui1562fb82011-05-09 11:01:31 -0700319
Ryan Cuiec4d6332011-05-02 14:15:25 -0700320def _get_commit_desc(commit):
321 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400322 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700323
324
325# Common Hooks
326
Ryan Cui1562fb82011-05-09 11:01:31 -0700327
Mike Frysingerae409522014-02-01 03:16:11 -0500328def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700329 """Checks that there aren't any lines longer than maxlen characters in any of
330 the text files to be submitted.
331 """
332 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800333 SKIP_REGEXP = re.compile('|'.join([
334 r'https?://',
335 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700336
337 errors = []
338 files = _filter_files(_get_affected_files(commit),
339 COMMON_INCLUDED_PATHS,
340 COMMON_EXCLUDED_PATHS)
341
342 for afile in files:
343 for line_num, line in _get_file_diff(afile, commit):
344 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500345 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800346 continue
347
348 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
349 if len(errors) == 5: # Just show the first 5 errors.
350 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700351
352 if errors:
353 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700354 return HookFailure(msg, errors)
355
Ryan Cuiec4d6332011-05-02 14:15:25 -0700356
Mike Frysingerae409522014-02-01 03:16:11 -0500357def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700358 """Checks that there is no stray whitespace at source lines end."""
359 errors = []
360 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500361 COMMON_INCLUDED_PATHS,
362 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700363 for afile in files:
364 for line_num, line in _get_file_diff(afile, commit):
365 if line.rstrip() != line:
366 errors.append('%s, line %s' % (afile, line_num))
367 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700368 return HookFailure('Found line ending with white space in:', errors)
369
Ryan Cuiec4d6332011-05-02 14:15:25 -0700370
Mike Frysingerae409522014-02-01 03:16:11 -0500371def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700372 """Checks there are no unexpanded tabs."""
373 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700374 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700375 r".*\.ebuild$",
376 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500377 r".*/[M|m]akefile$",
378 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700379 ]
380
381 errors = []
382 files = _filter_files(_get_affected_files(commit),
383 COMMON_INCLUDED_PATHS,
384 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
385
386 for afile in files:
387 for line_num, line in _get_file_diff(afile, commit):
388 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500389 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700390 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700391 return HookFailure('Found a tab character in:', errors)
392
Ryan Cuiec4d6332011-05-02 14:15:25 -0700393
Mike Frysingerae409522014-02-01 03:16:11 -0500394def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700395 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700396 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700397
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700398 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700399 msg = 'Changelist description needs TEST field (after first line)'
400 return HookFailure(msg)
401
Ryan Cuiec4d6332011-05-02 14:15:25 -0700402
Mike Frysingerae409522014-02-01 03:16:11 -0500403def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700404 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
405 msg = 'Changelist has invalid CQ-DEPEND target.'
406 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
407 try:
408 patch.GetPaladinDeps(_get_commit_desc(commit))
409 except ValueError as ex:
410 return HookFailure(msg, [example, str(ex)])
411
412
Mike Frysingerae409522014-02-01 03:16:11 -0500413def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700414 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700415 OLD_BUG_RE = r'\nBUG=.*chromium-os'
416 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
417 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
418 'the chromium tracker in your BUG= line now.')
419 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700420
David James5c0073d2013-04-03 08:48:52 -0700421 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700422 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700423 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700424 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700425 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
426 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700427 return HookFailure(msg)
428
Ryan Cuiec4d6332011-05-02 14:15:25 -0700429
Doug Anderson42b8a052013-06-26 10:45:36 -0700430def _check_for_uprev(project, commit):
431 """Check that we're not missing a revbump of an ebuild in the given commit.
432
433 If the given commit touches files in a directory that has ebuilds somewhere
434 up the directory hierarchy, it's very likely that we need an ebuild revbump
435 in order for those changes to take effect.
436
437 It's not totally trivial to detect a revbump, so at least detect that an
438 ebuild with a revision number in it was touched. This should handle the
439 common case where we use a symlink to do the revbump.
440
441 TODO: it would be nice to enhance this hook to:
442 * Handle cases where people revbump with a slightly different syntax. I see
443 one ebuild (puppy) that revbumps with _pN. This is a false positive.
444 * Catches cases where people aren't using symlinks for revbumps. If they
445 edit a revisioned file directly (and are expected to rename it for revbump)
446 we'll miss that. Perhaps we could detect that the file touched is a
447 symlink?
448
449 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
450 still better off than without this check.
451
452 Args:
453 project: The project to look at
454 commit: The commit to look at
455
456 Returns:
457 A HookFailure or None.
458 """
Mike Frysinger011af942014-01-17 16:12:22 -0500459 # If this is the portage-stable overlay, then ignore the check. It's rare
460 # that we're doing anything other than importing files from upstream, so
461 # forcing a rev bump makes no sense.
462 whitelist = (
463 'chromiumos/overlays/portage-stable',
464 )
465 if project in whitelist:
466 return None
467
Doug Anderson42b8a052013-06-26 10:45:36 -0700468 affected_paths = _get_affected_files(commit, include_deletes=True)
469
470 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500471 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700472 affected_paths = [path for path in affected_paths
473 if os.path.basename(path) not in whitelist]
474 if not affected_paths:
475 return None
476
477 # If we've touched any file named with a -rN.ebuild then we'll say we're
478 # OK right away. See TODO above about enhancing this.
479 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
480 for path in affected_paths)
481 if touched_revved_ebuild:
482 return None
483
484 # We want to examine the current contents of all directories that are parents
485 # of files that were touched (up to the top of the project).
486 #
487 # ...note: we use the current directory contents even though it may have
488 # changed since the commit we're looking at. This is just a heuristic after
489 # all. Worst case we don't flag a missing revbump.
490 project_top = os.getcwd()
491 dirs_to_check = set([project_top])
492 for path in affected_paths:
493 path = os.path.dirname(path)
494 while os.path.exists(path) and not os.path.samefile(path, project_top):
495 dirs_to_check.add(path)
496 path = os.path.dirname(path)
497
498 # Look through each directory. If it's got an ebuild in it then we'll
499 # consider this as a case when we need a revbump.
500 for dir_path in dirs_to_check:
501 contents = os.listdir(dir_path)
502 ebuilds = [os.path.join(dir_path, path)
503 for path in contents if path.endswith('.ebuild')]
504 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
505
506 # If the -9999.ebuild file was touched the bot will uprev for us.
507 # ...we'll use a simple intersection here as a heuristic...
508 if set(ebuilds_9999) & set(affected_paths):
509 continue
510
511 if ebuilds:
512 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
513 'or a -r1.ebuild symlink if this is a new ebuild')
514
515 return None
516
517
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500518def _check_ebuild_eapi(project, commit):
519 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
520
521 We want to get away from older EAPI's as it makes life confusing and they
522 have less builtin error checking.
523
524 Args:
525 project: The project to look at
526 commit: The commit to look at
527
528 Returns:
529 A HookFailure or None.
530 """
531 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500532 # that we're doing anything other than importing files from upstream, and
533 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500534 whitelist = (
535 'chromiumos/overlays/portage-stable',
536 )
537 if project in whitelist:
538 return None
539
540 BAD_EAPIS = ('0', '1', '2', '3')
541
542 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
543
544 ebuilds_re = [r'\.ebuild$']
545 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
546 ebuilds_re)
547 bad_ebuilds = []
548
549 for ebuild in ebuilds:
550 # If the ebuild does not specify an EAPI, it defaults to 0.
551 eapi = '0'
552
553 lines = _get_file_content(ebuild, commit).splitlines()
554 if len(lines) == 1:
555 # This is most likely a symlink, so skip it entirely.
556 continue
557
558 for line in lines:
559 m = get_eapi.match(line)
560 if m:
561 # Once we hit the first EAPI line in this ebuild, stop processing.
562 # The spec requires that there only be one and it be first, so
563 # checking all possible values is pointless. We also assume that
564 # it's "the" EAPI line and not something in the middle of a heredoc.
565 eapi = m.group(1)
566 break
567
568 if eapi in BAD_EAPIS:
569 bad_ebuilds.append((ebuild, eapi))
570
571 if bad_ebuilds:
572 # pylint: disable=C0301
573 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
574 # pylint: enable=C0301
575 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500576 'These ebuilds are using old EAPIs. If these are imported from\n'
577 'Gentoo, then you may ignore and upload once with the --no-verify\n'
578 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500579 '\t%s\n'
580 'See this guide for more details:\n%s\n' %
581 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
582
583
Mike Frysinger89bdb852014-02-01 05:26:26 -0500584def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500585 """Make sure we use the new style KEYWORDS when possible in ebuilds.
586
587 If an ebuild generally does not care about the arch it is running on, then
588 ebuilds should flag it with one of:
589 KEYWORDS="*" # A stable ebuild.
590 KEYWORDS="~*" # An unstable ebuild.
591 KEYWORDS="-* ..." # Is known to only work on specific arches.
592
593 Args:
594 project: The project to look at
595 commit: The commit to look at
596
597 Returns:
598 A HookFailure or None.
599 """
600 WHITELIST = set(('*', '-*', '~*'))
601
602 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
603
Mike Frysinger89bdb852014-02-01 05:26:26 -0500604 ebuilds_re = [r'\.ebuild$']
605 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
606 ebuilds_re)
607
Mike Frysingerc51ece72014-01-17 16:23:40 -0500608 for ebuild in ebuilds:
609 for _, line in _get_file_diff(ebuild, commit):
610 m = get_keywords.match(line)
611 if m:
612 keywords = set(m.group(1).split())
613 if not keywords or WHITELIST - keywords != WHITELIST:
614 continue
615
616 return HookFailure(
617 'Please update KEYWORDS to use a glob:\n'
618 'If the ebuild should be marked stable (normal for non-9999 '
619 'ebuilds):\n'
620 ' KEYWORDS="*"\n'
621 'If the ebuild should be marked unstable (normal for '
622 'cros-workon / 9999 ebuilds):\n'
623 ' KEYWORDS="~*"\n'
624 'If the ebuild needs to be marked for only specific arches,'
625 'then use -* like so:\n'
626 ' KEYWORDS="-* arm ..."\n')
627
628
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800629def _check_ebuild_licenses(_project, commit):
630 """Check if the LICENSE field in the ebuild is correct."""
631 affected_paths = _get_affected_files(commit)
632 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
633
634 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800635 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800636
637 for ebuild in touched_ebuilds:
638 # Skip virutal packages.
639 if ebuild.split('/')[-3] == 'virtual':
640 continue
641
642 try:
643 license_types = licenses.GetLicenseTypesFromEbuild(ebuild)
644 except ValueError as e:
645 return HookFailure(e.message, [ebuild])
646
647 # Also ignore licenses ending with '?'
648 for license_type in [x for x in license_types
649 if x not in LICENSES_IGNORE and not x.endswith('?')]:
650 try:
651 licenses.Licensing.FindLicenseType(license_type)
652 except AssertionError as e:
653 return HookFailure(e.message, [ebuild])
654
655
Mike Frysingercd363c82014-02-01 05:20:18 -0500656def _check_ebuild_virtual_pv(project, commit):
657 """Enforce the virtual PV policies."""
658 # If this is the portage-stable overlay, then ignore the check.
659 # We want to import virtuals as-is from upstream Gentoo.
660 whitelist = (
661 'chromiumos/overlays/portage-stable',
662 )
663 if project in whitelist:
664 return None
665
666 # We assume the repo name is the same as the dir name on disk.
667 # It would be dumb to not have them match though.
668 project = os.path.basename(project)
669
670 is_variant = lambda x: x.startswith('overlay-variant-')
671 is_board = lambda x: x.startswith('overlay-')
672 is_private = lambda x: x.endswith('-private')
673
674 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
675
676 ebuilds_re = [r'\.ebuild$']
677 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
678 ebuilds_re)
679 bad_ebuilds = []
680
681 for ebuild in ebuilds:
682 m = get_pv.match(ebuild)
683 if m:
684 overlay = m.group(1)
685 if not overlay or not is_board(overlay):
686 overlay = project
687
688 pv = m.group(3).split('-', 1)[0]
689
690 if is_private(overlay):
691 want_pv = '3.5' if is_variant(overlay) else '3'
692 elif is_board(overlay):
693 want_pv = '2.5' if is_variant(overlay) else '2'
694 else:
695 want_pv = '1'
696
697 if pv != want_pv:
698 bad_ebuilds.append((ebuild, pv, want_pv))
699
700 if bad_ebuilds:
701 # pylint: disable=C0301
702 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
703 # pylint: enable=C0301
704 return HookFailure(
705 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
706 '\t%s\n'
707 'If this is an upstream Gentoo virtual, then you may ignore this\n'
708 'check (and re-run w/--no-verify). Otherwise, please see this\n'
709 'page for more details:\n%s\n' %
710 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
711 for x in bad_ebuilds]), url))
712
713
Mike Frysingerae409522014-02-01 03:16:11 -0500714def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700715 """Verify that Change-ID is present in last paragraph of commit message."""
716 desc = _get_commit_desc(commit)
717 loc = desc.rfind('\nChange-Id:')
718 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700719 return HookFailure('Change-Id must be in last paragraph of description.')
720
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700721
Mike Frysingerae409522014-02-01 03:16:11 -0500722def _check_license(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700723 """Verifies the license header."""
724 LICENSE_HEADER = (
Chris Sosaed7a3fa2014-02-26 12:18:31 -0800725 r".* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. "
726 "All rights reserved\." "\n"
727 r".* Use of this source code is governed by a BSD-style license that can "
728 "be\n"
729 r".* found in the LICENSE file\."
730 "\n"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700731 )
David Hendricks35030d02013-02-04 17:49:16 -0800732 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700733
David Hendricks35030d02013-02-04 17:49:16 -0800734 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700735
736
737# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700738
Ryan Cui1562fb82011-05-09 11:01:31 -0700739
Mike Frysingerae409522014-02-01 03:16:11 -0500740def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700741 """Runs checkpatch.pl on the given project"""
742 hooks_dir = _get_hooks_dir()
Mike Frysingerae409522014-02-01 03:16:11 -0500743 cmd = ['%s/checkpatch.pl' % hooks_dir] + list(options) + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700744 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700745 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700746 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700747 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700748
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700749
Anton Staaf815d6852011-08-22 10:08:45 -0700750def _run_checkpatch_no_tree(project, commit):
751 return _run_checkpatch(project, commit, ['--no-tree'])
752
Mike Frysingerae409522014-02-01 03:16:11 -0500753
Randall Spangler7318fd62013-11-21 12:16:58 -0800754def _run_checkpatch_ec(project, commit):
755 """Runs checkpatch with options for Chromium EC projects."""
756 return _run_checkpatch(project, commit, ['--no-tree',
757 '--ignore=MSLEEP,VOLATILE'])
758
Mike Frysingerae409522014-02-01 03:16:11 -0500759
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700760def _run_checkpatch_depthcharge(project, commit):
761 """Runs checkpatch with options for depthcharge."""
762 return _run_checkpatch(project, commit, [
763 '--no-tree',
764 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION'])
765
766
Mike Frysingerae409522014-02-01 03:16:11 -0500767def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700768 """Makes sure kernel config changes are not mixed with code changes"""
769 files = _get_affected_files(commit)
770 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
771 return HookFailure('Changes to chromeos/config/ and regular files must '
772 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700773
Mike Frysingerae409522014-02-01 03:16:11 -0500774
775def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700776 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700777 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700778 try:
779 json.load(open(f))
780 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700781 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700782
783
Mike Frysingerae409522014-02-01 03:16:11 -0500784def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400785 """Make sure Manifest files only have DIST lines"""
786 paths = []
787
788 for path in _get_affected_files(commit):
789 if os.path.basename(path) != 'Manifest':
790 continue
791 if not os.path.exists(path):
792 continue
793
794 with open(path, 'r') as f:
795 for line in f.readlines():
796 if not line.startswith('DIST '):
797 paths.append(path)
798 break
799
800 if paths:
801 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
802 ('\n'.join(paths),))
803
804
Mike Frysingerae409522014-02-01 03:16:11 -0500805def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700806 """Check for a non-empty 'BRANCH=' field in the commit message."""
807 BRANCH_RE = r'\nBRANCH=\S+'
808
809 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
810 msg = ('Changelist description needs BRANCH field (after first line)\n'
811 'E.g. BRANCH=none or BRANCH=link,snow')
812 return HookFailure(msg)
813
814
Mike Frysingerae409522014-02-01 03:16:11 -0500815def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800816 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
817 SIGNOFF_RE = r'\nSigned-off-by: \S+'
818
819 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
820 msg = ('Changelist description needs Signed-off-by: field\n'
821 'E.g. Signed-off-by: My Name <me@chromium.org>')
822 return HookFailure(msg)
823
824
Jon Salz3ee59de2012-08-18 13:54:22 +0800825def _run_project_hook_script(script, project, commit):
826 """Runs a project hook script.
827
828 The script is run with the following environment variables set:
829 PRESUBMIT_PROJECT: The affected project
830 PRESUBMIT_COMMIT: The affected commit
831 PRESUBMIT_FILES: A newline-separated list of affected files
832
833 The script is considered to fail if the exit code is non-zero. It should
834 write an error message to stdout.
835 """
836 env = dict(os.environ)
837 env['PRESUBMIT_PROJECT'] = project
838 env['PRESUBMIT_COMMIT'] = commit
839
840 # Put affected files in an environment variable
841 files = _get_affected_files(commit)
842 env['PRESUBMIT_FILES'] = '\n'.join(files)
843
844 process = subprocess.Popen(script, env=env, shell=True,
845 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800846 stdout=subprocess.PIPE,
847 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800848 stdout, _ = process.communicate()
849 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800850 if stdout:
851 stdout = re.sub('(?m)^', ' ', stdout)
852 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800853 (script, process.returncode,
854 ':\n' + stdout if stdout else ''))
855
856
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700857def _moved_to_platform2(project, _commit):
858 """Forbids commits to legacy repo in src/platform."""
859 return HookFailure('%s has been moved to platform2. This change should be '
860 'made there.' % project)
861
862
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700863# Base
864
Ryan Cui1562fb82011-05-09 11:01:31 -0700865
Ryan Cui9b651632011-05-11 11:38:58 -0700866# A list of hooks that are not project-specific
867_COMMON_HOOKS = [
868 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700869 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700870 _check_change_has_test_field,
871 _check_change_has_proper_changeid,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500872 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500873 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800874 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500875 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700876 _check_no_stray_whitespace,
877 _check_no_long_lines,
878 _check_license,
879 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700880 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700881]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700882
Ryan Cui1562fb82011-05-09 11:01:31 -0700883
Ryan Cui9b651632011-05-11 11:38:58 -0700884# A dictionary of project-specific hooks(callbacks), indexed by project name.
885# dict[project] = [callback1, callback2]
886_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400887 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400888 "chromeos/overlays/chromeos-overlay": [_check_manifests],
889 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800890 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700891 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700892 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400893 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
894 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400895 "chromiumos/overlays/board-overlays": [_check_manifests],
896 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
897 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700898 # TODO(bsimonnet): remove this check once src/platform/common-mk has been
899 # removed from the manifest (crbug.com/379236).
900 "chromiumos/platform/common-mk": [_moved_to_platform2],
Bertrand SIMONNET6710d292014-06-02 15:29:23 -0700901 # TODO(bsimonnet): remove this check once src/platform/libchromeos has been
902 # removed from the manifest (crbug.com/379939)
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700903 "chromiumos/platform/depthcharge": [_run_checkpatch_depthcharge],
Bertrand SIMONNET6710d292014-06-02 15:29:23 -0700904 "chromiumos/platform/libchromeos": [_moved_to_platform2],
Randall Spangler7318fd62013-11-21 12:16:58 -0800905 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400906 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700907 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400908 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury203fbc22014-04-22 11:58:14 -0700909 "chromiumos/third_party/coreboot": [_check_change_has_signoff_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700910 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400911 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
912 "chromiumos/third_party/kernel-next": [_run_checkpatch,
913 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -0700914 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -0700915}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700916
Ryan Cui1562fb82011-05-09 11:01:31 -0700917
Ryan Cui9b651632011-05-11 11:38:58 -0700918# A dictionary of flags (keys) that can appear in the config file, and the hook
919# that the flag disables (value)
920_DISABLE_FLAGS = {
921 'stray_whitespace_check': _check_no_stray_whitespace,
922 'long_line_check': _check_no_long_lines,
923 'cros_license_check': _check_license,
924 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700925 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800926 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -0700927 'bug_field_check': _check_change_has_bug_field,
928 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700929}
930
931
Jon Salz3ee59de2012-08-18 13:54:22 +0800932def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700933 """Returns a set of hooks disabled by the current project's config file.
934
935 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800936
937 Args:
938 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700939 """
940 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800941 if not config.has_section(SECTION):
942 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700943
944 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800945 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700946 try:
Mike Frysingerae409522014-02-01 03:16:11 -0500947 if not config.getboolean(SECTION, flag):
948 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -0700949 except ValueError as e:
950 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400951 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700952
953 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
954 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
955
956
Jon Salz3ee59de2012-08-18 13:54:22 +0800957def _get_project_hook_scripts(config):
958 """Returns a list of project-specific hook scripts.
959
960 Args:
961 config: A ConfigParser for the project's config file.
962 """
963 SECTION = 'Hook Scripts'
964 if not config.has_section(SECTION):
965 return []
966
967 hook_names_values = config.items(SECTION)
968 hook_names_values.sort(key=lambda x: x[0])
969 return [x[1] for x in hook_names_values]
970
971
Ryan Cui9b651632011-05-11 11:38:58 -0700972def _get_project_hooks(project):
973 """Returns a list of hooks that need to be run for a project.
974
975 Expects to be called from within the project root.
976 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800977 config = ConfigParser.RawConfigParser()
978 try:
979 config.read(_CONFIG_FILE)
980 except ConfigParser.Error:
981 # Just use an empty config file
982 config = ConfigParser.RawConfigParser()
983
984 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700985 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
986
987 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700988 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
989 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700990
Jon Salz3ee59de2012-08-18 13:54:22 +0800991 for script in _get_project_hook_scripts(config):
992 hooks.append(functools.partial(_run_project_hook_script, script))
993
Ryan Cui9b651632011-05-11 11:38:58 -0700994 return hooks
995
996
Doug Anderson14749562013-06-26 13:38:29 -0700997def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700998 """For each project run its project specific hook from the hooks dictionary.
999
1000 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001001 project: The name of project to run hooks for.
1002 proj_dir: If non-None, this is the directory the project is in. If None,
1003 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001004 commit_list: A list of commits to run hooks against. If None or empty list
1005 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -07001006
1007 Returns:
1008 Boolean value of whether any errors were ecountered while running the hooks.
1009 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001010 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001011 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1012 if len(proj_dirs) == 0:
1013 print('%s cannot be found.' % project, file=sys.stderr)
1014 print('Please specify a valid project.', file=sys.stderr)
1015 return True
1016 if len(proj_dirs) > 1:
1017 print('%s is associated with multiple directories.' % project,
1018 file=sys.stderr)
1019 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1020 return True
1021 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001022
Ryan Cuiec4d6332011-05-02 14:15:25 -07001023 pwd = os.getcwd()
1024 # hooks assume they are run from the root of the project
1025 os.chdir(proj_dir)
1026
Doug Anderson14749562013-06-26 13:38:29 -07001027 if not commit_list:
1028 try:
1029 commit_list = _get_commits()
1030 except VerifyException as e:
1031 PrintErrorForProject(project, HookFailure(str(e)))
1032 os.chdir(pwd)
1033 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001034
Ryan Cui9b651632011-05-11 11:38:58 -07001035 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -07001036 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001037 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001038 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001039 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001040 hook_error = hook(project, commit)
1041 if hook_error:
1042 error_list.append(hook_error)
1043 error_found = True
1044 if error_list:
1045 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1046 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001047
Ryan Cuiec4d6332011-05-02 14:15:25 -07001048 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001049 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001050
Mike Frysingerae409522014-02-01 03:16:11 -05001051
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001052# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001053
Ryan Cui1562fb82011-05-09 11:01:31 -07001054
Mike Frysingerae409522014-02-01 03:16:11 -05001055def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001056 """Main function invoked directly by repo.
1057
1058 This function will exit directly upon error so that repo doesn't print some
1059 obscure error message.
1060
1061 Args:
1062 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001063 worktree_list: A list of directories. It should be the same length as
1064 project_list, so that each entry in project_list matches with a directory
1065 in worktree_list. If None, we will attempt to calculate the directories
1066 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001067 kwargs: Leave this here for forward-compatibility.
1068 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001069 found_error = False
David James2edd9002013-10-11 14:09:19 -07001070 if not worktree_list:
1071 worktree_list = [None] * len(project_list)
1072 for project, worktree in zip(project_list, worktree_list):
1073 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001074 found_error = True
1075
Mike Frysingerae409522014-02-01 03:16:11 -05001076 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001077 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001078 '- To disable some source style checks, and for other hints, see '
1079 '<checkout_dir>/src/repohooks/README\n'
1080 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001081 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001082 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001083
Ryan Cui1562fb82011-05-09 11:01:31 -07001084
Doug Anderson44a644f2011-11-02 10:37:37 -07001085def _identify_project(path):
1086 """Identify the repo project associated with the given path.
1087
1088 Returns:
1089 A string indicating what project is associated with the path passed in or
1090 a blank string upon failure.
1091 """
1092 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1093 stderr=subprocess.PIPE, cwd=path).strip()
1094
1095
1096def direct_main(args, verbose=False):
1097 """Run hooks directly (outside of the context of repo).
1098
1099 # Setup for doctests below.
1100 # ...note that some tests assume that running pre-upload on this CWD is fine.
1101 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1102 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1103 >>> olddir = os.getcwd()
1104
1105 # OK to run w/ no arugments; will run with CWD.
1106 >>> os.chdir(mydir)
1107 >>> direct_main(['prog_name'], verbose=True)
1108 Running hooks on chromiumos/repohooks
1109 0
1110 >>> os.chdir(olddir)
1111
1112 # Run specifying a dir
1113 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1114 Running hooks on chromiumos/repohooks
1115 0
1116
1117 # Not a problem to use a bogus project; we'll just get default settings.
1118 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1119 Running hooks on X
1120 0
1121
1122 # Run with project but no dir
1123 >>> os.chdir(mydir)
1124 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1125 Running hooks on X
1126 0
1127 >>> os.chdir(olddir)
1128
1129 # Try with a non-git CWD
1130 >>> os.chdir('/tmp')
1131 >>> direct_main(['prog_name'])
1132 Traceback (most recent call last):
1133 ...
1134 BadInvocation: The current directory is not part of a git project.
1135
1136 # Check various bad arguments...
1137 >>> direct_main(['prog_name', 'bogus'])
1138 Traceback (most recent call last):
1139 ...
1140 BadInvocation: Unexpected arguments: bogus
1141 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1142 Traceback (most recent call last):
1143 ...
1144 BadInvocation: Invalid dir: bogusdir
1145 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1146 Traceback (most recent call last):
1147 ...
1148 BadInvocation: Not a git directory: /tmp
1149
1150 Args:
1151 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001152 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001153
1154 Returns:
1155 0 if no pre-upload failures, 1 if failures.
1156
1157 Raises:
1158 BadInvocation: On some types of invocation errors.
1159 """
1160 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1161 parser = optparse.OptionParser(description=desc)
1162
1163 parser.add_option('--dir', default=None,
1164 help='The directory that the project lives in. If not '
1165 'specified, use the git project root based on the cwd.')
1166 parser.add_option('--project', default=None,
1167 help='The project repo path; this can affect how the hooks '
1168 'get run, since some hooks are project-specific. For '
1169 'chromite this is chromiumos/chromite. If not specified, '
1170 'the repo tool will be used to figure this out based on '
1171 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001172 parser.add_option('--rerun-since', default=None,
1173 help='Rerun hooks on old commits since the given date. '
1174 'The date should match git log\'s concept of a date. '
1175 'e.g. 2012-06-20')
1176
1177 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001178
1179 opts, args = parser.parse_args(args[1:])
1180
Doug Anderson14749562013-06-26 13:38:29 -07001181 if opts.rerun_since:
1182 if args:
1183 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1184 ' '.join(args))
1185
1186 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1187 all_commits = _run_command(cmd).splitlines()
1188 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1189
1190 # Eliminate chrome-bot commits but keep ordering the same...
1191 bot_commits = set(bot_commits)
1192 args = [c for c in all_commits if c not in bot_commits]
1193
Doug Anderson44a644f2011-11-02 10:37:37 -07001194
1195 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1196 # project from CWD
1197 if opts.dir is None:
1198 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1199 stderr=subprocess.PIPE).strip()
1200 if not git_dir:
1201 raise BadInvocation('The current directory is not part of a git project.')
1202 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1203 elif not os.path.isdir(opts.dir):
1204 raise BadInvocation('Invalid dir: %s' % opts.dir)
1205 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1206 raise BadInvocation('Not a git directory: %s' % opts.dir)
1207
1208 # Identify the project if it wasn't specified; this _requires_ the repo
1209 # tool to be installed and for the project to be part of a repo checkout.
1210 if not opts.project:
1211 opts.project = _identify_project(opts.dir)
1212 if not opts.project:
1213 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1214
1215 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001216 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001217
Doug Anderson14749562013-06-26 13:38:29 -07001218 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
1219 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -07001220 if found_error:
1221 return 1
1222 return 0
1223
1224
1225def _test():
1226 """Run any built-in tests."""
1227 import doctest
1228 doctest.testmod()
1229
1230
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001231if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001232 if sys.argv[1:2] == ["--test"]:
1233 _test()
1234 exit_code = 0
1235 else:
1236 prog_name = os.path.basename(sys.argv[0])
1237 try:
1238 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001239 except BadInvocation, err:
1240 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001241 exit_code = 1
1242 sys.exit(exit_code)