blob: 9501d00183441e8a70a63c6ddb3a6fa8f37b918e [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
Ryan Cui9b651632011-05-11 11:38:58 -07006import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +08007import functools
Dale Curtis2975c432011-05-03 17:25:20 -07008import json
Doug Anderson44a644f2011-11-02 10:37:37 -07009import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070010import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070011import re
David Hendricks4c018e72013-02-06 13:46:38 -080012import socket
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070013import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070014import subprocess
15
Ryan Cui1562fb82011-05-09 11:01:31 -070016from errors import (VerifyException, HookFailure, PrintErrorForProject,
17 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070018
David Jamesc3b68b32013-04-03 09:17:03 -070019# If repo imports us, the __name__ will be __builtin__, and the wrapper will
20# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
21# up. The same logic also happens to work if we're executed directly.
22if __name__ in ('__builtin__', '__main__'):
23 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
24
25from chromite.lib import patch
26
Ryan Cuiec4d6332011-05-02 14:15:25 -070027
28COMMON_INCLUDED_PATHS = [
29 # C++ and friends
30 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
31 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
32 # Scripts
33 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
34 # No extension at all, note that ALL CAPS files are black listed in
35 # COMMON_EXCLUDED_LIST below.
David Hendricks0af30eb2013-02-05 11:35:56 -080036 r"(^|.*[\\\/])[^.]+$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070037 # Other
38 r".*\.java$", r".*\.mk$", r".*\.am$",
39]
40
Ryan Cui1562fb82011-05-09 11:01:31 -070041
Ryan Cuiec4d6332011-05-02 14:15:25 -070042COMMON_EXCLUDED_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -070043 # avoid doing source file checks for kernel
44 r"/src/third_party/kernel/",
45 r"/src/third_party/kernel-next/",
Paul Taysomf8b6e012011-05-09 14:32:42 -070046 r"/src/third_party/ktop/",
47 r"/src/third_party/punybench/",
Ryan Cuiec4d6332011-05-02 14:15:25 -070048 r".*\bexperimental[\\\/].*",
49 r".*\b[A-Z0-9_]{2,}$",
50 r".*[\\\/]debian[\\\/]rules$",
Brian Harringd780d602011-10-18 16:48:08 -070051 # for ebuild trees, ignore any caches and manifest data
52 r".*/Manifest$",
53 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070054
55 # ignore profiles data (like overlay-tegra2/profiles)
56 r".*/overlay-.*/profiles/.*",
Andrew de los Reyes0e679922012-05-02 11:42:54 -070057 # ignore minified js and jquery
58 r".*\.min\.js",
59 r".*jquery.*\.js",
Ryan Cuiec4d6332011-05-02 14:15:25 -070060]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070061
Ryan Cui1562fb82011-05-09 11:01:31 -070062
Ryan Cui9b651632011-05-11 11:38:58 -070063_CONFIG_FILE = 'PRESUBMIT.cfg'
64
65
Doug Anderson44a644f2011-11-02 10:37:37 -070066# Exceptions
67
68
69class BadInvocation(Exception):
70 """An Exception indicating a bad invocation of the program."""
71 pass
72
73
Ryan Cui1562fb82011-05-09 11:01:31 -070074# General Helpers
75
Sean Paulba01d402011-05-05 11:36:23 -040076
Doug Anderson44a644f2011-11-02 10:37:37 -070077def _run_command(cmd, cwd=None, stderr=None):
78 """Executes the passed in command and returns raw stdout output.
79
80 Args:
81 cmd: The command to run; should be a list of strings.
82 cwd: The directory to switch to for running the command.
83 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
84 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
85
86 Returns:
87 The standard out from the process.
88 """
89 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
90 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -070091
Ryan Cui1562fb82011-05-09 11:01:31 -070092
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070093def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -070094 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -070095 if __name__ == '__main__':
96 # Works when file is run on its own (__file__ is defined)...
97 return os.path.abspath(os.path.dirname(__file__))
98 else:
99 # We need to do this when we're run through repo. Since repo executes
100 # us with execfile(), we don't get __file__ defined.
101 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
102 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700103
Ryan Cui1562fb82011-05-09 11:01:31 -0700104
Ryan Cuiec4d6332011-05-02 14:15:25 -0700105def _match_regex_list(subject, expressions):
106 """Try to match a list of regular expressions to a string.
107
108 Args:
109 subject: The string to match regexes on
110 expressions: A list of regular expressions to check for matches with.
111
112 Returns:
113 Whether the passed in subject matches any of the passed in regexes.
114 """
115 for expr in expressions:
116 if (re.search(expr, subject)):
117 return True
118 return False
119
Ryan Cui1562fb82011-05-09 11:01:31 -0700120
Ryan Cuiec4d6332011-05-02 14:15:25 -0700121def _filter_files(files, include_list, exclude_list=[]):
122 """Filter out files based on the conditions passed in.
123
124 Args:
125 files: list of filepaths to filter
126 include_list: list of regex that when matched with a file path will cause it
127 to be added to the output list unless the file is also matched with a
128 regex in the exclude_list.
129 exclude_list: list of regex that when matched with a file will prevent it
130 from being added to the output list, even if it is also matched with a
131 regex in the include_list.
132
133 Returns:
134 A list of filepaths that contain files matched in the include_list and not
135 in the exclude_list.
136 """
137 filtered = []
138 for f in files:
139 if (_match_regex_list(f, include_list) and
140 not _match_regex_list(f, exclude_list)):
141 filtered.append(f)
142 return filtered
143
Ryan Cuiec4d6332011-05-02 14:15:25 -0700144
David Hendricks35030d02013-02-04 17:49:16 -0800145def _verify_header_content(commit, content, fail_msg):
146 """Verify that file headers contain specified content.
147
148 Args:
149 commit: the affected commit.
150 content: the content of the header to be verified.
151 fail_msg: the first message to display in case of failure.
152
153 Returns:
154 The return value of HookFailure().
155 """
156 license_re = re.compile(content, re.MULTILINE)
157 bad_files = []
158 files = _filter_files(_get_affected_files(commit),
159 COMMON_INCLUDED_PATHS,
160 COMMON_EXCLUDED_PATHS)
161
162 for f in files:
Gabe Blackcf3c32c2013-02-27 00:26:13 -0800163 if os.path.exists(f): # Ignore non-existant files
164 contents = open(f).read()
165 if len(contents) == 0: continue # Ignore empty files
166 if not license_re.search(contents):
167 bad_files.append(f)
David Hendricks35030d02013-02-04 17:49:16 -0800168 if bad_files:
169 msg = "%s:\n%s\n%s" % (fail_msg, license_re.pattern,
170 "Found a bad header in these files:")
171 return HookFailure(msg, bad_files)
172
173
Ryan Cuiec4d6332011-05-02 14:15:25 -0700174# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700175
176
Ryan Cui4725d952011-05-05 15:41:19 -0700177def _get_upstream_branch():
178 """Returns the upstream tracking branch of the current branch.
179
180 Raises:
181 Error if there is no tracking branch
182 """
183 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
184 current_branch = current_branch.replace('refs/heads/', '')
185 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700186 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700187
188 cfg_option = 'branch.' + current_branch + '.%s'
189 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
190 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
191 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700192 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700193
194 return full_upstream.replace('heads', 'remotes/' + remote)
195
Ryan Cui1562fb82011-05-09 11:01:31 -0700196
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700197def _get_patch(commit):
198 """Returns the patch for this commit."""
199 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700200
Ryan Cui1562fb82011-05-09 11:01:31 -0700201
Jon Salz98255932012-08-18 14:48:02 +0800202def _try_utf8_decode(data):
203 """Attempts to decode a string as UTF-8.
204
205 Returns:
206 The decoded Unicode object, or the original string if parsing fails.
207 """
208 try:
209 return unicode(data, 'utf-8', 'strict')
210 except UnicodeDecodeError:
211 return data
212
213
Ryan Cuiec4d6332011-05-02 14:15:25 -0700214def _get_file_diff(file, commit):
215 """Returns a list of (linenum, lines) tuples that the commit touched."""
Ryan Cui72834d12011-05-05 14:51:33 -0700216 output = _run_command(['git', 'show', '-p', '--no-ext-diff', commit, file])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700217
218 new_lines = []
219 line_num = 0
220 for line in output.splitlines():
221 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
222 if m:
223 line_num = int(m.groups(1)[0])
224 continue
225 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800226 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700227 if not line.startswith('-'):
228 line_num += 1
229 return new_lines
230
Ryan Cui1562fb82011-05-09 11:01:31 -0700231
Doug Anderson42b8a052013-06-26 10:45:36 -0700232def _get_affected_files(commit, include_deletes=False):
233 """Returns list of absolute filepaths that were modified/added.
234
235 Args:
236 commit: The commit
237 include_deletes: If true we'll include delete in the list.
238
239 Returns:
240 A list of modified/added (and perhaps deleted) files
241 """
Ryan Cui72834d12011-05-05 14:51:33 -0700242 output = _run_command(['git', 'diff', '--name-status', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700243 files = []
244 for statusline in output.splitlines():
245 m = re.match('^(\w)+\t(.+)$', statusline.rstrip())
246 # Ignore deleted files, and return absolute paths of files
Doug Anderson42b8a052013-06-26 10:45:36 -0700247 if (include_deletes or m.group(1)[0] != 'D'):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700248 pwd = os.getcwd()
249 files.append(os.path.join(pwd, m.group(2)))
250 return files
251
Ryan Cui1562fb82011-05-09 11:01:31 -0700252
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700253def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700254 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700255 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700256 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700257
Ryan Cui1562fb82011-05-09 11:01:31 -0700258
Ryan Cuiec4d6332011-05-02 14:15:25 -0700259def _get_commit_desc(commit):
260 """Returns the full commit message of a commit."""
Sean Paul23a2c582011-05-06 13:10:44 -0400261 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700262
263
264# Common Hooks
265
Ryan Cui1562fb82011-05-09 11:01:31 -0700266
Ryan Cuiec4d6332011-05-02 14:15:25 -0700267def _check_no_long_lines(project, commit):
268 """Checks that there aren't any lines longer than maxlen characters in any of
269 the text files to be submitted.
270 """
271 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800272 SKIP_REGEXP = re.compile('|'.join([
273 r'https?://',
274 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700275
276 errors = []
277 files = _filter_files(_get_affected_files(commit),
278 COMMON_INCLUDED_PATHS,
279 COMMON_EXCLUDED_PATHS)
280
281 for afile in files:
282 for line_num, line in _get_file_diff(afile, commit):
283 # Allow certain lines to exceed the maxlen rule.
Jon Salz98255932012-08-18 14:48:02 +0800284 if (len(line) <= MAX_LEN or SKIP_REGEXP.search(line)):
285 continue
286
287 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
288 if len(errors) == 5: # Just show the first 5 errors.
289 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700290
291 if errors:
292 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700293 return HookFailure(msg, errors)
294
Ryan Cuiec4d6332011-05-02 14:15:25 -0700295
296def _check_no_stray_whitespace(project, commit):
297 """Checks that there is no stray whitespace at source lines end."""
298 errors = []
299 files = _filter_files(_get_affected_files(commit),
300 COMMON_INCLUDED_PATHS,
301 COMMON_EXCLUDED_PATHS)
302
303 for afile in files:
304 for line_num, line in _get_file_diff(afile, commit):
305 if line.rstrip() != line:
306 errors.append('%s, line %s' % (afile, line_num))
307 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700308 return HookFailure('Found line ending with white space in:', errors)
309
Ryan Cuiec4d6332011-05-02 14:15:25 -0700310
311def _check_no_tabs(project, commit):
312 """Checks there are no unexpanded tabs."""
313 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700314 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700315 r".*\.ebuild$",
316 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500317 r".*/[M|m]akefile$",
318 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700319 ]
320
321 errors = []
322 files = _filter_files(_get_affected_files(commit),
323 COMMON_INCLUDED_PATHS,
324 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
325
326 for afile in files:
327 for line_num, line in _get_file_diff(afile, commit):
328 if '\t' in line:
329 errors.append('%s, line %s' % (afile, line_num))
330 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700331 return HookFailure('Found a tab character in:', errors)
332
Ryan Cuiec4d6332011-05-02 14:15:25 -0700333
334def _check_change_has_test_field(project, commit):
335 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700336 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700337
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700338 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700339 msg = 'Changelist description needs TEST field (after first line)'
340 return HookFailure(msg)
341
Ryan Cuiec4d6332011-05-02 14:15:25 -0700342
David Jamesc3b68b32013-04-03 09:17:03 -0700343def _check_change_has_valid_cq_depend(project, commit):
344 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
345 msg = 'Changelist has invalid CQ-DEPEND target.'
346 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
347 try:
348 patch.GetPaladinDeps(_get_commit_desc(commit))
349 except ValueError as ex:
350 return HookFailure(msg, [example, str(ex)])
351
352
Ryan Cuiec4d6332011-05-02 14:15:25 -0700353def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700354 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700355 OLD_BUG_RE = r'\nBUG=.*chromium-os'
356 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
357 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
358 'the chromium tracker in your BUG= line now.')
359 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700360
David James5c0073d2013-04-03 08:48:52 -0700361 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700362 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700363 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700364 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700365 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
366 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700367 return HookFailure(msg)
368
Ryan Cuiec4d6332011-05-02 14:15:25 -0700369
Doug Anderson42b8a052013-06-26 10:45:36 -0700370def _check_for_uprev(project, commit):
371 """Check that we're not missing a revbump of an ebuild in the given commit.
372
373 If the given commit touches files in a directory that has ebuilds somewhere
374 up the directory hierarchy, it's very likely that we need an ebuild revbump
375 in order for those changes to take effect.
376
377 It's not totally trivial to detect a revbump, so at least detect that an
378 ebuild with a revision number in it was touched. This should handle the
379 common case where we use a symlink to do the revbump.
380
381 TODO: it would be nice to enhance this hook to:
382 * Handle cases where people revbump with a slightly different syntax. I see
383 one ebuild (puppy) that revbumps with _pN. This is a false positive.
384 * Catches cases where people aren't using symlinks for revbumps. If they
385 edit a revisioned file directly (and are expected to rename it for revbump)
386 we'll miss that. Perhaps we could detect that the file touched is a
387 symlink?
388
389 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
390 still better off than without this check.
391
392 Args:
393 project: The project to look at
394 commit: The commit to look at
395
396 Returns:
397 A HookFailure or None.
398 """
399 affected_paths = _get_affected_files(commit, include_deletes=True)
400
401 # Don't yell about changes to whitelisted files...
402 whitelist = ['Manifest', 'metadata.xml']
403 affected_paths = [path for path in affected_paths
404 if os.path.basename(path) not in whitelist]
405 if not affected_paths:
406 return None
407
408 # If we've touched any file named with a -rN.ebuild then we'll say we're
409 # OK right away. See TODO above about enhancing this.
410 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
411 for path in affected_paths)
412 if touched_revved_ebuild:
413 return None
414
415 # We want to examine the current contents of all directories that are parents
416 # of files that were touched (up to the top of the project).
417 #
418 # ...note: we use the current directory contents even though it may have
419 # changed since the commit we're looking at. This is just a heuristic after
420 # all. Worst case we don't flag a missing revbump.
421 project_top = os.getcwd()
422 dirs_to_check = set([project_top])
423 for path in affected_paths:
424 path = os.path.dirname(path)
425 while os.path.exists(path) and not os.path.samefile(path, project_top):
426 dirs_to_check.add(path)
427 path = os.path.dirname(path)
428
429 # Look through each directory. If it's got an ebuild in it then we'll
430 # consider this as a case when we need a revbump.
431 for dir_path in dirs_to_check:
432 contents = os.listdir(dir_path)
433 ebuilds = [os.path.join(dir_path, path)
434 for path in contents if path.endswith('.ebuild')]
435 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
436
437 # If the -9999.ebuild file was touched the bot will uprev for us.
438 # ...we'll use a simple intersection here as a heuristic...
439 if set(ebuilds_9999) & set(affected_paths):
440 continue
441
442 if ebuilds:
443 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
444 'or a -r1.ebuild symlink if this is a new ebuild')
445
446 return None
447
448
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700449def _check_change_has_proper_changeid(project, commit):
450 """Verify that Change-ID is present in last paragraph of commit message."""
451 desc = _get_commit_desc(commit)
452 loc = desc.rfind('\nChange-Id:')
453 if loc == -1 or re.search('\n\s*\n\s*\S+', desc[loc:]):
Ryan Cui1562fb82011-05-09 11:01:31 -0700454 return HookFailure('Change-Id must be in last paragraph of description.')
455
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700456
Ryan Cuiec4d6332011-05-02 14:15:25 -0700457def _check_license(project, commit):
458 """Verifies the license header."""
459 LICENSE_HEADER = (
David Hendricks0af30eb2013-02-05 11:35:56 -0800460 r".* Copyright \(c\) 20[-0-9]{2,7} The Chromium OS Authors\. All rights "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700461 r"reserved\." "\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800462 r".* Use of this source code is governed by a BSD-style license that can "
Ryan Cuiec4d6332011-05-02 14:15:25 -0700463 "be\n"
David Hendricks0af30eb2013-02-05 11:35:56 -0800464 r".* found in the LICENSE file\."
Ryan Cuiec4d6332011-05-02 14:15:25 -0700465 "\n"
466 )
David Hendricks35030d02013-02-04 17:49:16 -0800467 FAIL_MSG = "License must match"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700468
David Hendricks35030d02013-02-04 17:49:16 -0800469 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700470
471
David Hendricksa0e310d2013-02-04 17:51:55 -0800472def _check_google_copyright(project, commit):
473 """Verifies Google Inc. as copyright holder."""
474 LICENSE_HEADER = (
475 r".* Copyright 20[-0-9]{2,7} Google Inc\."
476 )
477 FAIL_MSG = "Copyright must match"
478
David Hendricks4c018e72013-02-06 13:46:38 -0800479 # Avoid blocking partners and external contributors.
480 fqdn = socket.getfqdn()
481 if not fqdn.endswith(".corp.google.com"):
482 return None
483
David Hendricksa0e310d2013-02-04 17:51:55 -0800484 return _verify_header_content(commit, LICENSE_HEADER, FAIL_MSG)
485
486
Ryan Cuiec4d6332011-05-02 14:15:25 -0700487# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700488
Ryan Cui1562fb82011-05-09 11:01:31 -0700489
Anton Staaf815d6852011-08-22 10:08:45 -0700490def _run_checkpatch(project, commit, options=[]):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700491 """Runs checkpatch.pl on the given project"""
492 hooks_dir = _get_hooks_dir()
Anton Staaf815d6852011-08-22 10:08:45 -0700493 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700494 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700495 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700496 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700497 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700498
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700499
Anton Staaf815d6852011-08-22 10:08:45 -0700500def _run_checkpatch_no_tree(project, commit):
501 return _run_checkpatch(project, commit, ['--no-tree'])
502
Olof Johanssona96810f2012-09-04 16:20:03 -0700503def _kernel_configcheck(project, commit):
504 """Makes sure kernel config changes are not mixed with code changes"""
505 files = _get_affected_files(commit)
506 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
507 return HookFailure('Changes to chromeos/config/ and regular files must '
508 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700509
Dale Curtis2975c432011-05-03 17:25:20 -0700510def _run_json_check(project, commit):
511 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700512 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700513 try:
514 json.load(open(f))
515 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700516 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700517
518
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700519def _check_change_has_branch_field(project, commit):
520 """Check for a non-empty 'BRANCH=' field in the commit message."""
521 BRANCH_RE = r'\nBRANCH=\S+'
522
523 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
524 msg = ('Changelist description needs BRANCH field (after first line)\n'
525 'E.g. BRANCH=none or BRANCH=link,snow')
526 return HookFailure(msg)
527
528
Jon Salz3ee59de2012-08-18 13:54:22 +0800529def _run_project_hook_script(script, project, commit):
530 """Runs a project hook script.
531
532 The script is run with the following environment variables set:
533 PRESUBMIT_PROJECT: The affected project
534 PRESUBMIT_COMMIT: The affected commit
535 PRESUBMIT_FILES: A newline-separated list of affected files
536
537 The script is considered to fail if the exit code is non-zero. It should
538 write an error message to stdout.
539 """
540 env = dict(os.environ)
541 env['PRESUBMIT_PROJECT'] = project
542 env['PRESUBMIT_COMMIT'] = commit
543
544 # Put affected files in an environment variable
545 files = _get_affected_files(commit)
546 env['PRESUBMIT_FILES'] = '\n'.join(files)
547
548 process = subprocess.Popen(script, env=env, shell=True,
549 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800550 stdout=subprocess.PIPE,
551 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800552 stdout, _ = process.communicate()
553 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800554 if stdout:
555 stdout = re.sub('(?m)^', ' ', stdout)
556 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800557 (script, process.returncode,
558 ':\n' + stdout if stdout else ''))
559
560
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700561# Base
562
Ryan Cui1562fb82011-05-09 11:01:31 -0700563
Ryan Cui9b651632011-05-11 11:38:58 -0700564# A list of hooks that are not project-specific
565_COMMON_HOOKS = [
566 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -0700567 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -0700568 _check_change_has_test_field,
569 _check_change_has_proper_changeid,
570 _check_no_stray_whitespace,
571 _check_no_long_lines,
572 _check_license,
573 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -0700574 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -0700575]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700576
Ryan Cui1562fb82011-05-09 11:01:31 -0700577
Ryan Cui9b651632011-05-11 11:38:58 -0700578# A dictionary of project-specific hooks(callbacks), indexed by project name.
579# dict[project] = [callback1, callback2]
580_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -0400581 "chromeos/autotest-tools": [_run_json_check],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700582 "chromeos/platform/ec-private": [_run_checkpatch_no_tree,
583 _check_change_has_branch_field],
David Hendricks33f77d52013-02-04 17:53:02 -0800584 "chromeos/third_party/coreboot": [_check_change_has_branch_field,
585 _check_google_copyright],
Puneet Kumar57b9c092012-08-14 18:58:29 -0700586 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400587 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
588 _kernel_configcheck],
589 "chromeos/vendor/u-boot-exynos": [_run_checkpatch_no_tree],
590 "chromiumos/platform/ec": [_run_checkpatch_no_tree,
591 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700592 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400593 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700594 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -0400595 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
596 "chromiumos/third_party/kernel-next": [_run_checkpatch,
597 _kernel_configcheck],
598 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree,
599 _check_change_has_branch_field],
Ryan Cui9b651632011-05-11 11:38:58 -0700600}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700601
Ryan Cui1562fb82011-05-09 11:01:31 -0700602
Ryan Cui9b651632011-05-11 11:38:58 -0700603# A dictionary of flags (keys) that can appear in the config file, and the hook
604# that the flag disables (value)
605_DISABLE_FLAGS = {
606 'stray_whitespace_check': _check_no_stray_whitespace,
607 'long_line_check': _check_no_long_lines,
608 'cros_license_check': _check_license,
609 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700610 'branch_check': _check_change_has_branch_field,
Ryan Cui9b651632011-05-11 11:38:58 -0700611}
612
613
Jon Salz3ee59de2012-08-18 13:54:22 +0800614def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -0700615 """Returns a set of hooks disabled by the current project's config file.
616
617 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +0800618
619 Args:
620 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -0700621 """
622 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +0800623 if not config.has_section(SECTION):
624 return set()
Ryan Cui9b651632011-05-11 11:38:58 -0700625
626 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +0800627 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -0700628 try:
629 if not config.getboolean(SECTION, flag): disable_flags.append(flag)
630 except ValueError as e:
631 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
632 print msg + str(e)
633
634 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
635 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
636
637
Jon Salz3ee59de2012-08-18 13:54:22 +0800638def _get_project_hook_scripts(config):
639 """Returns a list of project-specific hook scripts.
640
641 Args:
642 config: A ConfigParser for the project's config file.
643 """
644 SECTION = 'Hook Scripts'
645 if not config.has_section(SECTION):
646 return []
647
648 hook_names_values = config.items(SECTION)
649 hook_names_values.sort(key=lambda x: x[0])
650 return [x[1] for x in hook_names_values]
651
652
Ryan Cui9b651632011-05-11 11:38:58 -0700653def _get_project_hooks(project):
654 """Returns a list of hooks that need to be run for a project.
655
656 Expects to be called from within the project root.
657 """
Jon Salz3ee59de2012-08-18 13:54:22 +0800658 config = ConfigParser.RawConfigParser()
659 try:
660 config.read(_CONFIG_FILE)
661 except ConfigParser.Error:
662 # Just use an empty config file
663 config = ConfigParser.RawConfigParser()
664
665 disabled_hooks = _get_disabled_hooks(config)
Ryan Cui9b651632011-05-11 11:38:58 -0700666 hooks = [hook for hook in _COMMON_HOOKS if hook not in disabled_hooks]
667
668 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700669 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
670 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -0700671
Jon Salz3ee59de2012-08-18 13:54:22 +0800672 for script in _get_project_hook_scripts(config):
673 hooks.append(functools.partial(_run_project_hook_script, script))
674
Ryan Cui9b651632011-05-11 11:38:58 -0700675 return hooks
676
677
Doug Anderson14749562013-06-26 13:38:29 -0700678def _run_project_hooks(project, proj_dir=None, commit_list=None):
Ryan Cui1562fb82011-05-09 11:01:31 -0700679 """For each project run its project specific hook from the hooks dictionary.
680
681 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -0700682 project: The name of project to run hooks for.
683 proj_dir: If non-None, this is the directory the project is in. If None,
684 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -0700685 commit_list: A list of commits to run hooks against. If None or empty list
686 then we'll automatically get the list of commits that would be uploaded.
Ryan Cui1562fb82011-05-09 11:01:31 -0700687
688 Returns:
689 Boolean value of whether any errors were ecountered while running the hooks.
690 """
Doug Anderson44a644f2011-11-02 10:37:37 -0700691 if proj_dir is None:
692 proj_dir = _run_command(['repo', 'forall', project, '-c', 'pwd']).strip()
693
Ryan Cuiec4d6332011-05-02 14:15:25 -0700694 pwd = os.getcwd()
695 # hooks assume they are run from the root of the project
696 os.chdir(proj_dir)
697
Doug Anderson14749562013-06-26 13:38:29 -0700698 if not commit_list:
699 try:
700 commit_list = _get_commits()
701 except VerifyException as e:
702 PrintErrorForProject(project, HookFailure(str(e)))
703 os.chdir(pwd)
704 return True
Ryan Cuifa55df52011-05-06 11:16:55 -0700705
Ryan Cui9b651632011-05-11 11:38:58 -0700706 hooks = _get_project_hooks(project)
Ryan Cui1562fb82011-05-09 11:01:31 -0700707 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -0700708 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -0700709 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -0700710 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -0700711 hook_error = hook(project, commit)
712 if hook_error:
713 error_list.append(hook_error)
714 error_found = True
715 if error_list:
716 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
717 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -0700718
Ryan Cuiec4d6332011-05-02 14:15:25 -0700719 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -0700720 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700721
722# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700723
Ryan Cui1562fb82011-05-09 11:01:31 -0700724
Anush Elangovan63afad72011-03-23 00:41:27 -0700725def main(project_list, **kwargs):
Doug Anderson06456632012-01-05 11:02:14 -0800726 """Main function invoked directly by repo.
727
728 This function will exit directly upon error so that repo doesn't print some
729 obscure error message.
730
731 Args:
732 project_list: List of projects to run on.
733 kwargs: Leave this here for forward-compatibility.
734 """
Ryan Cui1562fb82011-05-09 11:01:31 -0700735 found_error = False
736 for project in project_list:
Ryan Cui9b651632011-05-11 11:38:58 -0700737 if _run_project_hooks(project):
Ryan Cui1562fb82011-05-09 11:01:31 -0700738 found_error = True
739
740 if (found_error):
741 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -0700742 '- To disable some source style checks, and for other hints, see '
743 '<checkout_dir>/src/repohooks/README\n'
744 '- To upload only current project, run \'repo upload .\'')
Ryan Cui1562fb82011-05-09 11:01:31 -0700745 print >> sys.stderr, msg
Don Garrettdba548a2011-05-05 15:17:14 -0700746 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -0700747
Ryan Cui1562fb82011-05-09 11:01:31 -0700748
Doug Anderson44a644f2011-11-02 10:37:37 -0700749def _identify_project(path):
750 """Identify the repo project associated with the given path.
751
752 Returns:
753 A string indicating what project is associated with the path passed in or
754 a blank string upon failure.
755 """
756 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
757 stderr=subprocess.PIPE, cwd=path).strip()
758
759
760def direct_main(args, verbose=False):
761 """Run hooks directly (outside of the context of repo).
762
763 # Setup for doctests below.
764 # ...note that some tests assume that running pre-upload on this CWD is fine.
765 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
766 >>> mydir = os.path.dirname(os.path.abspath(__file__))
767 >>> olddir = os.getcwd()
768
769 # OK to run w/ no arugments; will run with CWD.
770 >>> os.chdir(mydir)
771 >>> direct_main(['prog_name'], verbose=True)
772 Running hooks on chromiumos/repohooks
773 0
774 >>> os.chdir(olddir)
775
776 # Run specifying a dir
777 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
778 Running hooks on chromiumos/repohooks
779 0
780
781 # Not a problem to use a bogus project; we'll just get default settings.
782 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
783 Running hooks on X
784 0
785
786 # Run with project but no dir
787 >>> os.chdir(mydir)
788 >>> direct_main(['prog_name', '--project=X'], verbose=True)
789 Running hooks on X
790 0
791 >>> os.chdir(olddir)
792
793 # Try with a non-git CWD
794 >>> os.chdir('/tmp')
795 >>> direct_main(['prog_name'])
796 Traceback (most recent call last):
797 ...
798 BadInvocation: The current directory is not part of a git project.
799
800 # Check various bad arguments...
801 >>> direct_main(['prog_name', 'bogus'])
802 Traceback (most recent call last):
803 ...
804 BadInvocation: Unexpected arguments: bogus
805 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
806 Traceback (most recent call last):
807 ...
808 BadInvocation: Invalid dir: bogusdir
809 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
810 Traceback (most recent call last):
811 ...
812 BadInvocation: Not a git directory: /tmp
813
814 Args:
815 args: The value of sys.argv
816
817 Returns:
818 0 if no pre-upload failures, 1 if failures.
819
820 Raises:
821 BadInvocation: On some types of invocation errors.
822 """
823 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
824 parser = optparse.OptionParser(description=desc)
825
826 parser.add_option('--dir', default=None,
827 help='The directory that the project lives in. If not '
828 'specified, use the git project root based on the cwd.')
829 parser.add_option('--project', default=None,
830 help='The project repo path; this can affect how the hooks '
831 'get run, since some hooks are project-specific. For '
832 'chromite this is chromiumos/chromite. If not specified, '
833 'the repo tool will be used to figure this out based on '
834 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -0700835 parser.add_option('--rerun-since', default=None,
836 help='Rerun hooks on old commits since the given date. '
837 'The date should match git log\'s concept of a date. '
838 'e.g. 2012-06-20')
839
840 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -0700841
842 opts, args = parser.parse_args(args[1:])
843
Doug Anderson14749562013-06-26 13:38:29 -0700844 if opts.rerun_since:
845 if args:
846 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
847 ' '.join(args))
848
849 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
850 all_commits = _run_command(cmd).splitlines()
851 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
852
853 # Eliminate chrome-bot commits but keep ordering the same...
854 bot_commits = set(bot_commits)
855 args = [c for c in all_commits if c not in bot_commits]
856
Doug Anderson44a644f2011-11-02 10:37:37 -0700857
858 # Check/normlaize git dir; if unspecified, we'll use the root of the git
859 # project from CWD
860 if opts.dir is None:
861 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
862 stderr=subprocess.PIPE).strip()
863 if not git_dir:
864 raise BadInvocation('The current directory is not part of a git project.')
865 opts.dir = os.path.dirname(os.path.abspath(git_dir))
866 elif not os.path.isdir(opts.dir):
867 raise BadInvocation('Invalid dir: %s' % opts.dir)
868 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
869 raise BadInvocation('Not a git directory: %s' % opts.dir)
870
871 # Identify the project if it wasn't specified; this _requires_ the repo
872 # tool to be installed and for the project to be part of a repo checkout.
873 if not opts.project:
874 opts.project = _identify_project(opts.dir)
875 if not opts.project:
876 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
877
878 if verbose:
879 print "Running hooks on %s" % (opts.project)
880
Doug Anderson14749562013-06-26 13:38:29 -0700881 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
882 commit_list=args)
Doug Anderson44a644f2011-11-02 10:37:37 -0700883 if found_error:
884 return 1
885 return 0
886
887
888def _test():
889 """Run any built-in tests."""
890 import doctest
891 doctest.testmod()
892
893
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -0700894if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -0700895 if sys.argv[1:2] == ["--test"]:
896 _test()
897 exit_code = 0
898 else:
899 prog_name = os.path.basename(sys.argv[0])
900 try:
901 exit_code = direct_main(sys.argv)
902 except BadInvocation, e:
903 print >>sys.stderr, "%s: %s" % (prog_name, str(e))
904 exit_code = 1
905 sys.exit(exit_code)