blob: 7837cb3760dd4a02055b809ad0b576231d5c02ab [file] [log] [blame]
Doug Anderson44a644f2011-11-02 10:37:37 -07001#!/usr/bin/env python
Jon Salz98255932012-08-18 14:48:02 +08002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysingerae409522014-02-01 03:16:11 -05006"""Presubmit checks to run when doing `repo upload`.
7
8You can add new checks by adding a functions to the HOOKS constants.
9"""
10
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040011from __future__ import print_function
12
Ryan Cui9b651632011-05-11 11:38:58 -070013import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +080014import functools
Dale Curtis2975c432011-05-03 17:25:20 -070015import json
Doug Anderson44a644f2011-11-02 10:37:37 -070016import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070017import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070018import re
David Hendricks4c018e72013-02-06 13:46:38 -080019import socket
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070020import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070021import subprocess
22
Ryan Cui1562fb82011-05-09 11:01:31 -070023from errors import (VerifyException, HookFailure, PrintErrorForProject,
24 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070025
David Jamesc3b68b32013-04-03 09:17:03 -070026# If repo imports us, the __name__ will be __builtin__, and the wrapper will
27# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
28# up. The same logic also happens to work if we're executed directly.
29if __name__ in ('__builtin__', '__main__'):
30 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
31
32from chromite.lib import patch
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -080033from chromite.licensing import licenses
David Jamesc3b68b32013-04-03 09:17:03 -070034
Ryan Cuiec4d6332011-05-02 14:15:25 -070035
36COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050037 # C++ and friends
38 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
39 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
40 # Scripts
41 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
42 # No extension at all, note that ALL CAPS files are black listed in
43 # COMMON_EXCLUDED_LIST below.
44 r"(^|.*[\\\/])[^.]+$",
45 # Other
46 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070047]
48
Ryan Cui1562fb82011-05-09 11:01:31 -070049
Ryan Cuiec4d6332011-05-02 14:15:25 -070050COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050051 # avoid doing source file checks for kernel
52 r"/src/third_party/kernel/",
53 r"/src/third_party/kernel-next/",
54 r"/src/third_party/ktop/",
55 r"/src/third_party/punybench/",
56 r".*\bexperimental[\\\/].*",
57 r".*\b[A-Z0-9_]{2,}$",
58 r".*[\\\/]debian[\\\/]rules$",
59 # for ebuild trees, ignore any caches and manifest data
60 r".*/Manifest$",
61 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070062
Mike Frysingerae409522014-02-01 03:16:11 -050063 # ignore profiles data (like overlay-tegra2/profiles)
64 r".*/overlay-.*/profiles/.*",
65 # ignore minified js and jquery
66 r".*\.min\.js",
67 r".*jquery.*\.js",
Ryan Cuiec4d6332011-05-02 14:15:25 -070068]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070069
Ryan Cui1562fb82011-05-09 11:01:31 -070070
Ryan Cui9b651632011-05-11 11:38:58 -070071_CONFIG_FILE = 'PRESUBMIT.cfg'
72
73
Doug Anderson44a644f2011-11-02 10:37:37 -070074# Exceptions
75
76
77class BadInvocation(Exception):
78 """An Exception indicating a bad invocation of the program."""
79 pass
80
81
Ryan Cui1562fb82011-05-09 11:01:31 -070082# General Helpers
83
Sean Paulba01d402011-05-05 11:36:23 -040084
Doug Anderson44a644f2011-11-02 10:37:37 -070085def _run_command(cmd, cwd=None, stderr=None):
86 """Executes the passed in command and returns raw stdout output.
87
88 Args:
89 cmd: The command to run; should be a list of strings.
90 cwd: The directory to switch to for running the command.
91 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
92 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
93
94 Returns:
95 The standard out from the process.
96 """
97 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
98 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -070099
Ryan Cui1562fb82011-05-09 11:01:31 -0700100
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700101def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700102 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700103 if __name__ == '__main__':
104 # Works when file is run on its own (__file__ is defined)...
105 return os.path.abspath(os.path.dirname(__file__))
106 else:
107 # We need to do this when we're run through repo. Since repo executes
108 # us with execfile(), we don't get __file__ defined.
109 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
110 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700111
Ryan Cui1562fb82011-05-09 11:01:31 -0700112
Ryan Cuiec4d6332011-05-02 14:15:25 -0700113def _match_regex_list(subject, expressions):
114 """Try to match a list of regular expressions to a string.
115
116 Args:
117 subject: The string to match regexes on
118 expressions: A list of regular expressions to check for matches with.
119
120 Returns:
121 Whether the passed in subject matches any of the passed in regexes.
122 """
123 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500124 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700125 return True
126 return False
127
Ryan Cui1562fb82011-05-09 11:01:31 -0700128
Mike Frysingerae409522014-02-01 03:16:11 -0500129def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700130 """Filter out files based on the conditions passed in.
131
132 Args:
133 files: list of filepaths to filter
134 include_list: list of regex that when matched with a file path will cause it
135 to be added to the output list unless the file is also matched with a
136 regex in the exclude_list.
137 exclude_list: list of regex that when matched with a file will prevent it
138 from being added to the output list, even if it is also matched with a
139 regex in the include_list.
140
141 Returns:
142 A list of filepaths that contain files matched in the include_list and not
143 in the exclude_list.
144 """
145 filtered = []
146 for f in files:
147 if (_match_regex_list(f, include_list) and
148 not _match_regex_list(f, exclude_list)):
149 filtered.append(f)
150 return filtered
151
Ryan Cuiec4d6332011-05-02 14:15:25 -0700152
David Hendricks35030d02013-02-04 17:49:16 -0800153def _verify_header_content(commit, content, fail_msg):
154 """Verify that file headers contain specified content.
155
156 Args:
157 commit: the affected commit.
158 content: the content of the header to be verified.
159 fail_msg: the first message to display in case of failure.
160
161 Returns:
162 The return value of HookFailure().
163 """
164 license_re = re.compile(content, re.MULTILINE)
165 bad_files = []
166 files = _filter_files(_get_affected_files(commit),
167 COMMON_INCLUDED_PATHS,
168 COMMON_EXCLUDED_PATHS)
169
170 for f in files:
Mike Frysingerae409522014-02-01 03:16:11 -0500171 # Ignore non-existant files
172 if os.path.exists(f):
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800173 contents = open(f).read()
Mike Frysingerae409522014-02-01 03:16:11 -0500174 if not contents:
175 # Ignore empty files
176 continue
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800177 if not license_re.search(contents):
178 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800179 if bad_files:
Mike Frysingerae409522014-02-01 03:16:11 -0500180 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
181 "Found a bad header in these files:")
182 return HookFailure(msg, bad_files)
David Hendricks35030d02013-02-04 17:49:16 -0800183
184
Ryan Cuiec4d6332011-05-02 14:15:25 -0700185# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700186
187
Ryan Cui4725d952011-05-05 15:41:19 -0700188def _get_upstream_branch():
189 """Returns the upstream tracking branch of the current branch.
190
191 Raises:
192 Error if there is no tracking branch
193 """
194 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
195 current_branch = current_branch.replace('refs/heads/', '')
196 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700197 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700198
199 cfg_option = 'branch.' + current_branch + '.%s'
200 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
201 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
202 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700203 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700204
205 return full_upstream.replace('heads', 'remotes/' + remote)
206
Ryan Cui1562fb82011-05-09 11:01:31 -0700207
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700208def _get_patch(commit):
209 """Returns the patch for this commit."""
210 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700211
Ryan Cui1562fb82011-05-09 11:01:31 -0700212
Jon Salz98255932012-08-18 14:48:02 +0800213def _try_utf8_decode(data):
214 """Attempts to decode a string as UTF-8.
215
216 Returns:
217 The decoded Unicode object, or the original string if parsing fails.
218 """
219 try:
220 return unicode(data, 'utf-8', 'strict')
221 except UnicodeDecodeError:
222 return data
223
224
Mike Frysingerae409522014-02-01 03:16:11 -0500225def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700226 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysinger5122a1f2014-02-01 02:47:35 -0500227 output = _run_command(['git', 'show', '-p', '--pretty=format:',
Mike Frysingerae409522014-02-01 03:16:11 -0500228 '--no-ext-diff', commit, path])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700229
230 new_lines = []
231 line_num = 0
232 for line in output.splitlines():
233 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
234 if m:
235 line_num = int(m.groups(1)[0])
236 continue
237 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800238 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700239 if not line.startswith('-'):
240 line_num += 1
241 return new_lines
242
Ryan Cui1562fb82011-05-09 11:01:31 -0700243
Doug Anderson42b8a052013-06-26 10:45:36 -0700244def _get_affected_files(commit, include_deletes=False):
245 """Returns list of absolute filepaths that were modified/added.
246
247 Args:
248 commit: The commit
249 include_deletes: If true we'll include delete in the list.
250
251 Returns:
252 A list of modified/added (and perhaps deleted) files
253 """
Ryan Cui72834d12011-05-05 14:51:33 -0700254 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700255 files = []
256 for statusline in output.splitlines():
257 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
258 # Ignore deleted files, and return absolute paths of files
Mike Frysingerae409522014-02-01 03:16:11 -0500259 if include_deletes or m.group(1)[0] != 'D':
Ryan Cuiec4d6332011-05-02 14:15:25 -0700260 pwd = os.getcwd()
261 files.append(os.path.join(pwd, m.group(2)))
262 return files
263
Ryan Cui1562fb82011-05-09 11:01:31 -0700264
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700265def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700266 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700267 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700268 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700269
Ryan Cui1562fb82011-05-09 11:01:31 -0700270
Ryan Cuiec4d6332011-05-02 14:15:25 -0700271def _get_commit_desc(commit):
272 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400273 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700274
275
276# Common Hooks
277
Ryan Cui1562fb82011-05-09 11:01:31 -0700278
Mike Frysingerae409522014-02-01 03:16:11 -0500279def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700280 """Checks that there aren't any lines longer than maxlen characters in any of
281 the text files to be submitted.
282 """
283 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800284 SKIP_REGEXP = re.compile('|'.join([
285 r'https?://',
286 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700287
288 errors = []
289 files = _filter_files(_get_affected_files(commit),
290 COMMON_INCLUDED_PATHS,
291 COMMON_EXCLUDED_PATHS)
292
293 for afile in files:
294 for line_num, line in _get_file_diff(afile, commit):
295 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500296 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800297 continue
298
299 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
300 if len(errors) == 5: # Just show the first 5 errors.
301 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700302
303 if errors:
304 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700305 return HookFailure(msg, errors)
306
Ryan Cuiec4d6332011-05-02 14:15:25 -0700307
Mike Frysingerae409522014-02-01 03:16:11 -0500308def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700309 """Checks that there is no stray whitespace at source lines end."""
310 errors = []
311 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500312 COMMON_INCLUDED_PATHS,
313 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700314
315 for afile in files:
316 for line_num, line in _get_file_diff(afile, commit):
317 if line.rstrip() != line:
318 errors.append('%s, line %s' % (afile, line_num))
319 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700320 return HookFailure('Found line ending with white space in:', errors)
321
Ryan Cuiec4d6332011-05-02 14:15:25 -0700322
Mike Frysingerae409522014-02-01 03:16:11 -0500323def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700324 """Checks there are no unexpanded tabs."""
325 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700326 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700327 r".*\.ebuild$",
328 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500329 r".*/[M|m]akefile$",
330 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700331 ]
332
333 errors = []
334 files = _filter_files(_get_affected_files(commit),
335 COMMON_INCLUDED_PATHS,
336 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
337
338 for afile in files:
339 for line_num, line in _get_file_diff(afile, commit):
340 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500341 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700342 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700343 return HookFailure('Found a tab character in:', errors)
344
Ryan Cuiec4d6332011-05-02 14:15:25 -0700345
Mike Frysingerae409522014-02-01 03:16:11 -0500346def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700347 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700348 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700349
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700350 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700351 msg = 'Changelist description needs TEST field (after first line)'
352 return HookFailure(msg)
353
Ryan Cuiec4d6332011-05-02 14:15:25 -0700354
Mike Frysingerae409522014-02-01 03:16:11 -0500355def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700356 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
357 msg = 'Changelist has invalid CQ-DEPEND target.'
358 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
359 try:
360 patch.GetPaladinDeps(_get_commit_desc(commit))
361 except ValueError as ex:
362 return HookFailure(msg, [example, str(ex)])
363
364
Mike Frysingerae409522014-02-01 03:16:11 -0500365def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700366 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700367 OLD_BUG_RE = r'\nBUG=.*chromium-os'
368 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
369 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
370 'the chromium tracker in your BUG= line now.')
371 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700372
David James5c0073d2013-04-03 08:48:52 -0700373 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700374 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700375 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700376 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700377 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
378 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700379 return HookFailure(msg)
380
Ryan Cuiec4d6332011-05-02 14:15:25 -0700381
Doug Anderson42b8a052013-06-26 10:45:36 -0700382def _check_for_uprev(project, commit):
383 """Check that we're not missing a revbump of an ebuild in the given commit.
384
385 If the given commit touches files in a directory that has ebuilds somewhere
386 up the directory hierarchy, it's very likely that we need an ebuild revbump
387 in order for those changes to take effect.
388
389 It's not totally trivial to detect a revbump, so at least detect that an
390 ebuild with a revision number in it was touched. This should handle the
391 common case where we use a symlink to do the revbump.
392
393 TODO: it would be nice to enhance this hook to:
394 * Handle cases where people revbump with a slightly different syntax. I see
395 one ebuild (puppy) that revbumps with _pN. This is a false positive.
396 * Catches cases where people aren't using symlinks for revbumps. If they
397 edit a revisioned file directly (and are expected to rename it for revbump)
398 we'll miss that. Perhaps we could detect that the file touched is a
399 symlink?
400
401 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
402 still better off than without this check.
403
404 Args:
405 project: The project to look at
406 commit: The commit to look at
407
408 Returns:
409 A HookFailure or None.
410 """
Mike Frysinger011af942014-01-17 16:12:22 -0500411 # If this is the portage-stable overlay, then ignore the check. It's rare
412 # that we're doing anything other than importing files from upstream, so
413 # forcing a rev bump makes no sense.
414 whitelist = (
415 'chromiumos/overlays/portage-stable',
416 )
417 if project in whitelist:
418 return None
419
Doug Anderson42b8a052013-06-26 10:45:36 -0700420 affected_paths = _get_affected_files(commit, include_deletes=True)
421
422 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500423 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700424 affected_paths = [path for path in affected_paths
425 if os.path.basename(path) not in whitelist]
426 if not affected_paths:
427 return None
428
429 # If we've touched any file named with a -rN.ebuild then we'll say we're
430 # OK right away. See TODO above about enhancing this.
431 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
432 for path in affected_paths)
433 if touched_revved_ebuild:
434 return None
435
436 # We want to examine the current contents of all directories that are parents
437 # of files that were touched (up to the top of the project).
438 #
439 # ...note: we use the current directory contents even though it may have
440 # changed since the commit we're looking at. This is just a heuristic after
441 # all. Worst case we don't flag a missing revbump.
442 project_top = os.getcwd()
443 dirs_to_check = set([project_top])
444 for path in affected_paths:
445 path = os.path.dirname(path)
446 while os.path.exists(path) and not os.path.samefile(path, project_top):
447 dirs_to_check.add(path)
448 path = os.path.dirname(path)
449
450 # Look through each directory. If it's got an ebuild in it then we'll
451 # consider this as a case when we need a revbump.
452 for dir_path in dirs_to_check:
453 contents = os.listdir(dir_path)
454 ebuilds = [os.path.join(dir_path, path)
455 for path in contents if path.endswith('.ebuild')]
456 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
457
458 # If the -9999.ebuild file was touched the bot will uprev for us.
459 # ...we'll use a simple intersection here as a heuristic...
460 if set(ebuilds_9999) & set(affected_paths):
461 continue
462
463 if ebuilds:
464 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
465 'or a -r1.ebuild symlink if this is a new ebuild')
466
467 return None
468
469
Mike Frysingerae409522014-02-01 03:16:11 -0500470def _check_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500471 """Make sure we use the new style KEYWORDS when possible in ebuilds.
472
473 If an ebuild generally does not care about the arch it is running on, then
474 ebuilds should flag it with one of:
475 KEYWORDS="*" # A stable ebuild.
476 KEYWORDS="~*" # An unstable ebuild.
477 KEYWORDS="-* ..." # Is known to only work on specific arches.
478
479 Args:
480 project: The project to look at
481 commit: The commit to look at
482
483 Returns:
484 A HookFailure or None.
485 """
486 WHITELIST = set(('*', '-*', '~*'))
487
488 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
489
490 ebuilds = [x for x in _get_affected_files(commit) if x.endswith('.ebuild')]
491 for ebuild in ebuilds:
492 for _, line in _get_file_diff(ebuild, commit):
493 m = get_keywords.match(line)
494 if m:
495 keywords = set(m.group(1).split())
496 if not keywords or WHITELIST - keywords != WHITELIST:
497 continue
498
499 return HookFailure(
500 'Please update KEYWORDS to use a glob:\n'
501 'If the ebuild should be marked stable (normal for non-9999 '
502 'ebuilds):\n'
503 ' KEYWORDS="*"\n'
504 'If the ebuild should be marked unstable (normal for '
505 'cros-workon / 9999 ebuilds):\n'
506 ' KEYWORDS="~*"\n'
507 'If the ebuild needs to be marked for only specific arches,'
508 'then use -* like so:\n'
509 ' KEYWORDS="-* arm ..."\n')
510
511
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800512def _check_ebuild_licenses(_project, commit):
513 """Check if the LICENSE field in the ebuild is correct."""
514 affected_paths = _get_affected_files(commit)
515 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
516
517 # A list of licenses to ignore for now.
518 LICENSES_IGNORE = ['||', '(', ')', 'Proprietary', 'as-is']
519
520 for ebuild in touched_ebuilds:
521 # Skip virutal packages.
522 if ebuild.split('/')[-3] == 'virtual':
523 continue
524
525 try:
526 license_types = licenses.GetLicenseTypesFromEbuild(ebuild)
527 except ValueError as e:
528 return HookFailure(e.message, [ebuild])
529
530 # Also ignore licenses ending with '?'
531 for license_type in [x for x in license_types
532 if x not in LICENSES_IGNORE and not x.endswith('?')]:
533 try:
534 licenses.Licensing.FindLicenseType(license_type)
535 except AssertionError as e:
536 return HookFailure(e.message, [ebuild])
537
538
Mike Frysingerae409522014-02-01 03:16:11 -0500539def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700540 """Verify that Change-ID is present in last paragraph of commit message."""
541 desc = _get_commit_desc(commit)
542 loc = desc.rfind('\nChange-Id:')
543 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700544 return HookFailure('Change-Id must be in last paragraph of description.')
545
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700546
Mike Frysingerae409522014-02-01 03:16:11 -0500547def _check_license(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700548 """Verifies the license header."""
549 LICENSE_HEADER = (
David Hendricks0af30eb2013-02-05 11:35:56 -0800550 r".* Copyright \(c\) 20[-0-9]{2,7} The Chromium OS Authors\. All rights "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700551 r"reserved\." "\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800552 r".* Use of this source code is governed by a BSD-style license that can "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700553 "be\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800554 r".* found in the LICENSE file\."
Ryan Cuiec4d6332011-05-02 14:15:25 -0700555 "\n"
556 )
David Hendricks35030d02013-02-04 17:49:16 -0800557 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700558
David Hendricks35030d02013-02-04 17:49:16 -0800559 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700560
561
Mike Frysingerae409522014-02-01 03:16:11 -0500562def _check_google_copyright(_project, commit):
David Hendricksa0e310d2013-02-04 17:51:55 -0800563 """Verifies Google Inc. as copyright holder."""
564 LICENSE_HEADER = (
565 r".* Copyright 20[-0-9]{2,7} Google Inc\."
566 )
567 FAIL_MSG = "Copyright must match"
568
David Hendricks4c018e72013-02-06 13:46:38 -0800569 # Avoid blocking partners and external contributors.
570 fqdn = socket.getfqdn()
571 if not fqdn.endswith(".corp.google.com"):
572 return None
573
David Hendricksa0e310d2013-02-04 17:51:55 -0800574 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
575
576
Ryan Cuiec4d6332011-05-02 14:15:25 -0700577# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700578
Ryan Cui1562fb82011-05-09 11:01:31 -0700579
Mike Frysingerae409522014-02-01 03:16:11 -0500580def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700581 """Runs checkpatch.pl on the given project"""
582 hooks_dir = _get_hooks_dir()
Mike Frysingerae409522014-02-01 03:16:11 -0500583 cmd = ['%s/checkpatch.pl' % hooks_dir] + list(options) + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700584 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700585 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700586 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700587 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700588
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700589
Anton Staaf815d6852011-08-22 10:08:45 -0700590def _run_checkpatch_no_tree(project, commit):
591 return _run_checkpatch(project, commit, ['--no-tree'])
592
Mike Frysingerae409522014-02-01 03:16:11 -0500593
Randall Spangler7318fd62013-11-21 12:16:58 -0800594def _run_checkpatch_ec(project, commit):
595 """Runs checkpatch with options for Chromium EC projects."""
596 return _run_checkpatch(project, commit, ['--no-tree',
597 '--ignore=MSLEEP,VOLATILE'])
598
Mike Frysingerae409522014-02-01 03:16:11 -0500599
600def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700601 """Makes sure kernel config changes are not mixed with code changes"""
602 files = _get_affected_files(commit)
603 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
604 return HookFailure('Changes to chromeos/config/ and regular files must '
605 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700606
Mike Frysingerae409522014-02-01 03:16:11 -0500607
608def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700609 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700610 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700611 try:
612 json.load(open(f))
613 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700614 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700615
616
Mike Frysingerae409522014-02-01 03:16:11 -0500617def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400618 """Make sure Manifest files only have DIST lines"""
619 paths = []
620
621 for path in _get_affected_files(commit):
622 if os.path.basename(path) != 'Manifest':
623 continue
624 if not os.path.exists(path):
625 continue
626
627 with open(path, 'r') as f:
628 for line in f.readlines():
629 if not line.startswith('DIST '):
630 paths.append(path)
631 break
632
633 if paths:
634 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
635 ('\n'.join(paths),))
636
637
Mike Frysingerae409522014-02-01 03:16:11 -0500638def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700639 """Check for a non-empty 'BRANCH=' field in the commit message."""
640 BRANCH_RE = r'\nBRANCH=\S+'
641
642 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
643 msg = ('Changelist description needs BRANCH field (after first line)\n'
644 'E.g. BRANCH=none or BRANCH=link,snow')
645 return HookFailure(msg)
646
647
Mike Frysingerae409522014-02-01 03:16:11 -0500648def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800649 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
650 SIGNOFF_RE = r'\nSigned-off-by: \S+'
651
652 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
653 msg = ('Changelist description needs Signed-off-by: field\n'
654 'E.g. Signed-off-by: My Name <me@chromium.org>')
655 return HookFailure(msg)
656
657
Jon Salz3ee59de2012-08-18 13:54:22 +0800658def _run_project_hook_script(script, project, commit):
659 """Runs a project hook script.
660
661 The script is run with the following environment variables set:
662 PRESUBMIT_PROJECT: The affected project
663 PRESUBMIT_COMMIT: The affected commit
664 PRESUBMIT_FILES: A newline-separated list of affected files
665
666 The script is considered to fail if the exit code is non-zero. It should
667 write an error message to stdout.
668 """
669 env = dict(os.environ)
670 env['PRESUBMIT_PROJECT'] = project
671 env['PRESUBMIT_COMMIT'] = commit
672
673 # Put affected files in an environment variable
674 files = _get_affected_files(commit)
675 env['PRESUBMIT_FILES'] = '\n'.join(files)
676
677 process = subprocess.Popen(script, env=env, shell=True,
678 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800679 stdout=subprocess.PIPE,
680 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800681 stdout, _ = process.communicate()
682 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800683 if stdout:
684 stdout = re.sub('(?m)^', ' ', stdout)
685 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800686 (script, process.returncode,
687 ':\n' + stdout if stdout else ''))
688
689
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700690# Base
691
Ryan Cui1562fb82011-05-09 11:01:31 -0700692
Ryan Cui9b651632011-05-11 11:38:58 -0700693# A list of hooks that are not project-specific
694_COMMON_HOOKS = [
695 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700696 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700697 _check_change_has_test_field,
698 _check_change_has_proper_changeid,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800699 _check_ebuild_licenses,
Ryan Cui9b651632011-05-11 11:38:58 -0700700 _check_no_stray_whitespace,
701 _check_no_long_lines,
702 _check_license,
703 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700704 _check_for_uprev,
Mike Frysingerc51ece72014-01-17 16:23:40 -0500705 _check_keywords,
Ryan Cui9b651632011-05-11 11:38:58 -0700706]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700707
Ryan Cui1562fb82011-05-09 11:01:31 -0700708
Ryan Cui9b651632011-05-11 11:38:58 -0700709# A dictionary of project-specific hooks(callbacks), indexed by project name.
710# dict[project] = [callback1, callback2]
711_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400712 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400713 "chromeos/overlays/chromeos-overlay": [_check_manifests],
714 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800715 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700716 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700717 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400718 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
719 _kernel_configcheck],
720 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400721 "chromiumos/overlays/board-overlays": [_check_manifests],
722 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
723 "chromiumos/overlays/portage-stable": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800724 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400725 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700726 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400727 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Shawn Nematbakhshb6ac17a2014-01-28 16:47:13 -0800728 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
729 _check_change_has_signoff_field,
730 _check_google_copyright],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700731 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400732 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
733 "chromiumos/third_party/kernel-next": [_run_checkpatch,
734 _kernel_configcheck],
735 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
736 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700737}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700738
Ryan Cui1562fb82011-05-09 11:01:31 -0700739
Ryan Cui9b651632011-05-11 11:38:58 -0700740# A dictionary of flags (keys) that can appear in the config file, and the hook
741# that the flag disables (value)
742_DISABLE_FLAGS = {
743 'stray_whitespace_check': _check_no_stray_whitespace,
744 'long_line_check': _check_no_long_lines,
745 'cros_license_check': _check_license,
746 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700747 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800748 'signoff_check': _check_change_has_signoff_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700749}
750
751
Jon Salz3ee59de2012-08-18 13:54:22 +0800752def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700753 """Returns a set of hooks disabled by the current project's config file.
754
755 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800756
757 Args:
758 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700759 """
760 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800761 if not config.has_section(SECTION):
762 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700763
764 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800765 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700766 try:
Mike Frysingerae409522014-02-01 03:16:11 -0500767 if not config.getboolean(SECTION, flag):
768 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -0700769 except ValueError as e:
770 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400771 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700772
773 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
774 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
775
776
Jon Salz3ee59de2012-08-18 13:54:22 +0800777def _get_project_hook_scripts(config):
778 """Returns a list of project-specific hook scripts.
779
780 Args:
781 config: A ConfigParser for the project's config file.
782 """
783 SECTION = 'Hook Scripts'
784 if not config.has_section(SECTION):
785 return []
786
787 hook_names_values = config.items(SECTION)
788 hook_names_values.sort(key=lambda x: x[0])
789 return [x[1] for x in hook_names_values]
790
791
Ryan Cui9b651632011-05-11 11:38:58 -0700792def _get_project_hooks(project):
793 """Returns a list of hooks that need to be run for a project.
794
795 Expects to be called from within the project root.
796 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800797 config = ConfigParser.RawConfigParser()
798 try:
799 config.read(_CONFIG_FILE)
800 except ConfigParser.Error:
801 # Just use an empty config file
802 config = ConfigParser.RawConfigParser()
803
804 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700805 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
806
807 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700808 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
809 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700810
Jon Salz3ee59de2012-08-18 13:54:22 +0800811 for script in _get_project_hook_scripts(config):
812 hooks.append(functools.partial(_run_project_hook_script, script))
813
Ryan Cui9b651632011-05-11 11:38:58 -0700814 return hooks
815
816
Doug Anderson14749562013-06-26 13:38:29 -0700817def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700818 """For each project run its project specific hook from the hooks dictionary.
819
820 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700821 project: The name of project to run hooks for.
822 proj_dir: If non-None, this is the directory the project is in. If None,
823 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700824 commit_list: A list of commits to run hooks against. If None or empty list
825 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700826
827 Returns:
828 Boolean value of whether any errors were ecountered while running the hooks.
829 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700830 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700831 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
832 if len(proj_dirs) == 0:
833 print('%s cannot be found.' % project, file=sys.stderr)
834 print('Please specify a valid project.', file=sys.stderr)
835 return True
836 if len(proj_dirs) > 1:
837 print('%s is associated with multiple directories.' % project,
838 file=sys.stderr)
839 print('Please specify a directory to help disambiguate.', file=sys.stderr)
840 return True
841 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700842
Ryan Cuiec4d6332011-05-02 14:15:25 -0700843 pwd = os.getcwd()
844 # hooks assume they are run from the root of the project
845 os.chdir(proj_dir)
846
Doug Anderson14749562013-06-26 13:38:29 -0700847 if not commit_list:
848 try:
849 commit_list = _get_commits()
850 except VerifyException as e:
851 PrintErrorForProject(project, HookFailure(str(e)))
852 os.chdir(pwd)
853 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700854
Ryan Cui9b651632011-05-11 11:38:58 -0700855 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -0700856 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700857 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700858 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -0700859 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -0700860 hook_error = hook(project, commit)
861 if hook_error:
862 error_list.append(hook_error)
863 error_found = True
864 if error_list:
865 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
866 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700867
Ryan Cuiec4d6332011-05-02 14:15:25 -0700868 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -0700869 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700870
Mike Frysingerae409522014-02-01 03:16:11 -0500871
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700872# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700873
Ryan Cui1562fb82011-05-09 11:01:31 -0700874
Mike Frysingerae409522014-02-01 03:16:11 -0500875def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -0800876 """Main function invoked directly by repo.
877
878 This function will exit directly upon error so that repo doesn't print some
879 obscure error message.
880
881 Args:
882 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -0700883 worktree_list: A list of directories. It should be the same length as
884 project_list, so that each entry in project_list matches with a directory
885 in worktree_list. If None, we will attempt to calculate the directories
886 automatically.
Doug Anderson06456632012-01-05 11:02:14 -0800887 kwargs: Leave this here for forward-compatibility.
888 """
Ryan Cui1562fb82011-05-09 11:01:31 -0700889 found_error = False
David James2edd9002013-10-11 14:09:19 -0700890 if not worktree_list:
891 worktree_list = [None] * len(project_list)
892 for project, worktree in zip(project_list, worktree_list):
893 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -0700894 found_error = True
895
Mike Frysingerae409522014-02-01 03:16:11 -0500896 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -0700897 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -0700898 '- To disable some source style checks, and for other hints, see '
899 '<checkout_dir>/src/repohooks/README\n'
900 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400901 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -0700902 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -0700903
Ryan Cui1562fb82011-05-09 11:01:31 -0700904
Doug Anderson44a644f2011-11-02 10:37:37 -0700905def _identify_project(path):
906 """Identify the repo project associated with the given path.
907
908 Returns:
909 A string indicating what project is associated with the path passed in or
910 a blank string upon failure.
911 """
912 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
913 stderr=subprocess.PIPE, cwd=path).strip()
914
915
916def direct_main(args, verbose=False):
917 """Run hooks directly (outside of the context of repo).
918
919 # Setup for doctests below.
920 # ...note that some tests assume that running pre-upload on this CWD is fine.
921 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
922 >>> mydir = os.path.dirname(os.path.abspath(__file__))
923 >>> olddir = os.getcwd()
924
925 # OK to run w/ no arugments; will run with CWD.
926 >>> os.chdir(mydir)
927 >>> direct_main(['prog_name'], verbose=True)
928 Running hooks on chromiumos/repohooks
929 0
930 >>> os.chdir(olddir)
931
932 # Run specifying a dir
933 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
934 Running hooks on chromiumos/repohooks
935 0
936
937 # Not a problem to use a bogus project; we'll just get default settings.
938 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
939 Running hooks on X
940 0
941
942 # Run with project but no dir
943 >>> os.chdir(mydir)
944 >>> direct_main(['prog_name', '--project=X'], verbose=True)
945 Running hooks on X
946 0
947 >>> os.chdir(olddir)
948
949 # Try with a non-git CWD
950 >>> os.chdir('/tmp')
951 >>> direct_main(['prog_name'])
952 Traceback (most recent call last):
953 ...
954 BadInvocation: The current directory is not part of a git project.
955
956 # Check various bad arguments...
957 >>> direct_main(['prog_name', 'bogus'])
958 Traceback (most recent call last):
959 ...
960 BadInvocation: Unexpected arguments: bogus
961 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
962 Traceback (most recent call last):
963 ...
964 BadInvocation: Invalid dir: bogusdir
965 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
966 Traceback (most recent call last):
967 ...
968 BadInvocation: Not a git directory: /tmp
969
970 Args:
971 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -0500972 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -0700973
974 Returns:
975 0 if no pre-upload failures, 1 if failures.
976
977 Raises:
978 BadInvocation: On some types of invocation errors.
979 """
980 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
981 parser = optparse.OptionParser(description=desc)
982
983 parser.add_option('--dir', default=None,
984 help='The directory that the project lives in. If not '
985 'specified, use the git project root based on the cwd.')
986 parser.add_option('--project', default=None,
987 help='The project repo path; this can affect how the hooks '
988 'get run, since some hooks are project-specific. For '
989 'chromite this is chromiumos/chromite. If not specified, '
990 'the repo tool will be used to figure this out based on '
991 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -0700992 parser.add_option('--rerun-since', default=None,
993 help='Rerun hooks on old commits since the given date. '
994 'The date should match git log\'s concept of a date. '
995 'e.g. 2012-06-20')
996
997 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -0700998
999 opts, args = parser.parse_args(args[1:])
1000
Doug Anderson14749562013-06-26 13:38:29 -07001001 if opts.rerun_since:
1002 if args:
1003 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1004 ' '.join(args))
1005
1006 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1007 all_commits = _run_command(cmd).splitlines()
1008 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1009
1010 # Eliminate chrome-bot commits but keep ordering the same...
1011 bot_commits = set(bot_commits)
1012 args = [c for c in all_commits if c not in bot_commits]
1013
Doug Anderson44a644f2011-11-02 10:37:37 -07001014
1015 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1016 # project from CWD
1017 if opts.dir is None:
1018 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1019 stderr=subprocess.PIPE).strip()
1020 if not git_dir:
1021 raise BadInvocation('The current directory is not part of a git project.')
1022 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1023 elif not os.path.isdir(opts.dir):
1024 raise BadInvocation('Invalid dir: %s' % opts.dir)
1025 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1026 raise BadInvocation('Not a git directory: %s' % opts.dir)
1027
1028 # Identify the project if it wasn't specified; this _requires_ the repo
1029 # tool to be installed and for the project to be part of a repo checkout.
1030 if not opts.project:
1031 opts.project = _identify_project(opts.dir)
1032 if not opts.project:
1033 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1034
1035 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001036 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001037
Doug Anderson14749562013-06-26 13:38:29 -07001038 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
1039 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -07001040 if found_error:
1041 return 1
1042 return 0
1043
1044
1045def _test():
1046 """Run any built-in tests."""
1047 import doctest
1048 doctest.testmod()
1049
1050
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001051if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001052 if sys.argv[1:2] == ["--test"]:
1053 _test()
1054 exit_code = 0
1055 else:
1056 prog_name = os.path.basename(sys.argv[0])
1057 try:
1058 exit_code = direct_main(sys.argv)
1059 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001060 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001061 exit_code = 1
1062 sys.exit(exit_code)