blob: 9ec5d1d70eccd1271d5df17ff92fc6f75c81a2af [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."""
Ryan Cui72834d12011-05-05 14:51:33 -0700219 output = _run_command(['git', 'show', '-p', '--no-ext-diff', commit, file])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700220
221 new_lines = []
222 line_num = 0
223 for line in output.splitlines():
224 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
225 if m:
226 line_num = int(m.groups(1)[0])
227 continue
228 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800229 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700230 if not line.startswith('-'):
231 line_num += 1
232 return new_lines
233
Ryan Cui1562fb82011-05-09 11:01:31 -0700234
Doug Anderson42b8a052013-06-26 10:45:36 -0700235def _get_affected_files(commit, include_deletes=False):
236 """Returns list of absolute filepaths that were modified/added.
237
238 Args:
239 commit: The commit
240 include_deletes: If true we'll include delete in the list.
241
242 Returns:
243 A list of modified/added (and perhaps deleted) files
244 """
Ryan Cui72834d12011-05-05 14:51:33 -0700245 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700246 files = []
247 for statusline in output.splitlines():
248 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
249 # Ignore deleted files, and return absolute paths of files
Doug Anderson42b8a052013-06-26 10:45:36 -0700250 if (include_deletes or m.group(1)[0] != 'D'):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700251 pwd = os.getcwd()
252 files.append(os.path.join(pwd, m.group(2)))
253 return files
254
Ryan Cui1562fb82011-05-09 11:01:31 -0700255
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700256def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700257 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700258 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700259 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700260
Ryan Cui1562fb82011-05-09 11:01:31 -0700261
Ryan Cuiec4d6332011-05-02 14:15:25 -0700262def _get_commit_desc(commit):
263 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400264 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700265
266
267# Common Hooks
268
Ryan Cui1562fb82011-05-09 11:01:31 -0700269
Ryan Cuiec4d6332011-05-02 14:15:25 -0700270def _check_no_long_lines(project, commit):
271 """Checks that there aren't any lines longer than maxlen characters in any of
272 the text files to be submitted.
273 """
274 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800275 SKIP_REGEXP = re.compile('|'.join([
276 r'https?://',
277 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700278
279 errors = []
280 files = _filter_files(_get_affected_files(commit),
281 COMMON_INCLUDED_PATHS,
282 COMMON_EXCLUDED_PATHS)
283
284 for afile in files:
285 for line_num, line in _get_file_diff(afile, commit):
286 # Allow certain lines to exceed the maxlen rule.
Jon Salz98255932012-08-18 14:48:02 +0800287 if (len(line) <= MAX_LEN or SKIP_REGEXP.search(line)):
288 continue
289
290 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
291 if len(errors) == 5: # Just show the first 5 errors.
292 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700293
294 if errors:
295 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700296 return HookFailure(msg, errors)
297
Ryan Cuiec4d6332011-05-02 14:15:25 -0700298
299def _check_no_stray_whitespace(project, commit):
300 """Checks that there is no stray whitespace at source lines end."""
301 errors = []
302 files = _filter_files(_get_affected_files(commit),
303 COMMON_INCLUDED_PATHS,
304 COMMON_EXCLUDED_PATHS)
305
306 for afile in files:
307 for line_num, line in _get_file_diff(afile, commit):
308 if line.rstrip() != line:
309 errors.append('%s, line %s' % (afile, line_num))
310 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700311 return HookFailure('Found line ending with white space in:', errors)
312
Ryan Cuiec4d6332011-05-02 14:15:25 -0700313
314def _check_no_tabs(project, commit):
315 """Checks there are no unexpanded tabs."""
316 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700317 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700318 r".*\.ebuild$",
319 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500320 r".*/[M|m]akefile$",
321 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700322 ]
323
324 errors = []
325 files = _filter_files(_get_affected_files(commit),
326 COMMON_INCLUDED_PATHS,
327 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
328
329 for afile in files:
330 for line_num, line in _get_file_diff(afile, commit):
331 if '\t' in line:
332 errors.append('%s, line %s' % (afile, line_num))
333 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700334 return HookFailure('Found a tab character in:', errors)
335
Ryan Cuiec4d6332011-05-02 14:15:25 -0700336
337def _check_change_has_test_field(project, commit):
338 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700339 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700340
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700341 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700342 msg = 'Changelist description needs TEST field (after first line)'
343 return HookFailure(msg)
344
Ryan Cuiec4d6332011-05-02 14:15:25 -0700345
David Jamesc3b68b32013-04-03 09:17:03 -0700346def _check_change_has_valid_cq_depend(project, commit):
347 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
348 msg = 'Changelist has invalid CQ-DEPEND target.'
349 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
350 try:
351 patch.GetPaladinDeps(_get_commit_desc(commit))
352 except ValueError as ex:
353 return HookFailure(msg, [example, str(ex)])
354
355
Ryan Cuiec4d6332011-05-02 14:15:25 -0700356def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700357 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700358 OLD_BUG_RE = r'\nBUG=.*chromium-os'
359 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
360 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
361 'the chromium tracker in your BUG= line now.')
362 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700363
David James5c0073d2013-04-03 08:48:52 -0700364 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700365 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700366 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700367 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700368 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
369 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700370 return HookFailure(msg)
371
Ryan Cuiec4d6332011-05-02 14:15:25 -0700372
Doug Anderson42b8a052013-06-26 10:45:36 -0700373def _check_for_uprev(project, commit):
374 """Check that we're not missing a revbump of an ebuild in the given commit.
375
376 If the given commit touches files in a directory that has ebuilds somewhere
377 up the directory hierarchy, it's very likely that we need an ebuild revbump
378 in order for those changes to take effect.
379
380 It's not totally trivial to detect a revbump, so at least detect that an
381 ebuild with a revision number in it was touched. This should handle the
382 common case where we use a symlink to do the revbump.
383
384 TODO: it would be nice to enhance this hook to:
385 * Handle cases where people revbump with a slightly different syntax. I see
386 one ebuild (puppy) that revbumps with _pN. This is a false positive.
387 * Catches cases where people aren't using symlinks for revbumps. If they
388 edit a revisioned file directly (and are expected to rename it for revbump)
389 we'll miss that. Perhaps we could detect that the file touched is a
390 symlink?
391
392 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
393 still better off than without this check.
394
395 Args:
396 project: The project to look at
397 commit: The commit to look at
398
399 Returns:
400 A HookFailure or None.
401 """
Mike Frysinger011af942014-01-17 16:12:22 -0500402 # If this is the portage-stable overlay, then ignore the check. It's rare
403 # that we're doing anything other than importing files from upstream, so
404 # forcing a rev bump makes no sense.
405 whitelist = (
406 'chromiumos/overlays/portage-stable',
407 )
408 if project in whitelist:
409 return None
410
Doug Anderson42b8a052013-06-26 10:45:36 -0700411 affected_paths = _get_affected_files(commit, include_deletes=True)
412
413 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500414 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700415 affected_paths = [path for path in affected_paths
416 if os.path.basename(path) not in whitelist]
417 if not affected_paths:
418 return None
419
420 # If we've touched any file named with a -rN.ebuild then we'll say we're
421 # OK right away. See TODO above about enhancing this.
422 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
423 for path in affected_paths)
424 if touched_revved_ebuild:
425 return None
426
427 # We want to examine the current contents of all directories that are parents
428 # of files that were touched (up to the top of the project).
429 #
430 # ...note: we use the current directory contents even though it may have
431 # changed since the commit we're looking at. This is just a heuristic after
432 # all. Worst case we don't flag a missing revbump.
433 project_top = os.getcwd()
434 dirs_to_check = set([project_top])
435 for path in affected_paths:
436 path = os.path.dirname(path)
437 while os.path.exists(path) and not os.path.samefile(path, project_top):
438 dirs_to_check.add(path)
439 path = os.path.dirname(path)
440
441 # Look through each directory. If it's got an ebuild in it then we'll
442 # consider this as a case when we need a revbump.
443 for dir_path in dirs_to_check:
444 contents = os.listdir(dir_path)
445 ebuilds = [os.path.join(dir_path, path)
446 for path in contents if path.endswith('.ebuild')]
447 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
448
449 # If the -9999.ebuild file was touched the bot will uprev for us.
450 # ...we'll use a simple intersection here as a heuristic...
451 if set(ebuilds_9999) & set(affected_paths):
452 continue
453
454 if ebuilds:
455 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
456 'or a -r1.ebuild symlink if this is a new ebuild')
457
458 return None
459
460
Mike Frysingerc51ece72014-01-17 16:23:40 -0500461def _check_keywords(project, commit):
462 """Make sure we use the new style KEYWORDS when possible in ebuilds.
463
464 If an ebuild generally does not care about the arch it is running on, then
465 ebuilds should flag it with one of:
466 KEYWORDS="*" # A stable ebuild.
467 KEYWORDS="~*" # An unstable ebuild.
468 KEYWORDS="-* ..." # Is known to only work on specific arches.
469
470 Args:
471 project: The project to look at
472 commit: The commit to look at
473
474 Returns:
475 A HookFailure or None.
476 """
477 WHITELIST = set(('*', '-*', '~*'))
478
479 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
480
481 ebuilds = [x for x in _get_affected_files(commit) if x.endswith('.ebuild')]
482 for ebuild in ebuilds:
483 for _, line in _get_file_diff(ebuild, commit):
484 m = get_keywords.match(line)
485 if m:
486 keywords = set(m.group(1).split())
487 if not keywords or WHITELIST - keywords != WHITELIST:
488 continue
489
490 return HookFailure(
491 'Please update KEYWORDS to use a glob:\n'
492 'If the ebuild should be marked stable (normal for non-9999 '
493 'ebuilds):\n'
494 ' KEYWORDS="*"\n'
495 'If the ebuild should be marked unstable (normal for '
496 'cros-workon / 9999 ebuilds):\n'
497 ' KEYWORDS="~*"\n'
498 'If the ebuild needs to be marked for only specific arches,'
499 'then use -* like so:\n'
500 ' KEYWORDS="-* arm ..."\n')
501
502
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800503def _check_ebuild_licenses(_project, commit):
504 """Check if the LICENSE field in the ebuild is correct."""
505 affected_paths = _get_affected_files(commit)
506 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
507
508 # A list of licenses to ignore for now.
509 LICENSES_IGNORE = ['||', '(', ')', 'Proprietary', 'as-is']
510
511 for ebuild in touched_ebuilds:
512 # Skip virutal packages.
513 if ebuild.split('/')[-3] == 'virtual':
514 continue
515
516 try:
517 license_types = licenses.GetLicenseTypesFromEbuild(ebuild)
518 except ValueError as e:
519 return HookFailure(e.message, [ebuild])
520
521 # Also ignore licenses ending with '?'
522 for license_type in [x for x in license_types
523 if x not in LICENSES_IGNORE and not x.endswith('?')]:
524 try:
525 licenses.Licensing.FindLicenseType(license_type)
526 except AssertionError as e:
527 return HookFailure(e.message, [ebuild])
528
529
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700530def _check_change_has_proper_changeid(project, commit):
531 """Verify that Change-ID is present in last paragraph of commit message."""
532 desc = _get_commit_desc(commit)
533 loc = desc.rfind('\nChange-Id:')
534 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700535 return HookFailure('Change-Id must be in last paragraph of description.')
536
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700537
Ryan Cuiec4d6332011-05-02 14:15:25 -0700538def _check_license(project, commit):
539 """Verifies the license header."""
540 LICENSE_HEADER = (
David Hendricks0af30eb2013-02-05 11:35:56 -0800541 r".* Copyright \(c\) 20[-0-9]{2,7} The Chromium OS Authors\. All rights "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700542 r"reserved\." "\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800543 r".* Use of this source code is governed by a BSD-style license that can "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700544 "be\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800545 r".* found in the LICENSE file\."
Ryan Cuiec4d6332011-05-02 14:15:25 -0700546 "\n"
547 )
David Hendricks35030d02013-02-04 17:49:16 -0800548 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700549
David Hendricks35030d02013-02-04 17:49:16 -0800550 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700551
552
David Hendricksa0e310d2013-02-04 17:51:55 -0800553def _check_google_copyright(project, commit):
554 """Verifies Google Inc. as copyright holder."""
555 LICENSE_HEADER = (
556 r".* Copyright 20[-0-9]{2,7} Google Inc\."
557 )
558 FAIL_MSG = "Copyright must match"
559
David Hendricks4c018e72013-02-06 13:46:38 -0800560 # Avoid blocking partners and external contributors.
561 fqdn = socket.getfqdn()
562 if not fqdn.endswith(".corp.google.com"):
563 return None
564
David Hendricksa0e310d2013-02-04 17:51:55 -0800565 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
566
567
Ryan Cuiec4d6332011-05-02 14:15:25 -0700568# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700569
Ryan Cui1562fb82011-05-09 11:01:31 -0700570
Anton Staaf815d6852011-08-22 10:08:45 -0700571def _run_checkpatch(project, commit, options=[]):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700572 """Runs checkpatch.pl on the given project"""
573 hooks_dir = _get_hooks_dir()
Anton Staaf815d6852011-08-22 10:08:45 -0700574 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700575 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700576 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700577 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700578 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700579
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700580
Anton Staaf815d6852011-08-22 10:08:45 -0700581def _run_checkpatch_no_tree(project, commit):
582 return _run_checkpatch(project, commit, ['--no-tree'])
583
Randall Spangler7318fd62013-11-21 12:16:58 -0800584def _run_checkpatch_ec(project, commit):
585 """Runs checkpatch with options for Chromium EC projects."""
586 return _run_checkpatch(project, commit, ['--no-tree',
587 '--ignore=MSLEEP,VOLATILE'])
588
Olof Johanssona96810f2012-09-04 16:20:03 -0700589def _kernel_configcheck(project, commit):
590 """Makes sure kernel config changes are not mixed with code changes"""
591 files = _get_affected_files(commit)
592 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
593 return HookFailure('Changes to chromeos/config/ and regular files must '
594 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700595
Dale Curtis2975c432011-05-03 17:25:20 -0700596def _run_json_check(project, commit):
597 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700598 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700599 try:
600 json.load(open(f))
601 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700602 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700603
604
Mike Frysinger52b537e2013-08-22 22:59:53 -0400605def _check_manifests(project, commit):
606 """Make sure Manifest files only have DIST lines"""
607 paths = []
608
609 for path in _get_affected_files(commit):
610 if os.path.basename(path) != 'Manifest':
611 continue
612 if not os.path.exists(path):
613 continue
614
615 with open(path, 'r') as f:
616 for line in f.readlines():
617 if not line.startswith('DIST '):
618 paths.append(path)
619 break
620
621 if paths:
622 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
623 ('\n'.join(paths),))
624
625
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700626def _check_change_has_branch_field(project, commit):
627 """Check for a non-empty 'BRANCH=' field in the commit message."""
628 BRANCH_RE = r'\nBRANCH=\S+'
629
630 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
631 msg = ('Changelist description needs BRANCH field (after first line)\n'
632 'E.g. BRANCH=none or BRANCH=link,snow')
633 return HookFailure(msg)
634
635
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800636def _check_change_has_signoff_field(project, commit):
637 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
638 SIGNOFF_RE = r'\nSigned-off-by: \S+'
639
640 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
641 msg = ('Changelist description needs Signed-off-by: field\n'
642 'E.g. Signed-off-by: My Name <me@chromium.org>')
643 return HookFailure(msg)
644
645
Jon Salz3ee59de2012-08-18 13:54:22 +0800646def _run_project_hook_script(script, project, commit):
647 """Runs a project hook script.
648
649 The script is run with the following environment variables set:
650 PRESUBMIT_PROJECT: The affected project
651 PRESUBMIT_COMMIT: The affected commit
652 PRESUBMIT_FILES: A newline-separated list of affected files
653
654 The script is considered to fail if the exit code is non-zero. It should
655 write an error message to stdout.
656 """
657 env = dict(os.environ)
658 env['PRESUBMIT_PROJECT'] = project
659 env['PRESUBMIT_COMMIT'] = commit
660
661 # Put affected files in an environment variable
662 files = _get_affected_files(commit)
663 env['PRESUBMIT_FILES'] = '\n'.join(files)
664
665 process = subprocess.Popen(script, env=env, shell=True,
666 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800667 stdout=subprocess.PIPE,
668 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800669 stdout, _ = process.communicate()
670 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800671 if stdout:
672 stdout = re.sub('(?m)^', ' ', stdout)
673 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800674 (script, process.returncode,
675 ':\n' + stdout if stdout else ''))
676
677
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700678# Base
679
Ryan Cui1562fb82011-05-09 11:01:31 -0700680
Ryan Cui9b651632011-05-11 11:38:58 -0700681# A list of hooks that are not project-specific
682_COMMON_HOOKS = [
683 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700684 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700685 _check_change_has_test_field,
686 _check_change_has_proper_changeid,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800687 _check_ebuild_licenses,
Ryan Cui9b651632011-05-11 11:38:58 -0700688 _check_no_stray_whitespace,
689 _check_no_long_lines,
690 _check_license,
691 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700692 _check_for_uprev,
Mike Frysingerc51ece72014-01-17 16:23:40 -0500693 _check_keywords,
Ryan Cui9b651632011-05-11 11:38:58 -0700694]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700695
Ryan Cui1562fb82011-05-09 11:01:31 -0700696
Ryan Cui9b651632011-05-11 11:38:58 -0700697# A dictionary of project-specific hooks(callbacks), indexed by project name.
698# dict[project] = [callback1, callback2]
699_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400700 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400701 "chromeos/overlays/chromeos-overlay": [_check_manifests],
702 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800703 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700704 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700705 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400706 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
707 _kernel_configcheck],
708 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
Mike Frysinger52b537e2013-08-22 22:59:53 -0400709 "chromiumos/overlays/board-overlays": [_check_manifests],
710 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
711 "chromiumos/overlays/portage-stable": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -0800712 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -0400713 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700714 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400715 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Shawn Nematbakhshb6ac17a2014-01-28 16:47:13 -0800716 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
717 _check_change_has_signoff_field,
718 _check_google_copyright],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700719 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400720 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
721 "chromiumos/third_party/kernel-next": [_run_checkpatch,
722 _kernel_configcheck],
723 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
724 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700725}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700726
Ryan Cui1562fb82011-05-09 11:01:31 -0700727
Ryan Cui9b651632011-05-11 11:38:58 -0700728# A dictionary of flags (keys) that can appear in the config file, and the hook
729# that the flag disables (value)
730_DISABLE_FLAGS = {
731 'stray_whitespace_check': _check_no_stray_whitespace,
732 'long_line_check': _check_no_long_lines,
733 'cros_license_check': _check_license,
734 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700735 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800736 'signoff_check': _check_change_has_signoff_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700737}
738
739
Jon Salz3ee59de2012-08-18 13:54:22 +0800740def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700741 """Returns a set of hooks disabled by the current project's config file.
742
743 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800744
745 Args:
746 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700747 """
748 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800749 if not config.has_section(SECTION):
750 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700751
752 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800753 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700754 try:
755 if not config.getboolean(SECTION, flag): disable_flags.append(flag)
756 except ValueError as e:
757 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400758 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -0700759
760 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
761 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
762
763
Jon Salz3ee59de2012-08-18 13:54:22 +0800764def _get_project_hook_scripts(config):
765 """Returns a list of project-specific hook scripts.
766
767 Args:
768 config: A ConfigParser for the project's config file.
769 """
770 SECTION = 'Hook Scripts'
771 if not config.has_section(SECTION):
772 return []
773
774 hook_names_values = config.items(SECTION)
775 hook_names_values.sort(key=lambda x: x[0])
776 return [x[1] for x in hook_names_values]
777
778
Ryan Cui9b651632011-05-11 11:38:58 -0700779def _get_project_hooks(project):
780 """Returns a list of hooks that need to be run for a project.
781
782 Expects to be called from within the project root.
783 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800784 config = ConfigParser.RawConfigParser()
785 try:
786 config.read(_CONFIG_FILE)
787 except ConfigParser.Error:
788 # Just use an empty config file
789 config = ConfigParser.RawConfigParser()
790
791 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700792 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
793
794 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700795 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
796 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700797
Jon Salz3ee59de2012-08-18 13:54:22 +0800798 for script in _get_project_hook_scripts(config):
799 hooks.append(functools.partial(_run_project_hook_script, script))
800
Ryan Cui9b651632011-05-11 11:38:58 -0700801 return hooks
802
803
Doug Anderson14749562013-06-26 13:38:29 -0700804def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700805 """For each project run its project specific hook from the hooks dictionary.
806
807 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700808 project: The name of project to run hooks for.
809 proj_dir: If non-None, this is the directory the project is in. If None,
810 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700811 commit_list: A list of commits to run hooks against. If None or empty list
812 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700813
814 Returns:
815 Boolean value of whether any errors were ecountered while running the hooks.
816 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700817 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -0700818 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
819 if len(proj_dirs) == 0:
820 print('%s cannot be found.' % project, file=sys.stderr)
821 print('Please specify a valid project.', file=sys.stderr)
822 return True
823 if len(proj_dirs) > 1:
824 print('%s is associated with multiple directories.' % project,
825 file=sys.stderr)
826 print('Please specify a directory to help disambiguate.', file=sys.stderr)
827 return True
828 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -0700829
Ryan Cuiec4d6332011-05-02 14:15:25 -0700830 pwd = os.getcwd()
831 # hooks assume they are run from the root of the project
832 os.chdir(proj_dir)
833
Doug Anderson14749562013-06-26 13:38:29 -0700834 if not commit_list:
835 try:
836 commit_list = _get_commits()
837 except VerifyException as e:
838 PrintErrorForProject(project, HookFailure(str(e)))
839 os.chdir(pwd)
840 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700841
Ryan Cui9b651632011-05-11 11:38:58 -0700842 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -0700843 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700844 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700845 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -0700846 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -0700847 hook_error = hook(project, commit)
848 if hook_error:
849 error_list.append(hook_error)
850 error_found = True
851 if error_list:
852 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
853 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700854
Ryan Cuiec4d6332011-05-02 14:15:25 -0700855 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -0700856 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700857
858# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700859
Ryan Cui1562fb82011-05-09 11:01:31 -0700860
David James2edd9002013-10-11 14:09:19 -0700861def main(project_list, worktree_list=None, **kwargs):
Doug Anderson06456632012-01-05 11:02:14 -0800862 """Main function invoked directly by repo.
863
864 This function will exit directly upon error so that repo doesn't print some
865 obscure error message.
866
867 Args:
868 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -0700869 worktree_list: A list of directories. It should be the same length as
870 project_list, so that each entry in project_list matches with a directory
871 in worktree_list. If None, we will attempt to calculate the directories
872 automatically.
Doug Anderson06456632012-01-05 11:02:14 -0800873 kwargs: Leave this here for forward-compatibility.
874 """
Ryan Cui1562fb82011-05-09 11:01:31 -0700875 found_error = False
David James2edd9002013-10-11 14:09:19 -0700876 if not worktree_list:
877 worktree_list = [None] * len(project_list)
878 for project, worktree in zip(project_list, worktree_list):
879 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -0700880 found_error = True
881
882 if (found_error):
883 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -0700884 '- To disable some source style checks, and for other hints, see '
885 '<checkout_dir>/src/repohooks/README\n'
886 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -0400887 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -0700888 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -0700889
Ryan Cui1562fb82011-05-09 11:01:31 -0700890
Doug Anderson44a644f2011-11-02 10:37:37 -0700891def _identify_project(path):
892 """Identify the repo project associated with the given path.
893
894 Returns:
895 A string indicating what project is associated with the path passed in or
896 a blank string upon failure.
897 """
898 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
899 stderr=subprocess.PIPE, cwd=path).strip()
900
901
902def direct_main(args, verbose=False):
903 """Run hooks directly (outside of the context of repo).
904
905 # Setup for doctests below.
906 # ...note that some tests assume that running pre-upload on this CWD is fine.
907 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
908 >>> mydir = os.path.dirname(os.path.abspath(__file__))
909 >>> olddir = os.getcwd()
910
911 # OK to run w/ no arugments; will run with CWD.
912 >>> os.chdir(mydir)
913 >>> direct_main(['prog_name'], verbose=True)
914 Running hooks on chromiumos/repohooks
915 0
916 >>> os.chdir(olddir)
917
918 # Run specifying a dir
919 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
920 Running hooks on chromiumos/repohooks
921 0
922
923 # Not a problem to use a bogus project; we'll just get default settings.
924 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
925 Running hooks on X
926 0
927
928 # Run with project but no dir
929 >>> os.chdir(mydir)
930 >>> direct_main(['prog_name', '--project=X'], verbose=True)
931 Running hooks on X
932 0
933 >>> os.chdir(olddir)
934
935 # Try with a non-git CWD
936 >>> os.chdir('/tmp')
937 >>> direct_main(['prog_name'])
938 Traceback (most recent call last):
939 ...
940 BadInvocation: The current directory is not part of a git project.
941
942 # Check various bad arguments...
943 >>> direct_main(['prog_name', 'bogus'])
944 Traceback (most recent call last):
945 ...
946 BadInvocation: Unexpected arguments: bogus
947 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
948 Traceback (most recent call last):
949 ...
950 BadInvocation: Invalid dir: bogusdir
951 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
952 Traceback (most recent call last):
953 ...
954 BadInvocation: Not a git directory: /tmp
955
956 Args:
957 args: The value of sys.argv
958
959 Returns:
960 0 if no pre-upload failures, 1 if failures.
961
962 Raises:
963 BadInvocation: On some types of invocation errors.
964 """
965 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
966 parser = optparse.OptionParser(description=desc)
967
968 parser.add_option('--dir', default=None,
969 help='The directory that the project lives in. If not '
970 'specified, use the git project root based on the cwd.')
971 parser.add_option('--project', default=None,
972 help='The project repo path; this can affect how the hooks '
973 'get run, since some hooks are project-specific. For '
974 'chromite this is chromiumos/chromite. If not specified, '
975 'the repo tool will be used to figure this out based on '
976 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -0700977 parser.add_option('--rerun-since', default=None,
978 help='Rerun hooks on old commits since the given date. '
979 'The date should match git log\'s concept of a date. '
980 'e.g. 2012-06-20')
981
982 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -0700983
984 opts, args = parser.parse_args(args[1:])
985
Doug Anderson14749562013-06-26 13:38:29 -0700986 if opts.rerun_since:
987 if args:
988 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
989 ' '.join(args))
990
991 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
992 all_commits = _run_command(cmd).splitlines()
993 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
994
995 # Eliminate chrome-bot commits but keep ordering the same...
996 bot_commits = set(bot_commits)
997 args = [c for c in all_commits if c not in bot_commits]
998
Doug Anderson44a644f2011-11-02 10:37:37 -0700999
1000 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1001 # project from CWD
1002 if opts.dir is None:
1003 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1004 stderr=subprocess.PIPE).strip()
1005 if not git_dir:
1006 raise BadInvocation('The current directory is not part of a git project.')
1007 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1008 elif not os.path.isdir(opts.dir):
1009 raise BadInvocation('Invalid dir: %s' % opts.dir)
1010 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1011 raise BadInvocation('Not a git directory: %s' % opts.dir)
1012
1013 # Identify the project if it wasn't specified; this _requires_ the repo
1014 # tool to be installed and for the project to be part of a repo checkout.
1015 if not opts.project:
1016 opts.project = _identify_project(opts.dir)
1017 if not opts.project:
1018 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1019
1020 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001021 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001022
Doug Anderson14749562013-06-26 13:38:29 -07001023 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
1024 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -07001025 if found_error:
1026 return 1
1027 return 0
1028
1029
1030def _test():
1031 """Run any built-in tests."""
1032 import doctest
1033 doctest.testmod()
1034
1035
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001036if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001037 if sys.argv[1:2] == ["--test"]:
1038 _test()
1039 exit_code = 0
1040 else:
1041 prog_name = os.path.basename(sys.argv[0])
1042 try:
1043 exit_code = direct_main(sys.argv)
1044 except BadInvocation, e:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001045 print("%s: %s" % (prog_name, str(e)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001046 exit_code = 1
1047 sys.exit(exit_code)