blob: 6bc0e1587c44ab2423d39a160fa93ae0850a1318 [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 Frysinger09d6a3d2013-10-08 22:21:03 -04006from __future__ import print_function
7
Ryan Cui9b651632011-05-11 11:38:58 -07008import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +08009import functools
Dale Curtis2975c432011-05-03 17:25:20 -070010import json
Doug Anderson44a644f2011-11-02 10:37:37 -070011import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070012import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070013import re
David Hendricks4c018e72013-02-06 13:46:38 -080014import socket
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070015import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070016import subprocess
17
Ryan Cui1562fb82011-05-09 11:01:31 -070018from errors import (VerifyException, HookFailure, PrintErrorForProject,
19 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070020
David Jamesc3b68b32013-04-03 09:17:03 -070021# If repo imports us, the __name__ will be __builtin__, and the wrapper will
22# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
23# up. The same logic also happens to work if we're executed directly.
24if __name__ in ('__builtin__', '__main__'):
25 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
26
27from chromite.lib import patch
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -080028from chromite.licensing import licenses
David Jamesc3b68b32013-04-03 09:17:03 -070029
Ryan Cuiec4d6332011-05-02 14:15:25 -070030
31COMMON_INCLUDED_PATHS = [
32 # C++ and friends
33 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
34 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
35 # Scripts
36 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
37 # No extension at all, note that ALL CAPS files are black listed in
38 # COMMON_EXCLUDED_LIST below.
David Hendricks0af30eb2013-02-05 11:35:56 -080039 r"(^|.*[\\\/])[^.]+$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070040 # Other
41 r".*\.java$", r".*\.mk$", r".*\.am$",
42]
43
Ryan Cui1562fb82011-05-09 11:01:31 -070044
Ryan Cuiec4d6332011-05-02 14:15:25 -070045COMMON_EXCLUDED_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -070046 # avoid doing source file checks for kernel
47 r"/src/third_party/kernel/",
48 r"/src/third_party/kernel-next/",
Paul Taysomf8b6e012011-05-09 14:32:42 -070049 r"/src/third_party/ktop/",
50 r"/src/third_party/punybench/",
Ryan Cuiec4d6332011-05-02 14:15:25 -070051 r".*\bexperimental[\\\/].*",
52 r".*\b[A-Z0-9_]{2,}$",
53 r".*[\\\/]debian[\\\/]rules$",
Brian Harringd780d602011-10-18 16:48:08 -070054 # for ebuild trees, ignore any caches and manifest data
55 r".*/Manifest$",
56 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070057
58 # ignore profiles data (like overlay-tegra2/profiles)
59 r".*/overlay-.*/profiles/.*",
Andrew de los Reyes0e679922012-05-02 11:42:54 -070060 # ignore minified js and jquery
61 r".*\.min\.js",
62 r".*jquery.*\.js",
Ryan Cuiec4d6332011-05-02 14:15:25 -070063]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070064
Ryan Cui1562fb82011-05-09 11:01:31 -070065
Ryan Cui9b651632011-05-11 11:38:58 -070066_CONFIG_FILE = 'PRESUBMIT.cfg'
67
68
Doug Anderson44a644f2011-11-02 10:37:37 -070069# Exceptions
70
71
72class BadInvocation(Exception):
73 """An Exception indicating a bad invocation of the program."""
74 pass
75
76
Ryan Cui1562fb82011-05-09 11:01:31 -070077# General Helpers
78
Sean Paulba01d402011-05-05 11:36:23 -040079
Doug Anderson44a644f2011-11-02 10:37:37 -070080def _run_command(cmd, cwd=None, stderr=None):
81 """Executes the passed in command and returns raw stdout output.
82
83 Args:
84 cmd: The command to run; should be a list of strings.
85 cwd: The directory to switch to for running the command.
86 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
87 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
88
89 Returns:
90 The standard out from the process.
91 """
92 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
93 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -070094
Ryan Cui1562fb82011-05-09 11:01:31 -070095
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070096def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -070097 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -070098 if __name__ == '__main__':
99 # Works when file is run on its own (__file__ is defined)...
100 return os.path.abspath(os.path.dirname(__file__))
101 else:
102 # We need to do this when we're run through repo. Since repo executes
103 # us with execfile(), we don't get __file__ defined.
104 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
105 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700106
Ryan Cui1562fb82011-05-09 11:01:31 -0700107
Ryan Cuiec4d6332011-05-02 14:15:25 -0700108def _match_regex_list(subject, expressions):
109 """Try to match a list of regular expressions to a string.
110
111 Args:
112 subject: The string to match regexes on
113 expressions: A list of regular expressions to check for matches with.
114
115 Returns:
116 Whether the passed in subject matches any of the passed in regexes.
117 """
118 for expr in expressions:
119 if (re.search(expr, subject)):
120 return True
121 return False
122
Ryan Cui1562fb82011-05-09 11:01:31 -0700123
Ryan Cuiec4d6332011-05-02 14:15:25 -0700124def _filter_files(files, include_list, exclude_list=[]):
125 """Filter out files based on the conditions passed in.
126
127 Args:
128 files: list of filepaths to filter
129 include_list: list of regex that when matched with a file path will cause it
130 to be added to the output list unless the file is also matched with a
131 regex in the exclude_list.
132 exclude_list: list of regex that when matched with a file will prevent it
133 from being added to the output list, even if it is also matched with a
134 regex in the include_list.
135
136 Returns:
137 A list of filepaths that contain files matched in the include_list and not
138 in the exclude_list.
139 """
140 filtered = []
141 for f in files:
142 if (_match_regex_list(f, include_list) and
143 not _match_regex_list(f, exclude_list)):
144 filtered.append(f)
145 return filtered
146
Ryan Cuiec4d6332011-05-02 14:15:25 -0700147
David Hendricks35030d02013-02-04 17:49:16 -0800148def _verify_header_content(commit, content, fail_msg):
149 """Verify that file headers contain specified content.
150
151 Args:
152 commit: the affected commit.
153 content: the content of the header to be verified.
154 fail_msg: the first message to display in case of failure.
155
156 Returns:
157 The return value of HookFailure().
158 """
159 license_re = re.compile(content, re.MULTILINE)
160 bad_files = []
161 files = _filter_files(_get_affected_files(commit),
162 COMMON_INCLUDED_PATHS,
163 COMMON_EXCLUDED_PATHS)
164
165 for f in files:
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800166 if os.path.exists(f): # Ignore non-existant files
167 contents = open(f).read()
168 if len(contents) == 0: continue # Ignore empty files
169 if not license_re.search(contents):
170 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800171 if bad_files:
172 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
173 "Found a bad header in these files:")
174 return HookFailure(msg, bad_files)
175
176
Ryan Cuiec4d6332011-05-02 14:15:25 -0700177# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700178
179
Ryan Cui4725d952011-05-05 15:41:19 -0700180def _get_upstream_branch():
181 """Returns the upstream tracking branch of the current branch.
182
183 Raises:
184 Error if there is no tracking branch
185 """
186 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
187 current_branch = current_branch.replace('refs/heads/', '')
188 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700189 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700190
191 cfg_option = 'branch.' + current_branch + '.%s'
192 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
193 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
194 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700195 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700196
197 return full_upstream.replace('heads', 'remotes/' + remote)
198
Ryan Cui1562fb82011-05-09 11:01:31 -0700199
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700200def _get_patch(commit):
201 """Returns the patch for this commit."""
202 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700203
Ryan Cui1562fb82011-05-09 11:01:31 -0700204
Jon Salz98255932012-08-18 14:48:02 +0800205def _try_utf8_decode(data):
206 """Attempts to decode a string as UTF-8.
207
208 Returns:
209 The decoded Unicode object, or the original string if parsing fails.
210 """
211 try:
212 return unicode(data, 'utf-8', 'strict')
213 except UnicodeDecodeError:
214 return data
215
216
Ryan Cuiec4d6332011-05-02 14:15:25 -0700217def _get_file_diff(file, commit):
218 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysinger5122a1f2014-02-01 02:47:35 -0500219 output = _run_command(['git', 'show', '-p', '--pretty=format:',
220 '--no-ext-diff', commit, file])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700221
222 new_lines = []
223 line_num = 0
224 for line in output.splitlines():
225 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
226 if m:
227 line_num = int(m.groups(1)[0])
228 continue
229 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800230 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700231 if not line.startswith('-'):
232 line_num += 1
233 return new_lines
234
Ryan Cui1562fb82011-05-09 11:01:31 -0700235
Doug Anderson42b8a052013-06-26 10:45:36 -0700236def _get_affected_files(commit, include_deletes=False):
237 """Returns list of absolute filepaths that were modified/added.
238
239 Args:
240 commit: The commit
241 include_deletes: If true we'll include delete in the list.
242
243 Returns:
244 A list of modified/added (and perhaps deleted) files
245 """
Ryan Cui72834d12011-05-05 14:51:33 -0700246 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700247 files = []
248 for statusline in output.splitlines():
249 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
250 # Ignore deleted files, and return absolute paths of files
Doug Anderson42b8a052013-06-26 10:45:36 -0700251 if (include_deletes or m.group(1)[0] != 'D'):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700252 pwd = os.getcwd()
253 files.append(os.path.join(pwd, m.group(2)))
254 return files
255
Ryan Cui1562fb82011-05-09 11:01:31 -0700256
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700257def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700258 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700259 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700260 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700261
Ryan Cui1562fb82011-05-09 11:01:31 -0700262
Ryan Cuiec4d6332011-05-02 14:15:25 -0700263def _get_commit_desc(commit):
264 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400265 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700266
267
268# Common Hooks
269
Ryan Cui1562fb82011-05-09 11:01:31 -0700270
Ryan Cuiec4d6332011-05-02 14:15:25 -0700271def _check_no_long_lines(project, commit):
272 """Checks that there aren't any lines longer than maxlen characters in any of
273 the text files to be submitted.
274 """
275 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800276 SKIP_REGEXP = re.compile('|'.join([
277 r'https?://',
278 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700279
280 errors = []
281 files = _filter_files(_get_affected_files(commit),
282 COMMON_INCLUDED_PATHS,
283 COMMON_EXCLUDED_PATHS)
284
285 for afile in files:
286 for line_num, line in _get_file_diff(afile, commit):
287 # Allow certain lines to exceed the maxlen rule.
Jon Salz98255932012-08-18 14:48:02 +0800288 if (len(line) <= MAX_LEN or SKIP_REGEXP.search(line)):
289 continue
290
291 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
292 if len(errors) == 5: # Just show the first 5 errors.
293 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700294
295 if errors:
296 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700297 return HookFailure(msg, errors)
298
Ryan Cuiec4d6332011-05-02 14:15:25 -0700299
300def _check_no_stray_whitespace(project, commit):
301 """Checks that there is no stray whitespace at source lines end."""
302 errors = []
303 files = _filter_files(_get_affected_files(commit),
304 COMMON_INCLUDED_PATHS,
305 COMMON_EXCLUDED_PATHS)
306
307 for afile in files:
308 for line_num, line in _get_file_diff(afile, commit):
309 if line.rstrip() != line:
310 errors.append('%s, line %s' % (afile, line_num))
311 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700312 return HookFailure('Found line ending with white space in:', errors)
313
Ryan Cuiec4d6332011-05-02 14:15:25 -0700314
315def _check_no_tabs(project, commit):
316 """Checks there are no unexpanded tabs."""
317 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700318 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700319 r".*\.ebuild$",
320 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500321 r".*/[M|m]akefile$",
322 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700323 ]
324
325 errors = []
326 files = _filter_files(_get_affected_files(commit),
327 COMMON_INCLUDED_PATHS,
328 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
329
330 for afile in files:
331 for line_num, line in _get_file_diff(afile, commit):
332 if '\t' in line:
333 errors.append('%s, line %s' % (afile, line_num))
334 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700335 return HookFailure('Found a tab character in:', errors)
336
Ryan Cuiec4d6332011-05-02 14:15:25 -0700337
338def _check_change_has_test_field(project, commit):
339 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700340 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700341
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700342 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700343 msg = 'Changelist description needs TEST field (after first line)'
344 return HookFailure(msg)
345
Ryan Cuiec4d6332011-05-02 14:15:25 -0700346
David Jamesc3b68b32013-04-03 09:17:03 -0700347def _check_change_has_valid_cq_depend(project, commit):
348 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
349 msg = 'Changelist has invalid CQ-DEPEND target.'
350 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
351 try:
352 patch.GetPaladinDeps(_get_commit_desc(commit))
353 except ValueError as ex:
354 return HookFailure(msg, [example, str(ex)])
355
356
Ryan Cuiec4d6332011-05-02 14:15:25 -0700357def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700358 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700359 OLD_BUG_RE = r'\nBUG=.*chromium-os'
360 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
361 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
362 'the chromium tracker in your BUG= line now.')
363 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700364
David James5c0073d2013-04-03 08:48:52 -0700365 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700366 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700367 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700368 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700369 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
370 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700371 return HookFailure(msg)
372
Ryan Cuiec4d6332011-05-02 14:15:25 -0700373
Doug Anderson42b8a052013-06-26 10:45:36 -0700374def _check_for_uprev(project, commit):
375 """Check that we're not missing a revbump of an ebuild in the given commit.
376
377 If the given commit touches files in a directory that has ebuilds somewhere
378 up the directory hierarchy, it's very likely that we need an ebuild revbump
379 in order for those changes to take effect.
380
381 It's not totally trivial to detect a revbump, so at least detect that an
382 ebuild with a revision number in it was touched. This should handle the
383 common case where we use a symlink to do the revbump.
384
385 TODO: it would be nice to enhance this hook to:
386 * Handle cases where people revbump with a slightly different syntax. I see
387 one ebuild (puppy) that revbumps with _pN. This is a false positive.
388 * Catches cases where people aren't using symlinks for revbumps. If they
389 edit a revisioned file directly (and are expected to rename it for revbump)
390 we'll miss that. Perhaps we could detect that the file touched is a
391 symlink?
392
393 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
394 still better off than without this check.
395
396 Args:
397 project: The project to look at
398 commit: The commit to look at
399
400 Returns:
401 A HookFailure or None.
402 """
Mike Frysinger011af942014-01-17 16:12:22 -0500403 # If this is the portage-stable overlay, then ignore the check. It's rare
404 # that we're doing anything other than importing files from upstream, so
405 # forcing a rev bump makes no sense.
406 whitelist = (
407 'chromiumos/overlays/portage-stable',
408 )
409 if project in whitelist:
410 return None
411
Doug Anderson42b8a052013-06-26 10:45:36 -0700412 affected_paths = _get_affected_files(commit, include_deletes=True)
413
414 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500415 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700416 affected_paths = [path for path in affected_paths
417 if os.path.basename(path) not in whitelist]
418 if not affected_paths:
419 return None
420
421 # If we've touched any file named with a -rN.ebuild then we'll say we're
422 # OK right away. See TODO above about enhancing this.
423 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
424 for path in affected_paths)
425 if touched_revved_ebuild:
426 return None
427
428 # We want to examine the current contents of all directories that are parents
429 # of files that were touched (up to the top of the project).
430 #
431 # ...note: we use the current directory contents even though it may have
432 # changed since the commit we're looking at. This is just a heuristic after
433 # all. Worst case we don't flag a missing revbump.
434 project_top = os.getcwd()
435 dirs_to_check = set([project_top])
436 for path in affected_paths:
437 path = os.path.dirname(path)
438 while os.path.exists(path) and not os.path.samefile(path, project_top):
439 dirs_to_check.add(path)
440 path = os.path.dirname(path)
441
442 # Look through each directory. If it's got an ebuild in it then we'll
443 # consider this as a case when we need a revbump.
444 for dir_path in dirs_to_check:
445 contents = os.listdir(dir_path)
446 ebuilds = [os.path.join(dir_path, path)
447 for path in contents if path.endswith('.ebuild')]
448 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
449
450 # If the -9999.ebuild file was touched the bot will uprev for us.
451 # ...we'll use a simple intersection here as a heuristic...
452 if set(ebuilds_9999) & set(affected_paths):
453 continue
454
455 if ebuilds:
456 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
457 'or a -r1.ebuild symlink if this is a new ebuild')
458
459 return None
460
461
Mike Frysingerc51ece72014-01-17 16:23:40 -0500462def _check_keywords(project, commit):
463 """Make sure we use the new style KEYWORDS when possible in ebuilds.
464
465 If an ebuild generally does not care about the arch it is running on, then
466 ebuilds should flag it with one of:
467 KEYWORDS="*" # A stable ebuild.
468 KEYWORDS="~*" # An unstable ebuild.
469 KEYWORDS="-* ..." # Is known to only work on specific arches.
470
471 Args:
472 project: The project to look at
473 commit: The commit to look at
474
475 Returns:
476 A HookFailure or None.
477 """
478 WHITELIST = set(('*', '-*', '~*'))
479
480 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
481
482 ebuilds = [x for x in _get_affected_files(commit) if x.endswith('.ebuild')]
483 for ebuild in ebuilds:
484 for _, line in _get_file_diff(ebuild, commit):
485 m = get_keywords.match(line)
486 if m:
487 keywords = set(m.group(1).split())
488 if not keywords or WHITELIST - keywords != WHITELIST:
489 continue
490
491 return HookFailure(
492 'Please update KEYWORDS to use a glob:\n'
493 'If the ebuild should be marked stable (normal for non-9999 '
494 'ebuilds):\n'
495 ' KEYWORDS="*"\n'
496 'If the ebuild should be marked unstable (normal for '
497 'cros-workon / 9999 ebuilds):\n'
498 ' KEYWORDS="~*"\n'
499 'If the ebuild needs to be marked for only specific arches,'
500 'then use -* like so:\n'
501 ' KEYWORDS="-* arm ..."\n')
502
503
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800504def _check_ebuild_licenses(_project, commit):
505 """Check if the LICENSE field in the ebuild is correct."""
506 affected_paths = _get_affected_files(commit)
507 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
508
509 # A list of licenses to ignore for now.
510 LICENSES_IGNORE = ['||', '(', ')', 'Proprietary', 'as-is']
511
512 for ebuild in touched_ebuilds:
513 # Skip virutal packages.
514 if ebuild.split('/')[-3] == 'virtual':
515 continue
516
517 try:
518 license_types = licenses.GetLicenseTypesFromEbuild(ebuild)
519 except ValueError as e:
520 return HookFailure(e.message, [ebuild])
521
522 # Also ignore licenses ending with '?'
523 for license_type in [x for x in license_types
524 if x not in LICENSES_IGNORE and not x.endswith('?')]:
525 try:
526 licenses.Licensing.FindLicenseType(license_type)
527 except AssertionError as e:
528 return HookFailure(e.message, [ebuild])
529
530
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700531def _check_change_has_proper_changeid(project, commit):
532 """Verify that Change-ID is present in last paragraph of commit message."""
533 desc = _get_commit_desc(commit)
534 loc = desc.rfind('\nChange-Id:')
535 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700536 return HookFailure('Change-Id must be in last paragraph of description.')
537
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700538
Ryan Cuiec4d6332011-05-02 14:15:25 -0700539def _check_license(project, commit):
540 """Verifies the license header."""
541 LICENSE_HEADER = (
David Hendricks0af30eb2013-02-05 11:35:56 -0800542 r".* Copyright \(c\) 20[-0-9]{2,7} The Chromium OS Authors\. All rights "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700543 r"reserved\." "\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800544 r".* Use of this source code is governed by a BSD-style license that can "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700545 "be\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800546 r".* found in the LICENSE file\."
Ryan Cuiec4d6332011-05-02 14:15:25 -0700547 "\n"
548 )
David Hendricks35030d02013-02-04 17:49:16 -0800549 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700550
David Hendricks35030d02013-02-04 17:49:16 -0800551 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700552
553
David Hendricksa0e310d2013-02-04 17:51:55 -0800554def _check_google_copyright(project, commit):
555 """Verifies Google Inc. as copyright holder."""
556 LICENSE_HEADER = (
557 r".* Copyright 20[-0-9]{2,7} Google Inc\."
558 )
559 FAIL_MSG = "Copyright must match"
560
David Hendricks4c018e72013-02-06 13:46:38 -0800561 # Avoid blocking partners and external contributors.
562 fqdn = socket.getfqdn()
563 if not fqdn.endswith(".corp.google.com"):
564 return None
565
David Hendricksa0e310d2013-02-04 17:51:55 -0800566 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
567
568
Ryan Cuiec4d6332011-05-02 14:15:25 -0700569# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700570
Ryan Cui1562fb82011-05-09 11:01:31 -0700571
Anton Staaf815d6852011-08-22 10:08:45 -0700572def _run_checkpatch(project, commit, options=[]):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700573 """Runs checkpatch.pl on the given project"""
574 hooks_dir = _get_hooks_dir()
Anton Staaf815d6852011-08-22 10:08:45 -0700575 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700576 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700577 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700578 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700579 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700580
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700581
Anton Staaf815d6852011-08-22 10:08:45 -0700582def _run_checkpatch_no_tree(project, commit):
583 return _run_checkpatch(project, commit, ['--no-tree'])
584
Randall Spangler7318fd62013-11-21 12:16:58 -0800585def _run_checkpatch_ec(project, commit):
586 """Runs checkpatch with options for Chromium EC projects."""
587 return _run_checkpatch(project, commit, ['--no-tree',
588 '--ignore=MSLEEP,VOLATILE'])
589
Olof Johanssona96810f2012-09-04 16:20:03 -0700590def _kernel_configcheck(project, commit):
591 """Makes sure kernel config changes are not mixed with code changes"""
592 files = _get_affected_files(commit)
593 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
594 return HookFailure('Changes to chromeos/config/ and regular files must '
595 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700596
Dale Curtis2975c432011-05-03 17:25:20 -0700597def _run_json_check(project, commit):
598 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700599 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700600 try:
601 json.load(open(f))
602 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700603 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700604
605
Mike Frysinger52b537e2013-08-22 22:59:53 -0400606def _check_manifests(project, commit):
607 """Make sure Manifest files only have DIST lines"""
608 paths = []
609
610 for path in _get_affected_files(commit):
611 if os.path.basename(path) != 'Manifest':
612 continue
613 if not os.path.exists(path):
614 continue
615
616 with open(path, 'r') as f:
617 for line in f.readlines():
618 if not line.startswith('DIST '):
619 paths.append(path)
620 break
621
622 if paths:
623 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
624 ('\n'.join(paths),))
625
626
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700627def _check_change_has_branch_field(project, commit):
628 """Check for a non-empty 'BRANCH=' field in the commit message."""
629 BRANCH_RE = r'\nBRANCH=\S+'
630
631 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
632 msg = ('Changelist description needs BRANCH field (after first line)\n'
633 'E.g. BRANCH=none or BRANCH=link,snow')
634 return HookFailure(msg)
635
636
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800637def _check_change_has_signoff_field(project, commit):
638 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
639 SIGNOFF_RE = r'\nSigned-off-by: \S+'
640
641 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
642 msg = ('Changelist description needs Signed-off-by: field\n'
643 'E.g. Signed-off-by: My Name <me@chromium.org>')
644 return HookFailure(msg)
645
646
Jon Salz3ee59de2012-08-18 13:54:22 +0800647def _run_project_hook_script(script, project, commit):
648 """Runs a project hook script.
649
650 The script is run with the following environment variables set:
651 PRESUBMIT_PROJECT: The affected project
652 PRESUBMIT_COMMIT: The affected commit
653 PRESUBMIT_FILES: A newline-separated list of affected files
654
655 The script is considered to fail if the exit code is non-zero. It should
656 write an error message to stdout.
657 """
658 env = dict(os.environ)
659 env['PRESUBMIT_PROJECT'] = project
660 env['PRESUBMIT_COMMIT'] = commit
661
662 # Put affected files in an environment variable
663 files = _get_affected_files(commit)
664 env['PRESUBMIT_FILES'] = '\n'.join(files)
665
666 process = subprocess.Popen(script, env=env, shell=True,
667 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800668 stdout=subprocess.PIPE,
669 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800670 stdout, _ = process.communicate()
671 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800672 if stdout:
673 stdout = re.sub('(?m)^', ' ', stdout)
674 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800675 (script, process.returncode,
676 ':\n' + stdout if stdout else ''))
677
678
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700679# Base
680
Ryan Cui1562fb82011-05-09 11:01:31 -0700681
Ryan Cui9b651632011-05-11 11:38:58 -0700682# A list of hooks that are not project-specific
683_COMMON_HOOKS = [
684 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700685 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700686 _check_change_has_test_field,
687 _check_change_has_proper_changeid,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800688 _check_ebuild_licenses,
Ryan Cui9b651632011-05-11 11:38:58 -0700689 _check_no_stray_whitespace,
690 _check_no_long_lines,
691 _check_license,
692 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700693 _check_for_uprev,
Mike Frysingerc51ece72014-01-17 16:23:40 -0500694 _check_keywords,
Ryan Cui9b651632011-05-11 11:38:58 -0700695]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700696
Ryan Cui1562fb82011-05-09 11:01:31 -0700697
Ryan Cui9b651632011-05-11 11:38:58 -0700698# A dictionary of project-specific hooks(callbacks), indexed by project name.
699# dict[project] = [callback1, callback2]
700_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400701 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400702 "chromeos/overlays/chromeos-overlay": [_check_manifests],
703 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800704 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700705 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700706 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400707 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
708 _kernel_configcheck],
709 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400710 "chromiumos/overlays/board-overlays": [_check_manifests],
711 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
712 "chromiumos/overlays/portage-stable": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800713 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400714 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700715 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400716 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Shawn Nematbakhshb6ac17a2014-01-28 16:47:13 -0800717 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
718 _check_change_has_signoff_field,
719 _check_google_copyright],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700720 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400721 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
722 "chromiumos/third_party/kernel-next": [_run_checkpatch,
723 _kernel_configcheck],
724 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
725 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700726}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700727
Ryan Cui1562fb82011-05-09 11:01:31 -0700728
Ryan Cui9b651632011-05-11 11:38:58 -0700729# A dictionary of flags (keys) that can appear in the config file, and the hook
730# that the flag disables (value)
731_DISABLE_FLAGS = {
732 'stray_whitespace_check': _check_no_stray_whitespace,
733 'long_line_check': _check_no_long_lines,
734 'cros_license_check': _check_license,
735 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700736 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800737 'signoff_check': _check_change_has_signoff_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700738}
739
740
Jon Salz3ee59de2012-08-18 13:54:22 +0800741def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700742 """Returns a set of hooks disabled by the current project's config file.
743
744 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800745
746 Args:
747 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700748 """
749 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800750 if not config.has_section(SECTION):
751 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700752
753 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800754 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700755 try:
756 if not config.getboolean(SECTION, flag): disable_flags.append(flag)
757 except ValueError as e:
758 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400759 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700760
761 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
762 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
763
764
Jon Salz3ee59de2012-08-18 13:54:22 +0800765def _get_project_hook_scripts(config):
766 """Returns a list of project-specific hook scripts.
767
768 Args:
769 config: A ConfigParser for the project's config file.
770 """
771 SECTION = 'Hook Scripts'
772 if not config.has_section(SECTION):
773 return []
774
775 hook_names_values = config.items(SECTION)
776 hook_names_values.sort(key=lambda x: x[0])
777 return [x[1] for x in hook_names_values]
778
779
Ryan Cui9b651632011-05-11 11:38:58 -0700780def _get_project_hooks(project):
781 """Returns a list of hooks that need to be run for a project.
782
783 Expects to be called from within the project root.
784 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800785 config = ConfigParser.RawConfigParser()
786 try:
787 config.read(_CONFIG_FILE)
788 except ConfigParser.Error:
789 # Just use an empty config file
790 config = ConfigParser.RawConfigParser()
791
792 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700793 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
794
795 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700796 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
797 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700798
Jon Salz3ee59de2012-08-18 13:54:22 +0800799 for script in _get_project_hook_scripts(config):
800 hooks.append(functools.partial(_run_project_hook_script, script))
801
Ryan Cui9b651632011-05-11 11:38:58 -0700802 return hooks
803
804
Doug Anderson14749562013-06-26 13:38:29 -0700805def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700806 """For each project run its project specific hook from the hooks dictionary.
807
808 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700809 project: The name of project to run hooks for.
810 proj_dir: If non-None, this is the directory the project is in. If None,
811 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700812 commit_list: A list of commits to run hooks against. If None or empty list
813 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700814
815 Returns:
816 Boolean value of whether any errors were ecountered while running the hooks.
817 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700818 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700819 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
820 if len(proj_dirs) == 0:
821 print('%s cannot be found.' % project, file=sys.stderr)
822 print('Please specify a valid project.', file=sys.stderr)
823 return True
824 if len(proj_dirs) > 1:
825 print('%s is associated with multiple directories.' % project,
826 file=sys.stderr)
827 print('Please specify a directory to help disambiguate.', file=sys.stderr)
828 return True
829 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700830
Ryan Cuiec4d6332011-05-02 14:15:25 -0700831 pwd = os.getcwd()
832 # hooks assume they are run from the root of the project
833 os.chdir(proj_dir)
834
Doug Anderson14749562013-06-26 13:38:29 -0700835 if not commit_list:
836 try:
837 commit_list = _get_commits()
838 except VerifyException as e:
839 PrintErrorForProject(project, HookFailure(str(e)))
840 os.chdir(pwd)
841 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700842
Ryan Cui9b651632011-05-11 11:38:58 -0700843 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -0700844 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700845 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700846 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -0700847 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -0700848 hook_error = hook(project, commit)
849 if hook_error:
850 error_list.append(hook_error)
851 error_found = True
852 if error_list:
853 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
854 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700855
Ryan Cuiec4d6332011-05-02 14:15:25 -0700856 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -0700857 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700858
859# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700860
Ryan Cui1562fb82011-05-09 11:01:31 -0700861
David James2edd9002013-10-11 14:09:19 -0700862def main(project_list, worktree_list=None, **kwargs):
Doug Anderson06456632012-01-05 11:02:14 -0800863 """Main function invoked directly by repo.
864
865 This function will exit directly upon error so that repo doesn't print some
866 obscure error message.
867
868 Args:
869 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -0700870 worktree_list: A list of directories. It should be the same length as
871 project_list, so that each entry in project_list matches with a directory
872 in worktree_list. If None, we will attempt to calculate the directories
873 automatically.
Doug Anderson06456632012-01-05 11:02:14 -0800874 kwargs: Leave this here for forward-compatibility.
875 """
Ryan Cui1562fb82011-05-09 11:01:31 -0700876 found_error = False
David James2edd9002013-10-11 14:09:19 -0700877 if not worktree_list:
878 worktree_list = [None] * len(project_list)
879 for project, worktree in zip(project_list, worktree_list):
880 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -0700881 found_error = True
882
883 if (found_error):
884 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -0700885 '- To disable some source style checks, and for other hints, see '
886 '<checkout_dir>/src/repohooks/README\n'
887 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400888 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -0700889 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -0700890
Ryan Cui1562fb82011-05-09 11:01:31 -0700891
Doug Anderson44a644f2011-11-02 10:37:37 -0700892def _identify_project(path):
893 """Identify the repo project associated with the given path.
894
895 Returns:
896 A string indicating what project is associated with the path passed in or
897 a blank string upon failure.
898 """
899 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
900 stderr=subprocess.PIPE, cwd=path).strip()
901
902
903def direct_main(args, verbose=False):
904 """Run hooks directly (outside of the context of repo).
905
906 # Setup for doctests below.
907 # ...note that some tests assume that running pre-upload on this CWD is fine.
908 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
909 >>> mydir = os.path.dirname(os.path.abspath(__file__))
910 >>> olddir = os.getcwd()
911
912 # OK to run w/ no arugments; will run with CWD.
913 >>> os.chdir(mydir)
914 >>> direct_main(['prog_name'], verbose=True)
915 Running hooks on chromiumos/repohooks
916 0
917 >>> os.chdir(olddir)
918
919 # Run specifying a dir
920 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
921 Running hooks on chromiumos/repohooks
922 0
923
924 # Not a problem to use a bogus project; we'll just get default settings.
925 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
926 Running hooks on X
927 0
928
929 # Run with project but no dir
930 >>> os.chdir(mydir)
931 >>> direct_main(['prog_name', '--project=X'], verbose=True)
932 Running hooks on X
933 0
934 >>> os.chdir(olddir)
935
936 # Try with a non-git CWD
937 >>> os.chdir('/tmp')
938 >>> direct_main(['prog_name'])
939 Traceback (most recent call last):
940 ...
941 BadInvocation: The current directory is not part of a git project.
942
943 # Check various bad arguments...
944 >>> direct_main(['prog_name', 'bogus'])
945 Traceback (most recent call last):
946 ...
947 BadInvocation: Unexpected arguments: bogus
948 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
949 Traceback (most recent call last):
950 ...
951 BadInvocation: Invalid dir: bogusdir
952 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
953 Traceback (most recent call last):
954 ...
955 BadInvocation: Not a git directory: /tmp
956
957 Args:
958 args: The value of sys.argv
959
960 Returns:
961 0 if no pre-upload failures, 1 if failures.
962
963 Raises:
964 BadInvocation: On some types of invocation errors.
965 """
966 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
967 parser = optparse.OptionParser(description=desc)
968
969 parser.add_option('--dir', default=None,
970 help='The directory that the project lives in. If not '
971 'specified, use the git project root based on the cwd.')
972 parser.add_option('--project', default=None,
973 help='The project repo path; this can affect how the hooks '
974 'get run, since some hooks are project-specific. For '
975 'chromite this is chromiumos/chromite. If not specified, '
976 'the repo tool will be used to figure this out based on '
977 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -0700978 parser.add_option('--rerun-since', default=None,
979 help='Rerun hooks on old commits since the given date. '
980 'The date should match git log\'s concept of a date. '
981 'e.g. 2012-06-20')
982
983 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -0700984
985 opts, args = parser.parse_args(args[1:])
986
Doug Anderson14749562013-06-26 13:38:29 -0700987 if opts.rerun_since:
988 if args:
989 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
990 ' '.join(args))
991
992 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
993 all_commits = _run_command(cmd).splitlines()
994 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
995
996 # Eliminate chrome-bot commits but keep ordering the same...
997 bot_commits = set(bot_commits)
998 args = [c for c in all_commits if c not in bot_commits]
999
Doug Anderson44a644f2011-11-02 10:37:37 -07001000
1001 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1002 # project from CWD
1003 if opts.dir is None:
1004 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1005 stderr=subprocess.PIPE).strip()
1006 if not git_dir:
1007 raise BadInvocation('The current directory is not part of a git project.')
1008 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1009 elif not os.path.isdir(opts.dir):
1010 raise BadInvocation('Invalid dir: %s' % opts.dir)
1011 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1012 raise BadInvocation('Not a git directory: %s' % opts.dir)
1013
1014 # Identify the project if it wasn't specified; this _requires_ the repo
1015 # tool to be installed and for the project to be part of a repo checkout.
1016 if not opts.project:
1017 opts.project = _identify_project(opts.dir)
1018 if not opts.project:
1019 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1020
1021 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001022 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001023
Doug Anderson14749562013-06-26 13:38:29 -07001024 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
1025 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -07001026 if found_error:
1027 return 1
1028 return 0
1029
1030
1031def _test():
1032 """Run any built-in tests."""
1033 import doctest
1034 doctest.testmod()
1035
1036
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001037if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001038 if sys.argv[1:2] == ["--test"]:
1039 _test()
1040 exit_code = 0
1041 else:
1042 prog_name = os.path.basename(sys.argv[0])
1043 try:
1044 exit_code = direct_main(sys.argv)
1045 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001046 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001047 exit_code = 1
1048 sys.exit(exit_code)