blob: 9e2b8964687500fa13e8f25191cd299f8a8e0707 [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
21
Ryan Cui1562fb82011-05-09 11:01:31 -070022from errors import (VerifyException, HookFailure, PrintErrorForProject,
23 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070024
David Jamesc3b68b32013-04-03 09:17:03 -070025# If repo imports us, the __name__ will be __builtin__, and the wrapper will
26# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
27# up. The same logic also happens to work if we're executed directly.
28if __name__ in ('__builtin__', '__main__'):
29 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
30
31from chromite.lib import patch
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -080032from chromite.licensing import licenses
David Jamesc3b68b32013-04-03 09:17:03 -070033
Ryan Cuiec4d6332011-05-02 14:15:25 -070034
35COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050036 # C++ and friends
37 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
38 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
39 # Scripts
40 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
41 # No extension at all, note that ALL CAPS files are black listed in
42 # COMMON_EXCLUDED_LIST below.
43 r"(^|.*[\\\/])[^.]+$",
44 # Other
45 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070046]
47
Ryan Cui1562fb82011-05-09 11:01:31 -070048
Ryan Cuiec4d6332011-05-02 14:15:25 -070049COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050050 # avoid doing source file checks for kernel
51 r"/src/third_party/kernel/",
52 r"/src/third_party/kernel-next/",
53 r"/src/third_party/ktop/",
54 r"/src/third_party/punybench/",
55 r".*\bexperimental[\\\/].*",
56 r".*\b[A-Z0-9_]{2,}$",
57 r".*[\\\/]debian[\\\/]rules$",
58 # for ebuild trees, ignore any caches and manifest data
59 r".*/Manifest$",
60 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070061
Mike Frysingerae409522014-02-01 03:16:11 -050062 # ignore profiles data (like overlay-tegra2/profiles)
63 r".*/overlay-.*/profiles/.*",
64 # ignore minified js and jquery
65 r".*\.min\.js",
66 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050067
68 # Ignore license files as the content is often taken verbatim.
69 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070070]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070071
Ryan Cui1562fb82011-05-09 11:01:31 -070072
Ryan Cui9b651632011-05-11 11:38:58 -070073_CONFIG_FILE = 'PRESUBMIT.cfg'
74
75
Doug Anderson44a644f2011-11-02 10:37:37 -070076# Exceptions
77
78
79class BadInvocation(Exception):
80 """An Exception indicating a bad invocation of the program."""
81 pass
82
83
Ryan Cui1562fb82011-05-09 11:01:31 -070084# General Helpers
85
Sean Paulba01d402011-05-05 11:36:23 -040086
Doug Anderson44a644f2011-11-02 10:37:37 -070087def _run_command(cmd, cwd=None, stderr=None):
88 """Executes the passed in command and returns raw stdout output.
89
90 Args:
91 cmd: The command to run; should be a list of strings.
92 cwd: The directory to switch to for running the command.
93 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
94 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
95
96 Returns:
97 The standard out from the process.
98 """
99 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
100 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700101
Ryan Cui1562fb82011-05-09 11:01:31 -0700102
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700103def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700104 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700105 if __name__ == '__main__':
106 # Works when file is run on its own (__file__ is defined)...
107 return os.path.abspath(os.path.dirname(__file__))
108 else:
109 # We need to do this when we're run through repo. Since repo executes
110 # us with execfile(), we don't get __file__ defined.
111 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
112 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700113
Ryan Cui1562fb82011-05-09 11:01:31 -0700114
Ryan Cuiec4d6332011-05-02 14:15:25 -0700115def _match_regex_list(subject, expressions):
116 """Try to match a list of regular expressions to a string.
117
118 Args:
119 subject: The string to match regexes on
120 expressions: A list of regular expressions to check for matches with.
121
122 Returns:
123 Whether the passed in subject matches any of the passed in regexes.
124 """
125 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500126 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700127 return True
128 return False
129
Ryan Cui1562fb82011-05-09 11:01:31 -0700130
Mike Frysingerae409522014-02-01 03:16:11 -0500131def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700132 """Filter out files based on the conditions passed in.
133
134 Args:
135 files: list of filepaths to filter
136 include_list: list of regex that when matched with a file path will cause it
137 to be added to the output list unless the file is also matched with a
138 regex in the exclude_list.
139 exclude_list: list of regex that when matched with a file will prevent it
140 from being added to the output list, even if it is also matched with a
141 regex in the include_list.
142
143 Returns:
144 A list of filepaths that contain files matched in the include_list and not
145 in the exclude_list.
146 """
147 filtered = []
148 for f in files:
149 if (_match_regex_list(f, include_list) and
150 not _match_regex_list(f, exclude_list)):
151 filtered.append(f)
152 return filtered
153
Ryan Cuiec4d6332011-05-02 14:15:25 -0700154
David Hendricks35030d02013-02-04 17:49:16 -0800155def _verify_header_content(commit, content, fail_msg):
156 """Verify that file headers contain specified content.
157
158 Args:
159 commit: the affected commit.
160 content: the content of the header to be verified.
161 fail_msg: the first message to display in case of failure.
162
Mike Frysinger33a458d2014-03-03 17:00:51 -0500163 Returns:
164 The return value of HookFailure().
David Hendricks35030d02013-02-04 17:49:16 -0800165 """
166 license_re = re.compile(content, re.MULTILINE)
167 bad_files = []
168 files = _filter_files(_get_affected_files(commit),
169 COMMON_INCLUDED_PATHS,
170 COMMON_EXCLUDED_PATHS)
171
172 for f in files:
Tom Wai-Hong Tam667db5d2014-02-27 06:28:14 +0800173 # Ignore non-existant files and symlinks
174 if os.path.exists(f) and not os.path.islink(f):
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800175 contents = open(f).read()
Mike Frysingerae409522014-02-01 03:16:11 -0500176 if not contents:
177 # Ignore empty files
178 continue
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800179 if not license_re.search(contents):
180 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800181 if bad_files:
Mike Frysingerae409522014-02-01 03:16:11 -0500182 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
183 "Found a bad header in these files:")
184 return HookFailure(msg, bad_files)
David Hendricks35030d02013-02-04 17:49:16 -0800185
186
Ryan Cuiec4d6332011-05-02 14:15:25 -0700187# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700188
189
Ryan Cui4725d952011-05-05 15:41:19 -0700190def _get_upstream_branch():
191 """Returns the upstream tracking branch of the current branch.
192
193 Raises:
194 Error if there is no tracking branch
195 """
196 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
197 current_branch = current_branch.replace('refs/heads/', '')
198 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700199 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700200
201 cfg_option = 'branch.' + current_branch + '.%s'
202 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
203 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
204 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700205 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700206
207 return full_upstream.replace('heads', 'remotes/' + remote)
208
Ryan Cui1562fb82011-05-09 11:01:31 -0700209
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700210def _get_patch(commit):
211 """Returns the patch for this commit."""
212 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700213
Ryan Cui1562fb82011-05-09 11:01:31 -0700214
Jon Salz98255932012-08-18 14:48:02 +0800215def _try_utf8_decode(data):
216 """Attempts to decode a string as UTF-8.
217
218 Returns:
219 The decoded Unicode object, or the original string if parsing fails.
220 """
221 try:
222 return unicode(data, 'utf-8', 'strict')
223 except UnicodeDecodeError:
224 return data
225
226
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500227def _get_file_content(path, commit):
228 """Returns the content of a file at a specific commit.
229
230 We can't rely on the file as it exists in the filesystem as people might be
231 uploading a series of changes which modifies the file multiple times.
232
233 Note: The "content" of a symlink is just the target. So if you're expecting
234 a full file, you should check that first. One way to detect is that the
235 content will not have any newlines.
236 """
237 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
238
239
Mike Frysingerae409522014-02-01 03:16:11 -0500240def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700241 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysinger5122a1f2014-02-01 02:47:35 -0500242 output = _run_command(['git', 'show', '-p', '--pretty=format:',
Mike Frysingerae409522014-02-01 03:16:11 -0500243 '--no-ext-diff', commit, path])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700244
245 new_lines = []
246 line_num = 0
247 for line in output.splitlines():
248 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
249 if m:
250 line_num = int(m.groups(1)[0])
251 continue
252 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800253 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700254 if not line.startswith('-'):
255 line_num += 1
256 return new_lines
257
Ryan Cui1562fb82011-05-09 11:01:31 -0700258
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500259def _get_affected_files(commit, include_deletes=False, relative=False):
Doug Anderson42b8a052013-06-26 10:45:36 -0700260 """Returns list of absolute filepaths that were modified/added.
261
262 Args:
263 commit: The commit
264 include_deletes: If true we'll include delete in the list.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500265 relative: Whether to return full paths to files.
Doug Anderson42b8a052013-06-26 10:45:36 -0700266
267 Returns:
268 A list of modified/added (and perhaps deleted) files
269 """
Ryan Cui72834d12011-05-05 14:51:33 -0700270 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700271 files = []
272 for statusline in output.splitlines():
273 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
274 # Ignore deleted files, and return absolute paths of files
Mike Frysingerae409522014-02-01 03:16:11 -0500275 if include_deletes or m.group(1)[0] != 'D':
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500276 f = m.group(2)
277 if not relative:
278 pwd = os.getcwd()
279 f = os.path.join(pwd, f)
280 files.append(f)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700281 return files
282
Ryan Cui1562fb82011-05-09 11:01:31 -0700283
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700284def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700285 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700286 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700287 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700288
Ryan Cui1562fb82011-05-09 11:01:31 -0700289
Ryan Cuiec4d6332011-05-02 14:15:25 -0700290def _get_commit_desc(commit):
291 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400292 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700293
294
295# Common Hooks
296
Ryan Cui1562fb82011-05-09 11:01:31 -0700297
Mike Frysingerae409522014-02-01 03:16:11 -0500298def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700299 """Checks that there aren't any lines longer than maxlen characters in any of
300 the text files to be submitted.
301 """
302 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800303 SKIP_REGEXP = re.compile('|'.join([
304 r'https?://',
305 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700306
307 errors = []
308 files = _filter_files(_get_affected_files(commit),
309 COMMON_INCLUDED_PATHS,
310 COMMON_EXCLUDED_PATHS)
311
312 for afile in files:
313 for line_num, line in _get_file_diff(afile, commit):
314 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500315 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800316 continue
317
318 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
319 if len(errors) == 5: # Just show the first 5 errors.
320 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700321
322 if errors:
323 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700324 return HookFailure(msg, errors)
325
Ryan Cuiec4d6332011-05-02 14:15:25 -0700326
Mike Frysingerae409522014-02-01 03:16:11 -0500327def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700328 """Checks that there is no stray whitespace at source lines end."""
329 errors = []
330 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500331 COMMON_INCLUDED_PATHS,
332 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700333
334 for afile in files:
335 for line_num, line in _get_file_diff(afile, commit):
336 if line.rstrip() != line:
337 errors.append('%s, line %s' % (afile, line_num))
338 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700339 return HookFailure('Found line ending with white space in:', errors)
340
Ryan Cuiec4d6332011-05-02 14:15:25 -0700341
Mike Frysingerae409522014-02-01 03:16:11 -0500342def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700343 """Checks there are no unexpanded tabs."""
344 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700345 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700346 r".*\.ebuild$",
347 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500348 r".*/[M|m]akefile$",
349 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700350 ]
351
352 errors = []
353 files = _filter_files(_get_affected_files(commit),
354 COMMON_INCLUDED_PATHS,
355 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
356
357 for afile in files:
358 for line_num, line in _get_file_diff(afile, commit):
359 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500360 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700361 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700362 return HookFailure('Found a tab character in:', errors)
363
Ryan Cuiec4d6332011-05-02 14:15:25 -0700364
Mike Frysingerae409522014-02-01 03:16:11 -0500365def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700366 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700367 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700368
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700369 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700370 msg = 'Changelist description needs TEST field (after first line)'
371 return HookFailure(msg)
372
Ryan Cuiec4d6332011-05-02 14:15:25 -0700373
Mike Frysingerae409522014-02-01 03:16:11 -0500374def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700375 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
376 msg = 'Changelist has invalid CQ-DEPEND target.'
377 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
378 try:
379 patch.GetPaladinDeps(_get_commit_desc(commit))
380 except ValueError as ex:
381 return HookFailure(msg, [example, str(ex)])
382
383
Mike Frysingerae409522014-02-01 03:16:11 -0500384def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700385 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700386 OLD_BUG_RE = r'\nBUG=.*chromium-os'
387 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
388 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
389 'the chromium tracker in your BUG= line now.')
390 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700391
David James5c0073d2013-04-03 08:48:52 -0700392 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700393 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700394 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700395 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700396 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
397 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700398 return HookFailure(msg)
399
Ryan Cuiec4d6332011-05-02 14:15:25 -0700400
Doug Anderson42b8a052013-06-26 10:45:36 -0700401def _check_for_uprev(project, commit):
402 """Check that we're not missing a revbump of an ebuild in the given commit.
403
404 If the given commit touches files in a directory that has ebuilds somewhere
405 up the directory hierarchy, it's very likely that we need an ebuild revbump
406 in order for those changes to take effect.
407
408 It's not totally trivial to detect a revbump, so at least detect that an
409 ebuild with a revision number in it was touched. This should handle the
410 common case where we use a symlink to do the revbump.
411
412 TODO: it would be nice to enhance this hook to:
413 * Handle cases where people revbump with a slightly different syntax. I see
414 one ebuild (puppy) that revbumps with _pN. This is a false positive.
415 * Catches cases where people aren't using symlinks for revbumps. If they
416 edit a revisioned file directly (and are expected to rename it for revbump)
417 we'll miss that. Perhaps we could detect that the file touched is a
418 symlink?
419
420 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
421 still better off than without this check.
422
423 Args:
424 project: The project to look at
425 commit: The commit to look at
426
427 Returns:
428 A HookFailure or None.
429 """
Mike Frysinger011af942014-01-17 16:12:22 -0500430 # If this is the portage-stable overlay, then ignore the check. It's rare
431 # that we're doing anything other than importing files from upstream, so
432 # forcing a rev bump makes no sense.
433 whitelist = (
434 'chromiumos/overlays/portage-stable',
435 )
436 if project in whitelist:
437 return None
438
Doug Anderson42b8a052013-06-26 10:45:36 -0700439 affected_paths = _get_affected_files(commit, include_deletes=True)
440
441 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500442 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700443 affected_paths = [path for path in affected_paths
444 if os.path.basename(path) not in whitelist]
445 if not affected_paths:
446 return None
447
448 # If we've touched any file named with a -rN.ebuild then we'll say we're
449 # OK right away. See TODO above about enhancing this.
450 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
451 for path in affected_paths)
452 if touched_revved_ebuild:
453 return None
454
455 # We want to examine the current contents of all directories that are parents
456 # of files that were touched (up to the top of the project).
457 #
458 # ...note: we use the current directory contents even though it may have
459 # changed since the commit we're looking at. This is just a heuristic after
460 # all. Worst case we don't flag a missing revbump.
461 project_top = os.getcwd()
462 dirs_to_check = set([project_top])
463 for path in affected_paths:
464 path = os.path.dirname(path)
465 while os.path.exists(path) and not os.path.samefile(path, project_top):
466 dirs_to_check.add(path)
467 path = os.path.dirname(path)
468
469 # Look through each directory. If it's got an ebuild in it then we'll
470 # consider this as a case when we need a revbump.
471 for dir_path in dirs_to_check:
472 contents = os.listdir(dir_path)
473 ebuilds = [os.path.join(dir_path, path)
474 for path in contents if path.endswith('.ebuild')]
475 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
476
477 # If the -9999.ebuild file was touched the bot will uprev for us.
478 # ...we'll use a simple intersection here as a heuristic...
479 if set(ebuilds_9999) & set(affected_paths):
480 continue
481
482 if ebuilds:
483 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
484 'or a -r1.ebuild symlink if this is a new ebuild')
485
486 return None
487
488
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500489def _check_ebuild_eapi(project, commit):
490 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
491
492 We want to get away from older EAPI's as it makes life confusing and they
493 have less builtin error checking.
494
495 Args:
496 project: The project to look at
497 commit: The commit to look at
498
499 Returns:
500 A HookFailure or None.
501 """
502 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500503 # that we're doing anything other than importing files from upstream, and
504 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500505 whitelist = (
506 'chromiumos/overlays/portage-stable',
507 )
508 if project in whitelist:
509 return None
510
511 BAD_EAPIS = ('0', '1', '2', '3')
512
513 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
514
515 ebuilds_re = [r'\.ebuild$']
516 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
517 ebuilds_re)
518 bad_ebuilds = []
519
520 for ebuild in ebuilds:
521 # If the ebuild does not specify an EAPI, it defaults to 0.
522 eapi = '0'
523
524 lines = _get_file_content(ebuild, commit).splitlines()
525 if len(lines) == 1:
526 # This is most likely a symlink, so skip it entirely.
527 continue
528
529 for line in lines:
530 m = get_eapi.match(line)
531 if m:
532 # Once we hit the first EAPI line in this ebuild, stop processing.
533 # The spec requires that there only be one and it be first, so
534 # checking all possible values is pointless. We also assume that
535 # it's "the" EAPI line and not something in the middle of a heredoc.
536 eapi = m.group(1)
537 break
538
539 if eapi in BAD_EAPIS:
540 bad_ebuilds.append((ebuild, eapi))
541
542 if bad_ebuilds:
543 # pylint: disable=C0301
544 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
545 # pylint: enable=C0301
546 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500547 'These ebuilds are using old EAPIs. If these are imported from\n'
548 'Gentoo, then you may ignore and upload once with the --no-verify\n'
549 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500550 '\t%s\n'
551 'See this guide for more details:\n%s\n' %
552 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
553
554
Mike Frysinger89bdb852014-02-01 05:26:26 -0500555def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500556 """Make sure we use the new style KEYWORDS when possible in ebuilds.
557
558 If an ebuild generally does not care about the arch it is running on, then
559 ebuilds should flag it with one of:
560 KEYWORDS="*" # A stable ebuild.
561 KEYWORDS="~*" # An unstable ebuild.
562 KEYWORDS="-* ..." # Is known to only work on specific arches.
563
564 Args:
565 project: The project to look at
566 commit: The commit to look at
567
568 Returns:
569 A HookFailure or None.
570 """
571 WHITELIST = set(('*', '-*', '~*'))
572
573 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
574
Mike Frysinger89bdb852014-02-01 05:26:26 -0500575 ebuilds_re = [r'\.ebuild$']
576 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
577 ebuilds_re)
578
Mike Frysingerc51ece72014-01-17 16:23:40 -0500579 for ebuild in ebuilds:
580 for _, line in _get_file_diff(ebuild, commit):
581 m = get_keywords.match(line)
582 if m:
583 keywords = set(m.group(1).split())
584 if not keywords or WHITELIST - keywords != WHITELIST:
585 continue
586
587 return HookFailure(
588 'Please update KEYWORDS to use a glob:\n'
589 'If the ebuild should be marked stable (normal for non-9999 '
590 'ebuilds):\n'
591 ' KEYWORDS="*"\n'
592 'If the ebuild should be marked unstable (normal for '
593 'cros-workon / 9999 ebuilds):\n'
594 ' KEYWORDS="~*"\n'
595 'If the ebuild needs to be marked for only specific arches,'
596 'then use -* like so:\n'
597 ' KEYWORDS="-* arm ..."\n')
598
599
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800600def _check_ebuild_licenses(_project, commit):
601 """Check if the LICENSE field in the ebuild is correct."""
602 affected_paths = _get_affected_files(commit)
603 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
604
605 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800606 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800607
608 for ebuild in touched_ebuilds:
609 # Skip virutal packages.
610 if ebuild.split('/')[-3] == 'virtual':
611 continue
612
613 try:
614 license_types = licenses.GetLicenseTypesFromEbuild(ebuild)
615 except ValueError as e:
616 return HookFailure(e.message, [ebuild])
617
618 # Also ignore licenses ending with '?'
619 for license_type in [x for x in license_types
620 if x not in LICENSES_IGNORE and not x.endswith('?')]:
621 try:
622 licenses.Licensing.FindLicenseType(license_type)
623 except AssertionError as e:
624 return HookFailure(e.message, [ebuild])
625
626
Mike Frysingercd363c82014-02-01 05:20:18 -0500627def _check_ebuild_virtual_pv(project, commit):
628 """Enforce the virtual PV policies."""
629 # If this is the portage-stable overlay, then ignore the check.
630 # We want to import virtuals as-is from upstream Gentoo.
631 whitelist = (
632 'chromiumos/overlays/portage-stable',
633 )
634 if project in whitelist:
635 return None
636
637 # We assume the repo name is the same as the dir name on disk.
638 # It would be dumb to not have them match though.
639 project = os.path.basename(project)
640
641 is_variant = lambda x: x.startswith('overlay-variant-')
642 is_board = lambda x: x.startswith('overlay-')
643 is_private = lambda x: x.endswith('-private')
644
645 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
646
647 ebuilds_re = [r'\.ebuild$']
648 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
649 ebuilds_re)
650 bad_ebuilds = []
651
652 for ebuild in ebuilds:
653 m = get_pv.match(ebuild)
654 if m:
655 overlay = m.group(1)
656 if not overlay or not is_board(overlay):
657 overlay = project
658
659 pv = m.group(3).split('-', 1)[0]
660
661 if is_private(overlay):
662 want_pv = '3.5' if is_variant(overlay) else '3'
663 elif is_board(overlay):
664 want_pv = '2.5' if is_variant(overlay) else '2'
665 else:
666 want_pv = '1'
667
668 if pv != want_pv:
669 bad_ebuilds.append((ebuild, pv, want_pv))
670
671 if bad_ebuilds:
672 # pylint: disable=C0301
673 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
674 # pylint: enable=C0301
675 return HookFailure(
676 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
677 '\t%s\n'
678 'If this is an upstream Gentoo virtual, then you may ignore this\n'
679 'check (and re-run w/--no-verify). Otherwise, please see this\n'
680 'page for more details:\n%s\n' %
681 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
682 for x in bad_ebuilds]), url))
683
684
Mike Frysingerae409522014-02-01 03:16:11 -0500685def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700686 """Verify that Change-ID is present in last paragraph of commit message."""
687 desc = _get_commit_desc(commit)
688 loc = desc.rfind('\nChange-Id:')
689 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700690 return HookFailure('Change-Id must be in last paragraph of description.')
691
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700692
Mike Frysingerae409522014-02-01 03:16:11 -0500693def _check_license(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700694 """Verifies the license header."""
695 LICENSE_HEADER = (
Chris Sosaed7a3fa2014-02-26 12:18:31 -0800696 r".* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. "
697 "All rights reserved\." "\n"
698 r".* Use of this source code is governed by a BSD-style license that can "
699 "be\n"
700 r".* found in the LICENSE file\."
701 "\n"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700702 )
David Hendricks35030d02013-02-04 17:49:16 -0800703 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700704
David Hendricks35030d02013-02-04 17:49:16 -0800705 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700706
707
708# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700709
Ryan Cui1562fb82011-05-09 11:01:31 -0700710
Mike Frysingerae409522014-02-01 03:16:11 -0500711def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700712 """Runs checkpatch.pl on the given project"""
713 hooks_dir = _get_hooks_dir()
Mike Frysingerae409522014-02-01 03:16:11 -0500714 cmd = ['%s/checkpatch.pl' % hooks_dir] + list(options) + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700715 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700716 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700717 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700718 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700719
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700720
Anton Staaf815d6852011-08-22 10:08:45 -0700721def _run_checkpatch_no_tree(project, commit):
722 return _run_checkpatch(project, commit, ['--no-tree'])
723
Mike Frysingerae409522014-02-01 03:16:11 -0500724
Randall Spangler7318fd62013-11-21 12:16:58 -0800725def _run_checkpatch_ec(project, commit):
726 """Runs checkpatch with options for Chromium EC projects."""
727 return _run_checkpatch(project, commit, ['--no-tree',
728 '--ignore=MSLEEP,VOLATILE'])
729
Mike Frysingerae409522014-02-01 03:16:11 -0500730
731def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700732 """Makes sure kernel config changes are not mixed with code changes"""
733 files = _get_affected_files(commit)
734 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
735 return HookFailure('Changes to chromeos/config/ and regular files must '
736 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700737
Mike Frysingerae409522014-02-01 03:16:11 -0500738
739def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700740 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700741 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700742 try:
743 json.load(open(f))
744 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700745 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700746
747
Mike Frysingerae409522014-02-01 03:16:11 -0500748def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400749 """Make sure Manifest files only have DIST lines"""
750 paths = []
751
752 for path in _get_affected_files(commit):
753 if os.path.basename(path) != 'Manifest':
754 continue
755 if not os.path.exists(path):
756 continue
757
758 with open(path, 'r') as f:
759 for line in f.readlines():
760 if not line.startswith('DIST '):
761 paths.append(path)
762 break
763
764 if paths:
765 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
766 ('\n'.join(paths),))
767
768
Mike Frysingerae409522014-02-01 03:16:11 -0500769def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700770 """Check for a non-empty 'BRANCH=' field in the commit message."""
771 BRANCH_RE = r'\nBRANCH=\S+'
772
773 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
774 msg = ('Changelist description needs BRANCH field (after first line)\n'
775 'E.g. BRANCH=none or BRANCH=link,snow')
776 return HookFailure(msg)
777
778
Mike Frysingerae409522014-02-01 03:16:11 -0500779def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800780 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
781 SIGNOFF_RE = r'\nSigned-off-by: \S+'
782
783 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
784 msg = ('Changelist description needs Signed-off-by: field\n'
785 'E.g. Signed-off-by: My Name <me@chromium.org>')
786 return HookFailure(msg)
787
788
Jon Salz3ee59de2012-08-18 13:54:22 +0800789def _run_project_hook_script(script, project, commit):
790 """Runs a project hook script.
791
792 The script is run with the following environment variables set:
793 PRESUBMIT_PROJECT: The affected project
794 PRESUBMIT_COMMIT: The affected commit
795 PRESUBMIT_FILES: A newline-separated list of affected files
796
797 The script is considered to fail if the exit code is non-zero. It should
798 write an error message to stdout.
799 """
800 env = dict(os.environ)
801 env['PRESUBMIT_PROJECT'] = project
802 env['PRESUBMIT_COMMIT'] = commit
803
804 # Put affected files in an environment variable
805 files = _get_affected_files(commit)
806 env['PRESUBMIT_FILES'] = '\n'.join(files)
807
808 process = subprocess.Popen(script, env=env, shell=True,
809 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800810 stdout=subprocess.PIPE,
811 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800812 stdout, _ = process.communicate()
813 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800814 if stdout:
815 stdout = re.sub('(?m)^', ' ', stdout)
816 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800817 (script, process.returncode,
818 ':\n' + stdout if stdout else ''))
819
820
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700821# Base
822
Ryan Cui1562fb82011-05-09 11:01:31 -0700823
Ryan Cui9b651632011-05-11 11:38:58 -0700824# A list of hooks that are not project-specific
825_COMMON_HOOKS = [
826 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700827 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700828 _check_change_has_test_field,
829 _check_change_has_proper_changeid,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500830 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -0500831 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800832 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -0500833 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -0700834 _check_no_stray_whitespace,
835 _check_no_long_lines,
836 _check_license,
837 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700838 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700839]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700840
Ryan Cui1562fb82011-05-09 11:01:31 -0700841
Ryan Cui9b651632011-05-11 11:38:58 -0700842# A dictionary of project-specific hooks(callbacks), indexed by project name.
843# dict[project] = [callback1, callback2]
844_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400845 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400846 "chromeos/overlays/chromeos-overlay": [_check_manifests],
847 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800848 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700849 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700850 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400851 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
852 _kernel_configcheck],
853 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400854 "chromiumos/overlays/board-overlays": [_check_manifests],
855 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
856 "chromiumos/overlays/portage-stable": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800857 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400858 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700859 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400860 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Shawn Nematbakhshb6ac17a2014-01-28 16:47:13 -0800861 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
Vadim Bendebury9ec1ae62014-04-07 16:05:25 -0700862 _check_change_has_signoff_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700863 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400864 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
865 "chromiumos/third_party/kernel-next": [_run_checkpatch,
866 _kernel_configcheck],
867 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
868 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700869}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700870
Ryan Cui1562fb82011-05-09 11:01:31 -0700871
Ryan Cui9b651632011-05-11 11:38:58 -0700872# A dictionary of flags (keys) that can appear in the config file, and the hook
873# that the flag disables (value)
874_DISABLE_FLAGS = {
875 'stray_whitespace_check': _check_no_stray_whitespace,
876 'long_line_check': _check_no_long_lines,
877 'cros_license_check': _check_license,
878 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700879 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800880 'signoff_check': _check_change_has_signoff_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700881}
882
883
Jon Salz3ee59de2012-08-18 13:54:22 +0800884def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700885 """Returns a set of hooks disabled by the current project's config file.
886
887 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800888
889 Args:
890 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700891 """
892 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800893 if not config.has_section(SECTION):
894 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700895
896 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800897 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700898 try:
Mike Frysingerae409522014-02-01 03:16:11 -0500899 if not config.getboolean(SECTION, flag):
900 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -0700901 except ValueError as e:
902 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400903 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700904
905 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
906 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
907
908
Jon Salz3ee59de2012-08-18 13:54:22 +0800909def _get_project_hook_scripts(config):
910 """Returns a list of project-specific hook scripts.
911
912 Args:
913 config: A ConfigParser for the project's config file.
914 """
915 SECTION = 'Hook Scripts'
916 if not config.has_section(SECTION):
917 return []
918
919 hook_names_values = config.items(SECTION)
920 hook_names_values.sort(key=lambda x: x[0])
921 return [x[1] for x in hook_names_values]
922
923
Ryan Cui9b651632011-05-11 11:38:58 -0700924def _get_project_hooks(project):
925 """Returns a list of hooks that need to be run for a project.
926
927 Expects to be called from within the project root.
928 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800929 config = ConfigParser.RawConfigParser()
930 try:
931 config.read(_CONFIG_FILE)
932 except ConfigParser.Error:
933 # Just use an empty config file
934 config = ConfigParser.RawConfigParser()
935
936 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700937 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
938
939 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700940 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
941 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700942
Jon Salz3ee59de2012-08-18 13:54:22 +0800943 for script in _get_project_hook_scripts(config):
944 hooks.append(functools.partial(_run_project_hook_script, script))
945
Ryan Cui9b651632011-05-11 11:38:58 -0700946 return hooks
947
948
Doug Anderson14749562013-06-26 13:38:29 -0700949def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700950 """For each project run its project specific hook from the hooks dictionary.
951
952 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700953 project: The name of project to run hooks for.
954 proj_dir: If non-None, this is the directory the project is in. If None,
955 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700956 commit_list: A list of commits to run hooks against. If None or empty list
957 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700958
959 Returns:
960 Boolean value of whether any errors were ecountered while running the hooks.
961 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700962 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700963 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
964 if len(proj_dirs) == 0:
965 print('%s cannot be found.' % project, file=sys.stderr)
966 print('Please specify a valid project.', file=sys.stderr)
967 return True
968 if len(proj_dirs) > 1:
969 print('%s is associated with multiple directories.' % project,
970 file=sys.stderr)
971 print('Please specify a directory to help disambiguate.', file=sys.stderr)
972 return True
973 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700974
Ryan Cuiec4d6332011-05-02 14:15:25 -0700975 pwd = os.getcwd()
976 # hooks assume they are run from the root of the project
977 os.chdir(proj_dir)
978
Doug Anderson14749562013-06-26 13:38:29 -0700979 if not commit_list:
980 try:
981 commit_list = _get_commits()
982 except VerifyException as e:
983 PrintErrorForProject(project, HookFailure(str(e)))
984 os.chdir(pwd)
985 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700986
Ryan Cui9b651632011-05-11 11:38:58 -0700987 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -0700988 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700989 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700990 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -0700991 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -0700992 hook_error = hook(project, commit)
993 if hook_error:
994 error_list.append(hook_error)
995 error_found = True
996 if error_list:
997 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
998 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700999
Ryan Cuiec4d6332011-05-02 14:15:25 -07001000 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001001 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001002
Mike Frysingerae409522014-02-01 03:16:11 -05001003
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001004# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001005
Ryan Cui1562fb82011-05-09 11:01:31 -07001006
Mike Frysingerae409522014-02-01 03:16:11 -05001007def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001008 """Main function invoked directly by repo.
1009
1010 This function will exit directly upon error so that repo doesn't print some
1011 obscure error message.
1012
1013 Args:
1014 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001015 worktree_list: A list of directories. It should be the same length as
1016 project_list, so that each entry in project_list matches with a directory
1017 in worktree_list. If None, we will attempt to calculate the directories
1018 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001019 kwargs: Leave this here for forward-compatibility.
1020 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001021 found_error = False
David James2edd9002013-10-11 14:09:19 -07001022 if not worktree_list:
1023 worktree_list = [None] * len(project_list)
1024 for project, worktree in zip(project_list, worktree_list):
1025 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001026 found_error = True
1027
Mike Frysingerae409522014-02-01 03:16:11 -05001028 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001029 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001030 '- To disable some source style checks, and for other hints, see '
1031 '<checkout_dir>/src/repohooks/README\n'
1032 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001033 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001034 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001035
Ryan Cui1562fb82011-05-09 11:01:31 -07001036
Doug Anderson44a644f2011-11-02 10:37:37 -07001037def _identify_project(path):
1038 """Identify the repo project associated with the given path.
1039
1040 Returns:
1041 A string indicating what project is associated with the path passed in or
1042 a blank string upon failure.
1043 """
1044 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1045 stderr=subprocess.PIPE, cwd=path).strip()
1046
1047
1048def direct_main(args, verbose=False):
1049 """Run hooks directly (outside of the context of repo).
1050
1051 # Setup for doctests below.
1052 # ...note that some tests assume that running pre-upload on this CWD is fine.
1053 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1054 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1055 >>> olddir = os.getcwd()
1056
1057 # OK to run w/ no arugments; will run with CWD.
1058 >>> os.chdir(mydir)
1059 >>> direct_main(['prog_name'], verbose=True)
1060 Running hooks on chromiumos/repohooks
1061 0
1062 >>> os.chdir(olddir)
1063
1064 # Run specifying a dir
1065 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1066 Running hooks on chromiumos/repohooks
1067 0
1068
1069 # Not a problem to use a bogus project; we'll just get default settings.
1070 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1071 Running hooks on X
1072 0
1073
1074 # Run with project but no dir
1075 >>> os.chdir(mydir)
1076 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1077 Running hooks on X
1078 0
1079 >>> os.chdir(olddir)
1080
1081 # Try with a non-git CWD
1082 >>> os.chdir('/tmp')
1083 >>> direct_main(['prog_name'])
1084 Traceback (most recent call last):
1085 ...
1086 BadInvocation: The current directory is not part of a git project.
1087
1088 # Check various bad arguments...
1089 >>> direct_main(['prog_name', 'bogus'])
1090 Traceback (most recent call last):
1091 ...
1092 BadInvocation: Unexpected arguments: bogus
1093 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1094 Traceback (most recent call last):
1095 ...
1096 BadInvocation: Invalid dir: bogusdir
1097 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1098 Traceback (most recent call last):
1099 ...
1100 BadInvocation: Not a git directory: /tmp
1101
1102 Args:
1103 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001104 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001105
1106 Returns:
1107 0 if no pre-upload failures, 1 if failures.
1108
1109 Raises:
1110 BadInvocation: On some types of invocation errors.
1111 """
1112 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1113 parser = optparse.OptionParser(description=desc)
1114
1115 parser.add_option('--dir', default=None,
1116 help='The directory that the project lives in. If not '
1117 'specified, use the git project root based on the cwd.')
1118 parser.add_option('--project', default=None,
1119 help='The project repo path; this can affect how the hooks '
1120 'get run, since some hooks are project-specific. For '
1121 'chromite this is chromiumos/chromite. If not specified, '
1122 'the repo tool will be used to figure this out based on '
1123 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001124 parser.add_option('--rerun-since', default=None,
1125 help='Rerun hooks on old commits since the given date. '
1126 'The date should match git log\'s concept of a date. '
1127 'e.g. 2012-06-20')
1128
1129 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001130
1131 opts, args = parser.parse_args(args[1:])
1132
Doug Anderson14749562013-06-26 13:38:29 -07001133 if opts.rerun_since:
1134 if args:
1135 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1136 ' '.join(args))
1137
1138 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1139 all_commits = _run_command(cmd).splitlines()
1140 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1141
1142 # Eliminate chrome-bot commits but keep ordering the same...
1143 bot_commits = set(bot_commits)
1144 args = [c for c in all_commits if c not in bot_commits]
1145
Doug Anderson44a644f2011-11-02 10:37:37 -07001146
1147 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1148 # project from CWD
1149 if opts.dir is None:
1150 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1151 stderr=subprocess.PIPE).strip()
1152 if not git_dir:
1153 raise BadInvocation('The current directory is not part of a git project.')
1154 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1155 elif not os.path.isdir(opts.dir):
1156 raise BadInvocation('Invalid dir: %s' % opts.dir)
1157 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1158 raise BadInvocation('Not a git directory: %s' % opts.dir)
1159
1160 # Identify the project if it wasn't specified; this _requires_ the repo
1161 # tool to be installed and for the project to be part of a repo checkout.
1162 if not opts.project:
1163 opts.project = _identify_project(opts.dir)
1164 if not opts.project:
1165 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1166
1167 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001168 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001169
Doug Anderson14749562013-06-26 13:38:29 -07001170 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
1171 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -07001172 if found_error:
1173 return 1
1174 return 0
1175
1176
1177def _test():
1178 """Run any built-in tests."""
1179 import doctest
1180 doctest.testmod()
1181
1182
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001183if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001184 if sys.argv[1:2] == ["--test"]:
1185 _test()
1186 exit_code = 0
1187 else:
1188 prog_name = os.path.basename(sys.argv[0])
1189 try:
1190 exit_code = direct_main(sys.argv)
1191 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001192 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001193 exit_code = 1
1194 sys.exit(exit_code)