blob: d8921554930bc35f4ac730716bd9e9f3f78926f8 [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",
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
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500260def _get_affected_files(commit, include_deletes=False, relative=False):
Doug Anderson42b8a052013-06-26 10:45:36 -0700261 """Returns list of absolute filepaths that were modified/added.
262
263 Args:
264 commit: The commit
265 include_deletes: If true we'll include delete in the list.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500266 relative: Whether to return full paths to files.
Doug Anderson42b8a052013-06-26 10:45:36 -0700267
268 Returns:
269 A list of modified/added (and perhaps deleted) files
270 """
Ryan Cui72834d12011-05-05 14:51:33 -0700271 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700272 files = []
273 for statusline in output.splitlines():
274 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
275 # Ignore deleted files, and return absolute paths of files
Mike Frysingerae409522014-02-01 03:16:11 -0500276 if include_deletes or m.group(1)[0] != 'D':
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500277 f = m.group(2)
278 if not relative:
279 pwd = os.getcwd()
280 f = os.path.join(pwd, f)
281 files.append(f)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700282 return files
283
Ryan Cui1562fb82011-05-09 11:01:31 -0700284
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700285def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700286 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700287 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700288 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700289
Ryan Cui1562fb82011-05-09 11:01:31 -0700290
Ryan Cuiec4d6332011-05-02 14:15:25 -0700291def _get_commit_desc(commit):
292 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400293 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700294
295
296# Common Hooks
297
Ryan Cui1562fb82011-05-09 11:01:31 -0700298
Mike Frysingerae409522014-02-01 03:16:11 -0500299def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700300 """Checks that there aren't any lines longer than maxlen characters in any of
301 the text files to be submitted.
302 """
303 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800304 SKIP_REGEXP = re.compile('|'.join([
305 r'https?://',
306 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700307
308 errors = []
309 files = _filter_files(_get_affected_files(commit),
310 COMMON_INCLUDED_PATHS,
311 COMMON_EXCLUDED_PATHS)
312
313 for afile in files:
314 for line_num, line in _get_file_diff(afile, commit):
315 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500316 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800317 continue
318
319 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
320 if len(errors) == 5: # Just show the first 5 errors.
321 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700322
323 if errors:
324 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700325 return HookFailure(msg, errors)
326
Ryan Cuiec4d6332011-05-02 14:15:25 -0700327
Mike Frysingerae409522014-02-01 03:16:11 -0500328def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700329 """Checks that there is no stray whitespace at source lines end."""
330 errors = []
331 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500332 COMMON_INCLUDED_PATHS,
333 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700334
335 for afile in files:
336 for line_num, line in _get_file_diff(afile, commit):
337 if line.rstrip() != line:
338 errors.append('%s, line %s' % (afile, line_num))
339 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700340 return HookFailure('Found line ending with white space in:', errors)
341
Ryan Cuiec4d6332011-05-02 14:15:25 -0700342
Mike Frysingerae409522014-02-01 03:16:11 -0500343def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700344 """Checks there are no unexpanded tabs."""
345 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700346 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700347 r".*\.ebuild$",
348 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500349 r".*/[M|m]akefile$",
350 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700351 ]
352
353 errors = []
354 files = _filter_files(_get_affected_files(commit),
355 COMMON_INCLUDED_PATHS,
356 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
357
358 for afile in files:
359 for line_num, line in _get_file_diff(afile, commit):
360 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500361 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700362 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700363 return HookFailure('Found a tab character in:', errors)
364
Ryan Cuiec4d6332011-05-02 14:15:25 -0700365
Mike Frysingerae409522014-02-01 03:16:11 -0500366def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700367 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700368 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700369
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700370 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700371 msg = 'Changelist description needs TEST field (after first line)'
372 return HookFailure(msg)
373
Ryan Cuiec4d6332011-05-02 14:15:25 -0700374
Mike Frysingerae409522014-02-01 03:16:11 -0500375def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700376 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
377 msg = 'Changelist has invalid CQ-DEPEND target.'
378 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
379 try:
380 patch.GetPaladinDeps(_get_commit_desc(commit))
381 except ValueError as ex:
382 return HookFailure(msg, [example, str(ex)])
383
384
Mike Frysingerae409522014-02-01 03:16:11 -0500385def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700386 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700387 OLD_BUG_RE = r'\nBUG=.*chromium-os'
388 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
389 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
390 'the chromium tracker in your BUG= line now.')
391 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700392
David James5c0073d2013-04-03 08:48:52 -0700393 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700394 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700395 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700396 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700397 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
398 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700399 return HookFailure(msg)
400
Ryan Cuiec4d6332011-05-02 14:15:25 -0700401
Doug Anderson42b8a052013-06-26 10:45:36 -0700402def _check_for_uprev(project, commit):
403 """Check that we're not missing a revbump of an ebuild in the given commit.
404
405 If the given commit touches files in a directory that has ebuilds somewhere
406 up the directory hierarchy, it's very likely that we need an ebuild revbump
407 in order for those changes to take effect.
408
409 It's not totally trivial to detect a revbump, so at least detect that an
410 ebuild with a revision number in it was touched. This should handle the
411 common case where we use a symlink to do the revbump.
412
413 TODO: it would be nice to enhance this hook to:
414 * Handle cases where people revbump with a slightly different syntax. I see
415 one ebuild (puppy) that revbumps with _pN. This is a false positive.
416 * Catches cases where people aren't using symlinks for revbumps. If they
417 edit a revisioned file directly (and are expected to rename it for revbump)
418 we'll miss that. Perhaps we could detect that the file touched is a
419 symlink?
420
421 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
422 still better off than without this check.
423
424 Args:
425 project: The project to look at
426 commit: The commit to look at
427
428 Returns:
429 A HookFailure or None.
430 """
Mike Frysinger011af942014-01-17 16:12:22 -0500431 # If this is the portage-stable overlay, then ignore the check. It's rare
432 # that we're doing anything other than importing files from upstream, so
433 # forcing a rev bump makes no sense.
434 whitelist = (
435 'chromiumos/overlays/portage-stable',
436 )
437 if project in whitelist:
438 return None
439
Doug Anderson42b8a052013-06-26 10:45:36 -0700440 affected_paths = _get_affected_files(commit, include_deletes=True)
441
442 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500443 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700444 affected_paths = [path for path in affected_paths
445 if os.path.basename(path) not in whitelist]
446 if not affected_paths:
447 return None
448
449 # If we've touched any file named with a -rN.ebuild then we'll say we're
450 # OK right away. See TODO above about enhancing this.
451 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
452 for path in affected_paths)
453 if touched_revved_ebuild:
454 return None
455
456 # We want to examine the current contents of all directories that are parents
457 # of files that were touched (up to the top of the project).
458 #
459 # ...note: we use the current directory contents even though it may have
460 # changed since the commit we're looking at. This is just a heuristic after
461 # all. Worst case we don't flag a missing revbump.
462 project_top = os.getcwd()
463 dirs_to_check = set([project_top])
464 for path in affected_paths:
465 path = os.path.dirname(path)
466 while os.path.exists(path) and not os.path.samefile(path, project_top):
467 dirs_to_check.add(path)
468 path = os.path.dirname(path)
469
470 # Look through each directory. If it's got an ebuild in it then we'll
471 # consider this as a case when we need a revbump.
472 for dir_path in dirs_to_check:
473 contents = os.listdir(dir_path)
474 ebuilds = [os.path.join(dir_path, path)
475 for path in contents if path.endswith('.ebuild')]
476 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
477
478 # If the -9999.ebuild file was touched the bot will uprev for us.
479 # ...we'll use a simple intersection here as a heuristic...
480 if set(ebuilds_9999) & set(affected_paths):
481 continue
482
483 if ebuilds:
484 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
485 'or a -r1.ebuild symlink if this is a new ebuild')
486
487 return None
488
489
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500490def _check_ebuild_eapi(project, commit):
491 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
492
493 We want to get away from older EAPI's as it makes life confusing and they
494 have less builtin error checking.
495
496 Args:
497 project: The project to look at
498 commit: The commit to look at
499
500 Returns:
501 A HookFailure or None.
502 """
503 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500504 # that we're doing anything other than importing files from upstream, and
505 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500506 whitelist = (
507 'chromiumos/overlays/portage-stable',
508 )
509 if project in whitelist:
510 return None
511
512 BAD_EAPIS = ('0', '1', '2', '3')
513
514 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
515
516 ebuilds_re = [r'\.ebuild$']
517 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
518 ebuilds_re)
519 bad_ebuilds = []
520
521 for ebuild in ebuilds:
522 # If the ebuild does not specify an EAPI, it defaults to 0.
523 eapi = '0'
524
525 lines = _get_file_content(ebuild, commit).splitlines()
526 if len(lines) == 1:
527 # This is most likely a symlink, so skip it entirely.
528 continue
529
530 for line in lines:
531 m = get_eapi.match(line)
532 if m:
533 # Once we hit the first EAPI line in this ebuild, stop processing.
534 # The spec requires that there only be one and it be first, so
535 # checking all possible values is pointless. We also assume that
536 # it's "the" EAPI line and not something in the middle of a heredoc.
537 eapi = m.group(1)
538 break
539
540 if eapi in BAD_EAPIS:
541 bad_ebuilds.append((ebuild, eapi))
542
543 if bad_ebuilds:
544 # pylint: disable=C0301
545 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
546 # pylint: enable=C0301
547 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500548 'These ebuilds are using old EAPIs. If these are imported from\n'
549 'Gentoo, then you may ignore and upload once with the --no-verify\n'
550 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500551 '\t%s\n'
552 'See this guide for more details:\n%s\n' %
553 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
554
555
Mike Frysinger89bdb852014-02-01 05:26:26 -0500556def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500557 """Make sure we use the new style KEYWORDS when possible in ebuilds.
558
559 If an ebuild generally does not care about the arch it is running on, then
560 ebuilds should flag it with one of:
561 KEYWORDS="*" # A stable ebuild.
562 KEYWORDS="~*" # An unstable ebuild.
563 KEYWORDS="-* ..." # Is known to only work on specific arches.
564
565 Args:
566 project: The project to look at
567 commit: The commit to look at
568
569 Returns:
570 A HookFailure or None.
571 """
572 WHITELIST = set(('*', '-*', '~*'))
573
574 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
575
Mike Frysinger89bdb852014-02-01 05:26:26 -0500576 ebuilds_re = [r'\.ebuild$']
577 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
578 ebuilds_re)
579
Mike Frysingerc51ece72014-01-17 16:23:40 -0500580 for ebuild in ebuilds:
581 for _, line in _get_file_diff(ebuild, commit):
582 m = get_keywords.match(line)
583 if m:
584 keywords = set(m.group(1).split())
585 if not keywords or WHITELIST - keywords != WHITELIST:
586 continue
587
588 return HookFailure(
589 'Please update KEYWORDS to use a glob:\n'
590 'If the ebuild should be marked stable (normal for non-9999 '
591 'ebuilds):\n'
592 ' KEYWORDS="*"\n'
593 'If the ebuild should be marked unstable (normal for '
594 'cros-workon / 9999 ebuilds):\n'
595 ' KEYWORDS="~*"\n'
596 'If the ebuild needs to be marked for only specific arches,'
597 'then use -* like so:\n'
598 ' KEYWORDS="-* arm ..."\n')
599
600
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800601def _check_ebuild_licenses(_project, commit):
602 """Check if the LICENSE field in the ebuild is correct."""
603 affected_paths = _get_affected_files(commit)
604 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
605
606 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800607 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800608
609 for ebuild in touched_ebuilds:
610 # Skip virutal packages.
611 if ebuild.split('/')[-3] == 'virtual':
612 continue
613
614 try:
615 license_types = licenses.GetLicenseTypesFromEbuild(ebuild)
616 except ValueError as e:
617 return HookFailure(e.message, [ebuild])
618
619 # Also ignore licenses ending with '?'
620 for license_type in [x for x in license_types
621 if x not in LICENSES_IGNORE and not x.endswith('?')]:
622 try:
623 licenses.Licensing.FindLicenseType(license_type)
624 except AssertionError as e:
625 return HookFailure(e.message, [ebuild])
626
627
Mike Frysingercd363c82014-02-01 05:20:18 -0500628def _check_ebuild_virtual_pv(project, commit):
629 """Enforce the virtual PV policies."""
630 # If this is the portage-stable overlay, then ignore the check.
631 # We want to import virtuals as-is from upstream Gentoo.
632 whitelist = (
633 'chromiumos/overlays/portage-stable',
634 )
635 if project in whitelist:
636 return None
637
638 # We assume the repo name is the same as the dir name on disk.
639 # It would be dumb to not have them match though.
640 project = os.path.basename(project)
641
642 is_variant = lambda x: x.startswith('overlay-variant-')
643 is_board = lambda x: x.startswith('overlay-')
644 is_private = lambda x: x.endswith('-private')
645
646 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
647
648 ebuilds_re = [r'\.ebuild$']
649 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
650 ebuilds_re)
651 bad_ebuilds = []
652
653 for ebuild in ebuilds:
654 m = get_pv.match(ebuild)
655 if m:
656 overlay = m.group(1)
657 if not overlay or not is_board(overlay):
658 overlay = project
659
660 pv = m.group(3).split('-', 1)[0]
661
662 if is_private(overlay):
663 want_pv = '3.5' if is_variant(overlay) else '3'
664 elif is_board(overlay):
665 want_pv = '2.5' if is_variant(overlay) else '2'
666 else:
667 want_pv = '1'
668
669 if pv != want_pv:
670 bad_ebuilds.append((ebuild, pv, want_pv))
671
672 if bad_ebuilds:
673 # pylint: disable=C0301
674 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
675 # pylint: enable=C0301
676 return HookFailure(
677 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
678 '\t%s\n'
679 'If this is an upstream Gentoo virtual, then you may ignore this\n'
680 'check (and re-run w/--no-verify). Otherwise, please see this\n'
681 'page for more details:\n%s\n' %
682 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
683 for x in bad_ebuilds]), url))
684
685
Mike Frysingerae409522014-02-01 03:16:11 -0500686def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700687 """Verify that Change-ID is present in last paragraph of commit message."""
688 desc = _get_commit_desc(commit)
689 loc = desc.rfind('\nChange-Id:')
690 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700691 return HookFailure('Change-Id must be in last paragraph of description.')
692
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700693
Mike Frysingerae409522014-02-01 03:16:11 -0500694def _check_license(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700695 """Verifies the license header."""
696 LICENSE_HEADER = (
Chris Sosaed7a3fa2014-02-26 12:18:31 -0800697 r".* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. "
698 "All rights reserved\." "\n"
699 r".* Use of this source code is governed by a BSD-style license that can "
700 "be\n"
701 r".* found in the LICENSE file\."
702 "\n"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700703 )
David Hendricks35030d02013-02-04 17:49:16 -0800704 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700705
David Hendricks35030d02013-02-04 17:49:16 -0800706 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700707
708
Mike Frysingerae409522014-02-01 03:16:11 -0500709def _check_google_copyright(_project, commit):
David Hendricksa0e310d2013-02-04 17:51:55 -0800710 """Verifies Google Inc. as copyright holder."""
711 LICENSE_HEADER = (
712 r".* Copyright 20[-0-9]{2,7} Google Inc\."
713 )
714 FAIL_MSG = "Copyright must match"
715
David Hendricks4c018e72013-02-06 13:46:38 -0800716 # Avoid blocking partners and external contributors.
717 fqdn = socket.getfqdn()
718 if not fqdn.endswith(".corp.google.com"):
719 return None
720
David Hendricksa0e310d2013-02-04 17:51:55 -0800721 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
722
723
Ryan Cuiec4d6332011-05-02 14:15:25 -0700724# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700725
Ryan Cui1562fb82011-05-09 11:01:31 -0700726
Mike Frysingerae409522014-02-01 03:16:11 -0500727def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700728 """Runs checkpatch.pl on the given project"""
729 hooks_dir = _get_hooks_dir()
Mike Frysingerae409522014-02-01 03:16:11 -0500730 cmd = ['%s/checkpatch.pl' % hooks_dir] + list(options) + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700731 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700732 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700733 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700734 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700735
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700736
Anton Staaf815d6852011-08-22 10:08:45 -0700737def _run_checkpatch_no_tree(project, commit):
738 return _run_checkpatch(project, commit, ['--no-tree'])
739
Mike Frysingerae409522014-02-01 03:16:11 -0500740
Randall Spangler7318fd62013-11-21 12:16:58 -0800741def _run_checkpatch_ec(project, commit):
742 """Runs checkpatch with options for Chromium EC projects."""
743 return _run_checkpatch(project, commit, ['--no-tree',
744 '--ignore=MSLEEP,VOLATILE'])
745
Mike Frysingerae409522014-02-01 03:16:11 -0500746
747def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700748 """Makes sure kernel config changes are not mixed with code changes"""
749 files = _get_affected_files(commit)
750 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
751 return HookFailure('Changes to chromeos/config/ and regular files must '
752 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700753
Mike Frysingerae409522014-02-01 03:16:11 -0500754
755def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700756 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700757 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700758 try:
759 json.load(open(f))
760 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700761 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700762
763
Mike Frysingerae409522014-02-01 03:16:11 -0500764def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400765 """Make sure Manifest files only have DIST lines"""
766 paths = []
767
768 for path in _get_affected_files(commit):
769 if os.path.basename(path) != 'Manifest':
770 continue
771 if not os.path.exists(path):
772 continue
773
774 with open(path, 'r') as f:
775 for line in f.readlines():
776 if not line.startswith('DIST '):
777 paths.append(path)
778 break
779
780 if paths:
781 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
782 ('\n'.join(paths),))
783
784
Mike Frysingerae409522014-02-01 03:16:11 -0500785def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700786 """Check for a non-empty 'BRANCH=' field in the commit message."""
787 BRANCH_RE = r'\nBRANCH=\S+'
788
789 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
790 msg = ('Changelist description needs BRANCH field (after first line)\n'
791 'E.g. BRANCH=none or BRANCH=link,snow')
792 return HookFailure(msg)
793
794
Mike Frysingerae409522014-02-01 03:16:11 -0500795def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800796 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
797 SIGNOFF_RE = r'\nSigned-off-by: \S+'
798
799 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
800 msg = ('Changelist description needs Signed-off-by: field\n'
801 'E.g. Signed-off-by: My Name <me@chromium.org>')
802 return HookFailure(msg)
803
804
Jon Salz3ee59de2012-08-18 13:54:22 +0800805def _run_project_hook_script(script, project, commit):
806 """Runs a project hook script.
807
808 The script is run with the following environment variables set:
809 PRESUBMIT_PROJECT: The affected project
810 PRESUBMIT_COMMIT: The affected commit
811 PRESUBMIT_FILES: A newline-separated list of affected files
812
813 The script is considered to fail if the exit code is non-zero. It should
814 write an error message to stdout.
815 """
816 env = dict(os.environ)
817 env['PRESUBMIT_PROJECT'] = project
818 env['PRESUBMIT_COMMIT'] = commit
819
820 # Put affected files in an environment variable
821 files = _get_affected_files(commit)
822 env['PRESUBMIT_FILES'] = '\n'.join(files)
823
824 process = subprocess.Popen(script, env=env, shell=True,
825 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800826 stdout=subprocess.PIPE,
827 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800828 stdout, _ = process.communicate()
829 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800830 if stdout:
831 stdout = re.sub('(?m)^', ' ', stdout)
832 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800833 (script, process.returncode,
834 ':\n' + stdout if stdout else ''))
835
836
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700837# Base
838
Ryan Cui1562fb82011-05-09 11:01:31 -0700839
Ryan Cui9b651632011-05-11 11:38:58 -0700840# A list of hooks that are not project-specific
841_COMMON_HOOKS = [
842 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700843 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700844 _check_change_has_test_field,
845 _check_change_has_proper_changeid,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500846 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500847 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800848 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500849 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700850 _check_no_stray_whitespace,
851 _check_no_long_lines,
852 _check_license,
853 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700854 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700855]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700856
Ryan Cui1562fb82011-05-09 11:01:31 -0700857
Ryan Cui9b651632011-05-11 11:38:58 -0700858# A dictionary of project-specific hooks(callbacks), indexed by project name.
859# dict[project] = [callback1, callback2]
860_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400861 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400862 "chromeos/overlays/chromeos-overlay": [_check_manifests],
863 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800864 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700865 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700866 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400867 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
868 _kernel_configcheck],
869 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400870 "chromiumos/overlays/board-overlays": [_check_manifests],
871 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
872 "chromiumos/overlays/portage-stable": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800873 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400874 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700875 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400876 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Shawn Nematbakhshb6ac17a2014-01-28 16:47:13 -0800877 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
878 _check_change_has_signoff_field,
879 _check_google_copyright],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700880 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400881 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
882 "chromiumos/third_party/kernel-next": [_run_checkpatch,
883 _kernel_configcheck],
884 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
885 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700886}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700887
Ryan Cui1562fb82011-05-09 11:01:31 -0700888
Ryan Cui9b651632011-05-11 11:38:58 -0700889# A dictionary of flags (keys) that can appear in the config file, and the hook
890# that the flag disables (value)
891_DISABLE_FLAGS = {
892 'stray_whitespace_check': _check_no_stray_whitespace,
893 'long_line_check': _check_no_long_lines,
894 'cros_license_check': _check_license,
895 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700896 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800897 'signoff_check': _check_change_has_signoff_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700898}
899
900
Jon Salz3ee59de2012-08-18 13:54:22 +0800901def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700902 """Returns a set of hooks disabled by the current project's config file.
903
904 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800905
906 Args:
907 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700908 """
909 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800910 if not config.has_section(SECTION):
911 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700912
913 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800914 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700915 try:
Mike Frysingerae409522014-02-01 03:16:11 -0500916 if not config.getboolean(SECTION, flag):
917 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -0700918 except ValueError as e:
919 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400920 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700921
922 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
923 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
924
925
Jon Salz3ee59de2012-08-18 13:54:22 +0800926def _get_project_hook_scripts(config):
927 """Returns a list of project-specific hook scripts.
928
929 Args:
930 config: A ConfigParser for the project's config file.
931 """
932 SECTION = 'Hook Scripts'
933 if not config.has_section(SECTION):
934 return []
935
936 hook_names_values = config.items(SECTION)
937 hook_names_values.sort(key=lambda x: x[0])
938 return [x[1] for x in hook_names_values]
939
940
Ryan Cui9b651632011-05-11 11:38:58 -0700941def _get_project_hooks(project):
942 """Returns a list of hooks that need to be run for a project.
943
944 Expects to be called from within the project root.
945 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800946 config = ConfigParser.RawConfigParser()
947 try:
948 config.read(_CONFIG_FILE)
949 except ConfigParser.Error:
950 # Just use an empty config file
951 config = ConfigParser.RawConfigParser()
952
953 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700954 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
955
956 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700957 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
958 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700959
Jon Salz3ee59de2012-08-18 13:54:22 +0800960 for script in _get_project_hook_scripts(config):
961 hooks.append(functools.partial(_run_project_hook_script, script))
962
Ryan Cui9b651632011-05-11 11:38:58 -0700963 return hooks
964
965
Doug Anderson14749562013-06-26 13:38:29 -0700966def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700967 """For each project run its project specific hook from the hooks dictionary.
968
969 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700970 project: The name of project to run hooks for.
971 proj_dir: If non-None, this is the directory the project is in. If None,
972 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700973 commit_list: A list of commits to run hooks against. If None or empty list
974 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700975
976 Returns:
977 Boolean value of whether any errors were ecountered while running the hooks.
978 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700979 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700980 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
981 if len(proj_dirs) == 0:
982 print('%s cannot be found.' % project, file=sys.stderr)
983 print('Please specify a valid project.', file=sys.stderr)
984 return True
985 if len(proj_dirs) > 1:
986 print('%s is associated with multiple directories.' % project,
987 file=sys.stderr)
988 print('Please specify a directory to help disambiguate.', file=sys.stderr)
989 return True
990 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700991
Ryan Cuiec4d6332011-05-02 14:15:25 -0700992 pwd = os.getcwd()
993 # hooks assume they are run from the root of the project
994 os.chdir(proj_dir)
995
Doug Anderson14749562013-06-26 13:38:29 -0700996 if not commit_list:
997 try:
998 commit_list = _get_commits()
999 except VerifyException as e:
1000 PrintErrorForProject(project, HookFailure(str(e)))
1001 os.chdir(pwd)
1002 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001003
Ryan Cui9b651632011-05-11 11:38:58 -07001004 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -07001005 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001006 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001007 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001008 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001009 hook_error = hook(project, commit)
1010 if hook_error:
1011 error_list.append(hook_error)
1012 error_found = True
1013 if error_list:
1014 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1015 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001016
Ryan Cuiec4d6332011-05-02 14:15:25 -07001017 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001018 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001019
Mike Frysingerae409522014-02-01 03:16:11 -05001020
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001021# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001022
Ryan Cui1562fb82011-05-09 11:01:31 -07001023
Mike Frysingerae409522014-02-01 03:16:11 -05001024def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001025 """Main function invoked directly by repo.
1026
1027 This function will exit directly upon error so that repo doesn't print some
1028 obscure error message.
1029
1030 Args:
1031 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001032 worktree_list: A list of directories. It should be the same length as
1033 project_list, so that each entry in project_list matches with a directory
1034 in worktree_list. If None, we will attempt to calculate the directories
1035 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001036 kwargs: Leave this here for forward-compatibility.
1037 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001038 found_error = False
David James2edd9002013-10-11 14:09:19 -07001039 if not worktree_list:
1040 worktree_list = [None] * len(project_list)
1041 for project, worktree in zip(project_list, worktree_list):
1042 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001043 found_error = True
1044
Mike Frysingerae409522014-02-01 03:16:11 -05001045 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001046 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001047 '- To disable some source style checks, and for other hints, see '
1048 '<checkout_dir>/src/repohooks/README\n'
1049 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001050 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001051 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001052
Ryan Cui1562fb82011-05-09 11:01:31 -07001053
Doug Anderson44a644f2011-11-02 10:37:37 -07001054def _identify_project(path):
1055 """Identify the repo project associated with the given path.
1056
1057 Returns:
1058 A string indicating what project is associated with the path passed in or
1059 a blank string upon failure.
1060 """
1061 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1062 stderr=subprocess.PIPE, cwd=path).strip()
1063
1064
1065def direct_main(args, verbose=False):
1066 """Run hooks directly (outside of the context of repo).
1067
1068 # Setup for doctests below.
1069 # ...note that some tests assume that running pre-upload on this CWD is fine.
1070 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1071 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1072 >>> olddir = os.getcwd()
1073
1074 # OK to run w/ no arugments; will run with CWD.
1075 >>> os.chdir(mydir)
1076 >>> direct_main(['prog_name'], verbose=True)
1077 Running hooks on chromiumos/repohooks
1078 0
1079 >>> os.chdir(olddir)
1080
1081 # Run specifying a dir
1082 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1083 Running hooks on chromiumos/repohooks
1084 0
1085
1086 # Not a problem to use a bogus project; we'll just get default settings.
1087 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1088 Running hooks on X
1089 0
1090
1091 # Run with project but no dir
1092 >>> os.chdir(mydir)
1093 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1094 Running hooks on X
1095 0
1096 >>> os.chdir(olddir)
1097
1098 # Try with a non-git CWD
1099 >>> os.chdir('/tmp')
1100 >>> direct_main(['prog_name'])
1101 Traceback (most recent call last):
1102 ...
1103 BadInvocation: The current directory is not part of a git project.
1104
1105 # Check various bad arguments...
1106 >>> direct_main(['prog_name', 'bogus'])
1107 Traceback (most recent call last):
1108 ...
1109 BadInvocation: Unexpected arguments: bogus
1110 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1111 Traceback (most recent call last):
1112 ...
1113 BadInvocation: Invalid dir: bogusdir
1114 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1115 Traceback (most recent call last):
1116 ...
1117 BadInvocation: Not a git directory: /tmp
1118
1119 Args:
1120 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001121 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001122
1123 Returns:
1124 0 if no pre-upload failures, 1 if failures.
1125
1126 Raises:
1127 BadInvocation: On some types of invocation errors.
1128 """
1129 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1130 parser = optparse.OptionParser(description=desc)
1131
1132 parser.add_option('--dir', default=None,
1133 help='The directory that the project lives in. If not '
1134 'specified, use the git project root based on the cwd.')
1135 parser.add_option('--project', default=None,
1136 help='The project repo path; this can affect how the hooks '
1137 'get run, since some hooks are project-specific. For '
1138 'chromite this is chromiumos/chromite. If not specified, '
1139 'the repo tool will be used to figure this out based on '
1140 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001141 parser.add_option('--rerun-since', default=None,
1142 help='Rerun hooks on old commits since the given date. '
1143 'The date should match git log\'s concept of a date. '
1144 'e.g. 2012-06-20')
1145
1146 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001147
1148 opts, args = parser.parse_args(args[1:])
1149
Doug Anderson14749562013-06-26 13:38:29 -07001150 if opts.rerun_since:
1151 if args:
1152 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1153 ' '.join(args))
1154
1155 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1156 all_commits = _run_command(cmd).splitlines()
1157 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1158
1159 # Eliminate chrome-bot commits but keep ordering the same...
1160 bot_commits = set(bot_commits)
1161 args = [c for c in all_commits if c not in bot_commits]
1162
Doug Anderson44a644f2011-11-02 10:37:37 -07001163
1164 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1165 # project from CWD
1166 if opts.dir is None:
1167 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1168 stderr=subprocess.PIPE).strip()
1169 if not git_dir:
1170 raise BadInvocation('The current directory is not part of a git project.')
1171 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1172 elif not os.path.isdir(opts.dir):
1173 raise BadInvocation('Invalid dir: %s' % opts.dir)
1174 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1175 raise BadInvocation('Not a git directory: %s' % opts.dir)
1176
1177 # Identify the project if it wasn't specified; this _requires_ the repo
1178 # tool to be installed and for the project to be part of a repo checkout.
1179 if not opts.project:
1180 opts.project = _identify_project(opts.dir)
1181 if not opts.project:
1182 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1183
1184 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001185 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001186
Doug Anderson14749562013-06-26 13:38:29 -07001187 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
1188 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -07001189 if found_error:
1190 return 1
1191 return 0
1192
1193
1194def _test():
1195 """Run any built-in tests."""
1196 import doctest
1197 doctest.testmod()
1198
1199
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001200if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001201 if sys.argv[1:2] == ["--test"]:
1202 _test()
1203 exit_code = 0
1204 else:
1205 prog_name = os.path.basename(sys.argv[0])
1206 try:
1207 exit_code = direct_main(sys.argv)
1208 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001209 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001210 exit_code = 1
1211 sys.exit(exit_code)