blob: fa964fbc3f34d6438ba7c49ed13f8fb8959fff26 [file] [log] [blame]
Doug Anderson44a644f2011-11-02 10:37:37 -07001#!/usr/bin/env python
Jon Salz98255932012-08-18 14:48:02 +08002# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Mike Frysingerae409522014-02-01 03:16:11 -05006"""Presubmit checks to run when doing `repo upload`.
7
8You can add new checks by adding a functions to the HOOKS constants.
9"""
10
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040011from __future__ import print_function
12
Ryan Cui9b651632011-05-11 11:38:58 -070013import ConfigParser
Jon Salz3ee59de2012-08-18 13:54:22 +080014import functools
Dale Curtis2975c432011-05-03 17:25:20 -070015import json
Doug Anderson44a644f2011-11-02 10:37:37 -070016import optparse
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070017import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070018import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070019import sys
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070020import subprocess
Peter Ammon811f6702014-06-12 15:45:38 -070021import stat
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070022
Ryan Cui1562fb82011-05-09 11:01:31 -070023from errors import (VerifyException, HookFailure, PrintErrorForProject,
24 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070025
David Jamesc3b68b32013-04-03 09:17:03 -070026# If repo imports us, the __name__ will be __builtin__, and the wrapper will
27# be in $CHROMEOS_CHECKOUT/.repo/repo/main.py, so we need to go two directories
28# up. The same logic also happens to work if we're executed directly.
29if __name__ in ('__builtin__', '__main__'):
30 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
31
Daniel Erata350fd32014-09-29 14:02:34 -070032from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070033from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040034from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070035
Vadim Bendebury2b62d742014-06-22 13:14:51 -070036PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070037
38COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050039 # C++ and friends
40 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
41 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
42 # Scripts
43 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
44 # No extension at all, note that ALL CAPS files are black listed in
45 # COMMON_EXCLUDED_LIST below.
46 r"(^|.*[\\\/])[^.]+$",
47 # Other
48 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070049]
50
Ryan Cui1562fb82011-05-09 11:01:31 -070051
Ryan Cuiec4d6332011-05-02 14:15:25 -070052COMMON_EXCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050053 # avoid doing source file checks for kernel
54 r"/src/third_party/kernel/",
55 r"/src/third_party/kernel-next/",
56 r"/src/third_party/ktop/",
57 r"/src/third_party/punybench/",
58 r".*\bexperimental[\\\/].*",
59 r".*\b[A-Z0-9_]{2,}$",
60 r".*[\\\/]debian[\\\/]rules$",
61 # for ebuild trees, ignore any caches and manifest data
62 r".*/Manifest$",
63 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070064
Mike Frysingerae409522014-02-01 03:16:11 -050065 # ignore profiles data (like overlay-tegra2/profiles)
Mike Frysinger94a670c2014-09-19 12:46:26 -040066 r"(^|.*/)overlay-.*/profiles/.*",
Mike Frysinger98638102014-08-28 00:15:08 -040067 r"^profiles/.*$",
68
Mike Frysingerae409522014-02-01 03:16:11 -050069 # ignore minified js and jquery
70 r".*\.min\.js",
71 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050072
73 # Ignore license files as the content is often taken verbatim.
74 r'.*/licenses/.*',
Ryan Cuiec4d6332011-05-02 14:15:25 -070075]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070076
Ryan Cui1562fb82011-05-09 11:01:31 -070077
Ryan Cui9b651632011-05-11 11:38:58 -070078_CONFIG_FILE = 'PRESUBMIT.cfg'
79
80
Doug Anderson44a644f2011-11-02 10:37:37 -070081# Exceptions
82
83
84class BadInvocation(Exception):
85 """An Exception indicating a bad invocation of the program."""
86 pass
87
88
Ryan Cui1562fb82011-05-09 11:01:31 -070089# General Helpers
90
Sean Paulba01d402011-05-05 11:36:23 -040091
Doug Anderson44a644f2011-11-02 10:37:37 -070092def _run_command(cmd, cwd=None, stderr=None):
93 """Executes the passed in command and returns raw stdout output.
94
95 Args:
96 cmd: The command to run; should be a list of strings.
97 cwd: The directory to switch to for running the command.
98 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
99 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
100
101 Returns:
102 The standard out from the process.
103 """
104 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
105 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700106
Ryan Cui1562fb82011-05-09 11:01:31 -0700107
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700108def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700109 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700110 if __name__ == '__main__':
111 # Works when file is run on its own (__file__ is defined)...
112 return os.path.abspath(os.path.dirname(__file__))
113 else:
114 # We need to do this when we're run through repo. Since repo executes
115 # us with execfile(), we don't get __file__ defined.
116 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
117 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700118
Ryan Cui1562fb82011-05-09 11:01:31 -0700119
Ryan Cuiec4d6332011-05-02 14:15:25 -0700120def _match_regex_list(subject, expressions):
121 """Try to match a list of regular expressions to a string.
122
123 Args:
124 subject: The string to match regexes on
125 expressions: A list of regular expressions to check for matches with.
126
127 Returns:
128 Whether the passed in subject matches any of the passed in regexes.
129 """
130 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500131 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700132 return True
133 return False
134
Ryan Cui1562fb82011-05-09 11:01:31 -0700135
Mike Frysingerae409522014-02-01 03:16:11 -0500136def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700137 """Filter out files based on the conditions passed in.
138
139 Args:
140 files: list of filepaths to filter
141 include_list: list of regex that when matched with a file path will cause it
142 to be added to the output list unless the file is also matched with a
143 regex in the exclude_list.
144 exclude_list: list of regex that when matched with a file will prevent it
145 from being added to the output list, even if it is also matched with a
146 regex in the include_list.
147
148 Returns:
149 A list of filepaths that contain files matched in the include_list and not
150 in the exclude_list.
151 """
152 filtered = []
153 for f in files:
154 if (_match_regex_list(f, include_list) and
155 not _match_regex_list(f, exclude_list)):
156 filtered.append(f)
157 return filtered
158
Ryan Cuiec4d6332011-05-02 14:15:25 -0700159
160# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700161
162
Ryan Cui4725d952011-05-05 15:41:19 -0700163def _get_upstream_branch():
164 """Returns the upstream tracking branch of the current branch.
165
166 Raises:
167 Error if there is no tracking branch
168 """
169 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
170 current_branch = current_branch.replace('refs/heads/', '')
171 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700172 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700173
174 cfg_option = 'branch.' + current_branch + '.%s'
175 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
176 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
177 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700178 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700179
180 return full_upstream.replace('heads', 'remotes/' + remote)
181
Ryan Cui1562fb82011-05-09 11:01:31 -0700182
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700183def _get_patch(commit):
184 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700185 if commit == PRE_SUBMIT:
186 return _run_command(['git', 'diff', '--cached', 'HEAD'])
187 else:
188 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700189
Ryan Cui1562fb82011-05-09 11:01:31 -0700190
Jon Salz98255932012-08-18 14:48:02 +0800191def _try_utf8_decode(data):
192 """Attempts to decode a string as UTF-8.
193
194 Returns:
195 The decoded Unicode object, or the original string if parsing fails.
196 """
197 try:
198 return unicode(data, 'utf-8', 'strict')
199 except UnicodeDecodeError:
200 return data
201
202
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500203def _get_file_content(path, commit):
204 """Returns the content of a file at a specific commit.
205
206 We can't rely on the file as it exists in the filesystem as people might be
207 uploading a series of changes which modifies the file multiple times.
208
209 Note: The "content" of a symlink is just the target. So if you're expecting
210 a full file, you should check that first. One way to detect is that the
211 content will not have any newlines.
212 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700213 if commit == PRE_SUBMIT:
214 return _run_command(['git', 'diff', 'HEAD', path])
215 else:
216 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500217
218
Mike Frysingerae409522014-02-01 03:16:11 -0500219def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700220 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700221 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff']
222 if commit == PRE_SUBMIT:
223 command += ['HEAD', path]
224 else:
225 command += [commit, path]
226 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700227
228 new_lines = []
229 line_num = 0
230 for line in output.splitlines():
231 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
232 if m:
233 line_num = int(m.groups(1)[0])
234 continue
235 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800236 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700237 if not line.startswith('-'):
238 line_num += 1
239 return new_lines
240
Ryan Cui1562fb82011-05-09 11:01:31 -0700241
Peter Ammon811f6702014-06-12 15:45:38 -0700242def _parse_affected_files(output, include_deletes=False, relative=False):
243 """Parses git diff's 'raw' format, returning a list of modified file paths.
244
245 This excludes directories and symlinks, and optionally includes files that
246 were deleted.
Doug Anderson42b8a052013-06-26 10:45:36 -0700247
248 Args:
Peter Ammon811f6702014-06-12 15:45:38 -0700249 output: The result of the 'git diff --raw' command
250 include_deletes: If true, we'll include deleted files in the result
251 relative: Whether to return relative or full paths to files
Doug Anderson42b8a052013-06-26 10:45:36 -0700252
253 Returns:
254 A list of modified/added (and perhaps deleted) files
255 """
Ryan Cuiec4d6332011-05-02 14:15:25 -0700256 files = []
Peter Ammon811f6702014-06-12 15:45:38 -0700257 # See the documentation for 'git diff --raw' for the relevant format.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700258 for statusline in output.splitlines():
Peter Ammon811f6702014-06-12 15:45:38 -0700259 attributes, paths = statusline.split('\t', 1)
260 _, mode, _, _, status = attributes.split(' ')
261
262 # Ignore symlinks and directories.
263 imode = int(mode, 8)
264 if stat.S_ISDIR(imode) or stat.S_ISLNK(imode):
265 continue
266
267 # Ignore deleted files, and optionally return absolute paths of files.
268 if include_deletes or status != 'D':
269 # If a file was merely modified, we will have a single file path.
270 # If it was moved, we will have two paths (source and destination).
271 # In either case, we want the last path.
272 f = paths.split('\t')[-1]
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500273 if not relative:
274 pwd = os.getcwd()
275 f = os.path.join(pwd, f)
276 files.append(f)
Peter Ammon811f6702014-06-12 15:45:38 -0700277
Ryan Cuiec4d6332011-05-02 14:15:25 -0700278 return files
279
Ryan Cui1562fb82011-05-09 11:01:31 -0700280
Peter Ammon811f6702014-06-12 15:45:38 -0700281def _get_affected_files(commit, include_deletes=False, relative=False):
282 """Returns list of file paths that were modified/added, excluding symlinks.
283
284 Args:
285 commit: The commit
286 include_deletes: If true, we'll include deleted files in the result
287 relative: Whether to return relative or full paths to files
288
289 Returns:
290 A list of modified/added (and perhaps deleted) files
291 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700292 if commit == PRE_SUBMIT:
293 return _run_command(['git', 'diff-index', '--cached',
294 '--name-only', 'HEAD']).split()
Peter Ammon811f6702014-06-12 15:45:38 -0700295 output = _run_command(['git', 'diff', '--raw', commit + '^!'])
296 return _parse_affected_files(output, include_deletes, relative)
297
298
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700299def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700300 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700301 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700302 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700303
Ryan Cui1562fb82011-05-09 11:01:31 -0700304
Ryan Cuiec4d6332011-05-02 14:15:25 -0700305def _get_commit_desc(commit):
306 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700307 if commit == PRE_SUBMIT:
308 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400309 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700310
311
312# Common Hooks
313
Ryan Cui1562fb82011-05-09 11:01:31 -0700314
Mike Frysingerae409522014-02-01 03:16:11 -0500315def _check_no_long_lines(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700316 """Checks that there aren't any lines longer than maxlen characters in any of
317 the text files to be submitted.
318 """
319 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800320 SKIP_REGEXP = re.compile('|'.join([
321 r'https?://',
322 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700323
324 errors = []
325 files = _filter_files(_get_affected_files(commit),
326 COMMON_INCLUDED_PATHS,
327 COMMON_EXCLUDED_PATHS)
328
329 for afile in files:
330 for line_num, line in _get_file_diff(afile, commit):
331 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500332 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800333 continue
334
335 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
336 if len(errors) == 5: # Just show the first 5 errors.
337 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700338
339 if errors:
340 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700341 return HookFailure(msg, errors)
342
Ryan Cuiec4d6332011-05-02 14:15:25 -0700343
Mike Frysingerae409522014-02-01 03:16:11 -0500344def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700345 """Checks that there is no stray whitespace at source lines end."""
346 errors = []
347 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500348 COMMON_INCLUDED_PATHS,
349 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700350 for afile in files:
351 for line_num, line in _get_file_diff(afile, commit):
352 if line.rstrip() != line:
353 errors.append('%s, line %s' % (afile, line_num))
354 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700355 return HookFailure('Found line ending with white space in:', errors)
356
Ryan Cuiec4d6332011-05-02 14:15:25 -0700357
Mike Frysingerae409522014-02-01 03:16:11 -0500358def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700359 """Checks there are no unexpanded tabs."""
360 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700361 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700362 r".*\.ebuild$",
363 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500364 r".*/[M|m]akefile$",
365 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700366 ]
367
368 errors = []
369 files = _filter_files(_get_affected_files(commit),
370 COMMON_INCLUDED_PATHS,
371 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
372
373 for afile in files:
374 for line_num, line in _get_file_diff(afile, commit):
375 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500376 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700377 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700378 return HookFailure('Found a tab character in:', errors)
379
Ryan Cuiec4d6332011-05-02 14:15:25 -0700380
Mike Frysingerae409522014-02-01 03:16:11 -0500381def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700382 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700383 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700384
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700385 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700386 msg = 'Changelist description needs TEST field (after first line)'
387 return HookFailure(msg)
388
Ryan Cuiec4d6332011-05-02 14:15:25 -0700389
Mike Frysingerae409522014-02-01 03:16:11 -0500390def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700391 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
392 msg = 'Changelist has invalid CQ-DEPEND target.'
393 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
394 try:
395 patch.GetPaladinDeps(_get_commit_desc(commit))
396 except ValueError as ex:
397 return HookFailure(msg, [example, str(ex)])
398
399
Mike Frysingerae409522014-02-01 03:16:11 -0500400def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700401 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700402 OLD_BUG_RE = r'\nBUG=.*chromium-os'
403 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
404 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
405 'the chromium tracker in your BUG= line now.')
406 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700407
David James5c0073d2013-04-03 08:48:52 -0700408 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700409 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700410 msg = ('Changelist description needs BUG field (after first line):\n'
David James5c0073d2013-04-03 08:48:52 -0700411 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700412 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
413 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700414 return HookFailure(msg)
415
Ryan Cuiec4d6332011-05-02 14:15:25 -0700416
Doug Anderson42b8a052013-06-26 10:45:36 -0700417def _check_for_uprev(project, commit):
418 """Check that we're not missing a revbump of an ebuild in the given commit.
419
420 If the given commit touches files in a directory that has ebuilds somewhere
421 up the directory hierarchy, it's very likely that we need an ebuild revbump
422 in order for those changes to take effect.
423
424 It's not totally trivial to detect a revbump, so at least detect that an
425 ebuild with a revision number in it was touched. This should handle the
426 common case where we use a symlink to do the revbump.
427
428 TODO: it would be nice to enhance this hook to:
429 * Handle cases where people revbump with a slightly different syntax. I see
430 one ebuild (puppy) that revbumps with _pN. This is a false positive.
431 * Catches cases where people aren't using symlinks for revbumps. If they
432 edit a revisioned file directly (and are expected to rename it for revbump)
433 we'll miss that. Perhaps we could detect that the file touched is a
434 symlink?
435
436 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
437 still better off than without this check.
438
439 Args:
440 project: The project to look at
441 commit: The commit to look at
442
443 Returns:
444 A HookFailure or None.
445 """
Mike Frysinger011af942014-01-17 16:12:22 -0500446 # If this is the portage-stable overlay, then ignore the check. It's rare
447 # that we're doing anything other than importing files from upstream, so
448 # forcing a rev bump makes no sense.
449 whitelist = (
450 'chromiumos/overlays/portage-stable',
451 )
452 if project in whitelist:
453 return None
454
Doug Anderson42b8a052013-06-26 10:45:36 -0700455 affected_paths = _get_affected_files(commit, include_deletes=True)
456
457 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500458 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Doug Anderson42b8a052013-06-26 10:45:36 -0700459 affected_paths = [path for path in affected_paths
460 if os.path.basename(path) not in whitelist]
461 if not affected_paths:
462 return None
463
464 # If we've touched any file named with a -rN.ebuild then we'll say we're
465 # OK right away. See TODO above about enhancing this.
466 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', path)
467 for path in affected_paths)
468 if touched_revved_ebuild:
469 return None
470
471 # We want to examine the current contents of all directories that are parents
472 # of files that were touched (up to the top of the project).
473 #
474 # ...note: we use the current directory contents even though it may have
475 # changed since the commit we're looking at. This is just a heuristic after
476 # all. Worst case we don't flag a missing revbump.
477 project_top = os.getcwd()
478 dirs_to_check = set([project_top])
479 for path in affected_paths:
480 path = os.path.dirname(path)
481 while os.path.exists(path) and not os.path.samefile(path, project_top):
482 dirs_to_check.add(path)
483 path = os.path.dirname(path)
484
485 # Look through each directory. If it's got an ebuild in it then we'll
486 # consider this as a case when we need a revbump.
487 for dir_path in dirs_to_check:
488 contents = os.listdir(dir_path)
489 ebuilds = [os.path.join(dir_path, path)
490 for path in contents if path.endswith('.ebuild')]
491 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
492
493 # If the -9999.ebuild file was touched the bot will uprev for us.
494 # ...we'll use a simple intersection here as a heuristic...
495 if set(ebuilds_9999) & set(affected_paths):
496 continue
497
498 if ebuilds:
499 return HookFailure('Changelist probably needs a revbump of an ebuild\n'
500 'or a -r1.ebuild symlink if this is a new ebuild')
501
502 return None
503
504
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500505def _check_ebuild_eapi(project, commit):
506 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
507
508 We want to get away from older EAPI's as it makes life confusing and they
509 have less builtin error checking.
510
511 Args:
512 project: The project to look at
513 commit: The commit to look at
514
515 Returns:
516 A HookFailure or None.
517 """
518 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500519 # that we're doing anything other than importing files from upstream, and
520 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500521 whitelist = (
522 'chromiumos/overlays/portage-stable',
523 )
524 if project in whitelist:
525 return None
526
527 BAD_EAPIS = ('0', '1', '2', '3')
528
529 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
530
531 ebuilds_re = [r'\.ebuild$']
532 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
533 ebuilds_re)
534 bad_ebuilds = []
535
536 for ebuild in ebuilds:
537 # If the ebuild does not specify an EAPI, it defaults to 0.
538 eapi = '0'
539
540 lines = _get_file_content(ebuild, commit).splitlines()
541 if len(lines) == 1:
542 # This is most likely a symlink, so skip it entirely.
543 continue
544
545 for line in lines:
546 m = get_eapi.match(line)
547 if m:
548 # Once we hit the first EAPI line in this ebuild, stop processing.
549 # The spec requires that there only be one and it be first, so
550 # checking all possible values is pointless. We also assume that
551 # it's "the" EAPI line and not something in the middle of a heredoc.
552 eapi = m.group(1)
553 break
554
555 if eapi in BAD_EAPIS:
556 bad_ebuilds.append((ebuild, eapi))
557
558 if bad_ebuilds:
559 # pylint: disable=C0301
560 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
561 # pylint: enable=C0301
562 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500563 'These ebuilds are using old EAPIs. If these are imported from\n'
564 'Gentoo, then you may ignore and upload once with the --no-verify\n'
565 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500566 '\t%s\n'
567 'See this guide for more details:\n%s\n' %
568 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
569
570
Mike Frysinger89bdb852014-02-01 05:26:26 -0500571def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500572 """Make sure we use the new style KEYWORDS when possible in ebuilds.
573
574 If an ebuild generally does not care about the arch it is running on, then
575 ebuilds should flag it with one of:
576 KEYWORDS="*" # A stable ebuild.
577 KEYWORDS="~*" # An unstable ebuild.
578 KEYWORDS="-* ..." # Is known to only work on specific arches.
579
580 Args:
581 project: The project to look at
582 commit: The commit to look at
583
584 Returns:
585 A HookFailure or None.
586 """
587 WHITELIST = set(('*', '-*', '~*'))
588
589 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
590
Mike Frysinger89bdb852014-02-01 05:26:26 -0500591 ebuilds_re = [r'\.ebuild$']
592 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
593 ebuilds_re)
594
Mike Frysinger8d42d742014-09-22 15:50:21 -0400595 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500596 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400597 # We get the full content rather than a diff as the latter does not work
598 # on new files (like when adding new ebuilds).
599 lines = _get_file_content(ebuild, commit).splitlines()
600 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500601 m = get_keywords.match(line)
602 if m:
603 keywords = set(m.group(1).split())
604 if not keywords or WHITELIST - keywords != WHITELIST:
605 continue
606
Mike Frysinger8d42d742014-09-22 15:50:21 -0400607 bad_ebuilds.append(ebuild)
608
609 if bad_ebuilds:
610 return HookFailure(
611 '%s\n'
612 'Please update KEYWORDS to use a glob:\n'
613 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
614 ' KEYWORDS="*"\n'
615 'If the ebuild should be marked unstable (normal for '
616 'cros-workon / 9999 ebuilds):\n'
617 ' KEYWORDS="~*"\n'
618 'If the ebuild needs to be marked for only specific arches,'
619 'then use -* like so:\n'
620 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500621
622
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800623def _check_ebuild_licenses(_project, commit):
624 """Check if the LICENSE field in the ebuild is correct."""
625 affected_paths = _get_affected_files(commit)
626 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
627
628 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800629 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800630
631 for ebuild in touched_ebuilds:
632 # Skip virutal packages.
633 if ebuild.split('/')[-3] == 'virtual':
634 continue
635
636 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400637 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800638 except ValueError as e:
639 return HookFailure(e.message, [ebuild])
640
641 # Also ignore licenses ending with '?'
642 for license_type in [x for x in license_types
643 if x not in LICENSES_IGNORE and not x.endswith('?')]:
644 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400645 licenses_lib.Licensing.FindLicenseType(license_type)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800646 except AssertionError as e:
647 return HookFailure(e.message, [ebuild])
648
649
Mike Frysingercd363c82014-02-01 05:20:18 -0500650def _check_ebuild_virtual_pv(project, commit):
651 """Enforce the virtual PV policies."""
652 # If this is the portage-stable overlay, then ignore the check.
653 # We want to import virtuals as-is from upstream Gentoo.
654 whitelist = (
655 'chromiumos/overlays/portage-stable',
656 )
657 if project in whitelist:
658 return None
659
660 # We assume the repo name is the same as the dir name on disk.
661 # It would be dumb to not have them match though.
662 project = os.path.basename(project)
663
664 is_variant = lambda x: x.startswith('overlay-variant-')
665 is_board = lambda x: x.startswith('overlay-')
666 is_private = lambda x: x.endswith('-private')
667
668 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
669
670 ebuilds_re = [r'\.ebuild$']
671 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
672 ebuilds_re)
673 bad_ebuilds = []
674
675 for ebuild in ebuilds:
676 m = get_pv.match(ebuild)
677 if m:
678 overlay = m.group(1)
679 if not overlay or not is_board(overlay):
680 overlay = project
681
682 pv = m.group(3).split('-', 1)[0]
683
684 if is_private(overlay):
685 want_pv = '3.5' if is_variant(overlay) else '3'
686 elif is_board(overlay):
687 want_pv = '2.5' if is_variant(overlay) else '2'
688 else:
689 want_pv = '1'
690
691 if pv != want_pv:
692 bad_ebuilds.append((ebuild, pv, want_pv))
693
694 if bad_ebuilds:
695 # pylint: disable=C0301
696 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
697 # pylint: enable=C0301
698 return HookFailure(
699 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
700 '\t%s\n'
701 'If this is an upstream Gentoo virtual, then you may ignore this\n'
702 'check (and re-run w/--no-verify). Otherwise, please see this\n'
703 'page for more details:\n%s\n' %
704 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
705 for x in bad_ebuilds]), url))
706
707
Mike Frysingerae409522014-02-01 03:16:11 -0500708def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700709 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -0400710 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700711 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -0400712 m = re.search(CHANGE_ID_RE, desc)
713 if not m or desc[m.end():].strip():
Ryan Cui1562fb82011-05-09 11:01:31 -0700714 return HookFailure('Change-Id must be in last paragraph of description.')
715
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700716
Mike Frysinger36b2ebc2014-10-31 14:02:03 -0400717def _check_commit_message_style(_project, commit):
718 """Verify that the commit message matches our style.
719
720 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
721 commit hooks.
722 """
723 desc = _get_commit_desc(commit)
724
725 # The first line should be by itself.
726 lines = desc.splitlines()
727 if len(lines) > 1 and lines[1]:
728 return HookFailure('The second line of the commit message must be blank.')
729
730 # The first line should be one sentence.
731 if '. ' in lines[0]:
732 return HookFailure('The first line cannot be more than one sentence.')
733
734 # The first line cannot be too long.
735 MAX_FIRST_LINE_LEN = 100
736 if len(lines[0]) > MAX_FIRST_LINE_LEN:
737 return HookFailure('The first line must be less than %i chars.' %
738 MAX_FIRST_LINE_LEN)
739
740
Mike Frysingerae409522014-02-01 03:16:11 -0500741def _check_license(_project, commit):
Mike Frysinger98638102014-08-28 00:15:08 -0400742 """Verifies the license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700743
Mike Frysinger98638102014-08-28 00:15:08 -0400744 Should be following the spec:
745 http://dev.chromium.org/developers/coding-style#TOC-File-headers
746 """
747 # For older years, be a bit more flexible as our policy says leave them be.
748 LICENSE_HEADER = (
749 r'.* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
750 'All rights reserved\.' '\n'
751 r'.* Use of this source code is governed by a BSD-style license that can '
752 'be\n'
753 r'.* found in the LICENSE file\.'
754 '\n'
755 )
756 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
757
758 # For newer years, be stricter.
759 COPYRIGHT_LINE = (
760 r'.* Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
761 'All rights reserved\.' '\n'
762 )
763 copyright_re = re.compile(COPYRIGHT_LINE)
764
765 bad_files = []
766 bad_copyright_files = []
767 files = _filter_files(_get_affected_files(commit, relative=True),
768 COMMON_INCLUDED_PATHS,
769 COMMON_EXCLUDED_PATHS)
770
771 for f in files:
772 contents = _get_file_content(f, commit)
773 if not contents:
774 # Ignore empty files.
775 continue
776
777 if not license_re.search(contents):
778 bad_files.append(f)
779 elif copyright_re.search(contents):
780 bad_copyright_files.append(f)
781
782 if bad_files:
783 msg = '%s:\n%s\n%s' % (
784 'License must match', license_re.pattern,
785 'Found a bad header in these files:')
786 return HookFailure(msg, bad_files)
787
788 if bad_copyright_files:
789 msg = 'Do not use (c) in copyright headers in new files:'
790 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700791
792
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400793def _check_layout_conf(_project, commit):
794 """Verifies the metadata/layout.conf file."""
795 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400796 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400797 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400798 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400799
Mike Frysinger94a670c2014-09-19 12:46:26 -0400800 # Handle multiple overlays in a single commit (like the public tree).
801 for f in _get_affected_files(commit, relative=True):
802 if f.endswith(repo_name):
803 repo_names.append(f)
804 elif f.endswith(layout_path):
805 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400806
807 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -0400808 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400809 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -0400810 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400811
Mike Frysinger94a670c2014-09-19 12:46:26 -0400812 # Gather all the errors in one pass so we show one full message.
813 all_errors = {}
814 for layout_path in layout_paths:
815 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400816
Mike Frysinger94a670c2014-09-19 12:46:26 -0400817 # Make sure the config file is sorted.
818 data = [x for x in _get_file_content(layout_path, commit).splitlines()
819 if x and x[0] != '#']
820 if sorted(data) != data:
821 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400822
Mike Frysinger94a670c2014-09-19 12:46:26 -0400823 # Require people to set specific values all the time.
824 settings = (
825 # TODO: Enable this for everyone. http://crbug.com/408038
826 #('fast caching', 'cache-format = md5-dict'),
827 ('fast manifests', 'thin-manifests = true'),
828 ('extra features', 'profile-formats = portage-2'),
829 )
830 for reason, line in settings:
831 if line not in data:
832 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400833
Mike Frysinger94a670c2014-09-19 12:46:26 -0400834 # Require one of these settings.
835 if ('use-manifests = true' not in data and
836 'use-manifests = strict' not in data):
837 errors += ['enable file checking with: use-manifests = true']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400838
Mike Frysinger94a670c2014-09-19 12:46:26 -0400839 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -0400840 for line in data:
841 if line.startswith('repo-name = '):
842 break
843 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -0400844 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400845
Mike Frysinger94a670c2014-09-19 12:46:26 -0400846 # Summarize all the errors we saw (if any).
847 lines = ''
848 for layout_path, errors in all_errors.items():
849 if errors:
850 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
851 if lines:
852 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
853 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400854
855
Ryan Cuiec4d6332011-05-02 14:15:25 -0700856# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700857
Ryan Cui1562fb82011-05-09 11:01:31 -0700858
Mike Frysingerae409522014-02-01 03:16:11 -0500859def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700860 """Runs checkpatch.pl on the given project"""
861 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700862 options = list(options)
863 if commit == PRE_SUBMIT:
864 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
865 # this case.
866 options.append('--ignore=MISSING_SIGN_OFF')
867 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700868 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700869 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700870 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700871 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700872
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700873
Anton Staaf815d6852011-08-22 10:08:45 -0700874def _run_checkpatch_no_tree(project, commit):
875 return _run_checkpatch(project, commit, ['--no-tree'])
876
Mike Frysingerae409522014-02-01 03:16:11 -0500877
Randall Spangler7318fd62013-11-21 12:16:58 -0800878def _run_checkpatch_ec(project, commit):
879 """Runs checkpatch with options for Chromium EC projects."""
880 return _run_checkpatch(project, commit, ['--no-tree',
881 '--ignore=MSLEEP,VOLATILE'])
882
Mike Frysingerae409522014-02-01 03:16:11 -0500883
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700884def _run_checkpatch_depthcharge(project, commit):
885 """Runs checkpatch with options for depthcharge."""
886 return _run_checkpatch(project, commit, [
887 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700888 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
889 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
890 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700891
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700892def _run_checkpatch_coreboot(project, commit):
893 """Runs checkpatch with options for coreboot."""
894 return _run_checkpatch(project, commit, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700895 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700896 '--no-tree',
897 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
Shawn Nematbakhsh6442c582014-08-22 14:32:06 -0700898 'GLOBAL_INITIALISERS,INITIALISED_STATIC,C99_COMMENTS'])
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700899
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700900
Mike Frysingerae409522014-02-01 03:16:11 -0500901def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700902 """Makes sure kernel config changes are not mixed with code changes"""
903 files = _get_affected_files(commit)
904 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
905 return HookFailure('Changes to chromeos/config/ and regular files must '
906 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700907
Mike Frysingerae409522014-02-01 03:16:11 -0500908
909def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700910 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700911 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700912 try:
913 json.load(open(f))
914 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700915 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700916
917
Mike Frysingerae409522014-02-01 03:16:11 -0500918def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400919 """Make sure Manifest files only have DIST lines"""
920 paths = []
921
922 for path in _get_affected_files(commit):
923 if os.path.basename(path) != 'Manifest':
924 continue
925 if not os.path.exists(path):
926 continue
927
928 with open(path, 'r') as f:
929 for line in f.readlines():
930 if not line.startswith('DIST '):
931 paths.append(path)
932 break
933
934 if paths:
935 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
936 ('\n'.join(paths),))
937
938
Mike Frysingerae409522014-02-01 03:16:11 -0500939def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700940 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700941 if commit == PRE_SUBMIT:
942 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700943 BRANCH_RE = r'\nBRANCH=\S+'
944
945 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
946 msg = ('Changelist description needs BRANCH field (after first line)\n'
947 'E.g. BRANCH=none or BRANCH=link,snow')
948 return HookFailure(msg)
949
950
Mike Frysingerae409522014-02-01 03:16:11 -0500951def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800952 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700953 if commit == PRE_SUBMIT:
954 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800955 SIGNOFF_RE = r'\nSigned-off-by: \S+'
956
957 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
958 msg = ('Changelist description needs Signed-off-by: field\n'
959 'E.g. Signed-off-by: My Name <me@chromium.org>')
960 return HookFailure(msg)
961
962
Jon Salz3ee59de2012-08-18 13:54:22 +0800963def _run_project_hook_script(script, project, commit):
964 """Runs a project hook script.
965
966 The script is run with the following environment variables set:
967 PRESUBMIT_PROJECT: The affected project
968 PRESUBMIT_COMMIT: The affected commit
969 PRESUBMIT_FILES: A newline-separated list of affected files
970
971 The script is considered to fail if the exit code is non-zero. It should
972 write an error message to stdout.
973 """
974 env = dict(os.environ)
975 env['PRESUBMIT_PROJECT'] = project
976 env['PRESUBMIT_COMMIT'] = commit
977
978 # Put affected files in an environment variable
979 files = _get_affected_files(commit)
980 env['PRESUBMIT_FILES'] = '\n'.join(files)
981
982 process = subprocess.Popen(script, env=env, shell=True,
983 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800984 stdout=subprocess.PIPE,
985 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800986 stdout, _ = process.communicate()
987 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800988 if stdout:
989 stdout = re.sub('(?m)^', ' ', stdout)
990 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800991 (script, process.returncode,
992 ':\n' + stdout if stdout else ''))
993
994
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700995def _moved_to_platform2(project, _commit):
996 """Forbids commits to legacy repo in src/platform."""
997 return HookFailure('%s has been moved to platform2. This change should be '
998 'made there.' % project)
999
1000
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001001def _check_project_prefix(_project, commit):
1002 """Fails if the change is project specific and the commit message is not
1003 prefixed by the project_name.
1004 """
1005
1006 files = _get_affected_files(commit, relative=True)
1007 prefix = os.path.commonprefix(files)
1008 prefix = os.path.dirname(prefix)
1009
1010 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07001011 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001012 return
1013
1014 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07001015
1016 # The common files may all be within a subdirectory of the main project
1017 # directory, so walk up the tree until we find an alias file.
1018 # _get_affected_files() should return relative paths, but check against '/' to
1019 # ensure that this loop terminates even if it receives an absolute path.
1020 while prefix and prefix != '/':
1021 alias_file = os.path.join(prefix, '.project_alias')
1022
1023 # If an alias exists, use it.
1024 if os.path.isfile(alias_file):
1025 project_name = osutils.ReadFile(alias_file).strip()
1026
1027 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001028
1029 if not _get_commit_desc(commit).startswith(project_name + ': '):
1030 return HookFailure('The commit title for changes affecting only %s'
1031 ' should start with \"%s: \"'
1032 % (project_name, project_name))
1033
1034
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001035# Base
1036
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001037# A list of hooks which are not project specific and check patch description
1038# (as opposed to patch body).
1039_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001040 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001041 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001042 _check_change_has_test_field,
1043 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001044 _check_commit_message_style,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001045]
1046
1047
1048# A list of hooks that are not project-specific
1049_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001050 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001051 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001052 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001053 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -07001054 _check_no_stray_whitespace,
1055 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001056 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001057 _check_license,
1058 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001059 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001060]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001061
Ryan Cui1562fb82011-05-09 11:01:31 -07001062
Ryan Cui9b651632011-05-11 11:38:58 -07001063# A dictionary of project-specific hooks(callbacks), indexed by project name.
1064# dict[project] = [callback1, callback2]
1065_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001066 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001067 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1068 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001069 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001070 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001071 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001072 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1073 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001074 "chromiumos/overlays/board-overlays": [_check_manifests],
1075 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1076 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001077 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury42052852014-10-06 16:02:38 -07001078 "chromiumos/platform/depthcharge": [_check_change_has_branch_field,
1079 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001080 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001081 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001082 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001083 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001084 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury42052852014-10-06 16:02:38 -07001085 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
1086 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001087 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001088 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001089 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1090 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1091 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001092 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001093}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001094
Ryan Cui1562fb82011-05-09 11:01:31 -07001095
Ryan Cui9b651632011-05-11 11:38:58 -07001096# A dictionary of flags (keys) that can appear in the config file, and the hook
1097# that the flag disables (value)
1098_DISABLE_FLAGS = {
1099 'stray_whitespace_check': _check_no_stray_whitespace,
1100 'long_line_check': _check_no_long_lines,
1101 'cros_license_check': _check_license,
1102 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001103 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001104 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001105 'bug_field_check': _check_change_has_bug_field,
1106 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001107}
1108
1109
Jon Salz3ee59de2012-08-18 13:54:22 +08001110def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001111 """Returns a set of hooks disabled by the current project's config file.
1112
1113 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001114
1115 Args:
1116 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001117 """
1118 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001119 if not config.has_section(SECTION):
1120 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001121
1122 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001123 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001124 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001125 if not config.getboolean(SECTION, flag):
1126 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001127 except ValueError as e:
1128 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001129 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001130
1131 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1132 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1133
1134
Jon Salz3ee59de2012-08-18 13:54:22 +08001135def _get_project_hook_scripts(config):
1136 """Returns a list of project-specific hook scripts.
1137
1138 Args:
1139 config: A ConfigParser for the project's config file.
1140 """
1141 SECTION = 'Hook Scripts'
1142 if not config.has_section(SECTION):
1143 return []
1144
1145 hook_names_values = config.items(SECTION)
1146 hook_names_values.sort(key=lambda x: x[0])
1147 return [x[1] for x in hook_names_values]
1148
1149
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001150def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001151 """Returns a list of hooks that need to be run for a project.
1152
1153 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001154
1155 Args:
1156 project: A string, name of the project.
1157 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001158 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001159 config = ConfigParser.RawConfigParser()
1160 try:
1161 config.read(_CONFIG_FILE)
1162 except ConfigParser.Error:
1163 # Just use an empty config file
1164 config = ConfigParser.RawConfigParser()
1165
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001166 if presubmit:
1167 hook_list = _COMMON_HOOKS
1168 else:
1169 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1170
Jon Salz3ee59de2012-08-18 13:54:22 +08001171 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001172 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001173
1174 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001175 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1176 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001177
Jon Salz3ee59de2012-08-18 13:54:22 +08001178 for script in _get_project_hook_scripts(config):
1179 hooks.append(functools.partial(_run_project_hook_script, script))
1180
Ryan Cui9b651632011-05-11 11:38:58 -07001181 return hooks
1182
1183
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001184def _run_project_hooks(project, proj_dir=None,
1185 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001186 """For each project run its project specific hook from the hooks dictionary.
1187
1188 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001189 project: The name of project to run hooks for.
1190 proj_dir: If non-None, this is the directory the project is in. If None,
1191 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001192 commit_list: A list of commits to run hooks against. If None or empty list
1193 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001194 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001195
1196 Returns:
1197 Boolean value of whether any errors were ecountered while running the hooks.
1198 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001199 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001200 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1201 if len(proj_dirs) == 0:
1202 print('%s cannot be found.' % project, file=sys.stderr)
1203 print('Please specify a valid project.', file=sys.stderr)
1204 return True
1205 if len(proj_dirs) > 1:
1206 print('%s is associated with multiple directories.' % project,
1207 file=sys.stderr)
1208 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1209 return True
1210 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001211
Ryan Cuiec4d6332011-05-02 14:15:25 -07001212 pwd = os.getcwd()
1213 # hooks assume they are run from the root of the project
1214 os.chdir(proj_dir)
1215
Doug Anderson14749562013-06-26 13:38:29 -07001216 if not commit_list:
1217 try:
1218 commit_list = _get_commits()
1219 except VerifyException as e:
1220 PrintErrorForProject(project, HookFailure(str(e)))
1221 os.chdir(pwd)
1222 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001223
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001224 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001225 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001226 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001227 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001228 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001229 hook_error = hook(project, commit)
1230 if hook_error:
1231 error_list.append(hook_error)
1232 error_found = True
1233 if error_list:
1234 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1235 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001236
Ryan Cuiec4d6332011-05-02 14:15:25 -07001237 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001238 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001239
Mike Frysingerae409522014-02-01 03:16:11 -05001240
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001241# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001242
Ryan Cui1562fb82011-05-09 11:01:31 -07001243
Mike Frysingerae409522014-02-01 03:16:11 -05001244def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001245 """Main function invoked directly by repo.
1246
1247 This function will exit directly upon error so that repo doesn't print some
1248 obscure error message.
1249
1250 Args:
1251 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001252 worktree_list: A list of directories. It should be the same length as
1253 project_list, so that each entry in project_list matches with a directory
1254 in worktree_list. If None, we will attempt to calculate the directories
1255 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001256 kwargs: Leave this here for forward-compatibility.
1257 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001258 found_error = False
David James2edd9002013-10-11 14:09:19 -07001259 if not worktree_list:
1260 worktree_list = [None] * len(project_list)
1261 for project, worktree in zip(project_list, worktree_list):
1262 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001263 found_error = True
1264
Mike Frysingerae409522014-02-01 03:16:11 -05001265 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001266 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001267 '- To disable some source style checks, and for other hints, see '
1268 '<checkout_dir>/src/repohooks/README\n'
1269 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001270 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001271 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001272
Ryan Cui1562fb82011-05-09 11:01:31 -07001273
Doug Anderson44a644f2011-11-02 10:37:37 -07001274def _identify_project(path):
1275 """Identify the repo project associated with the given path.
1276
1277 Returns:
1278 A string indicating what project is associated with the path passed in or
1279 a blank string upon failure.
1280 """
1281 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1282 stderr=subprocess.PIPE, cwd=path).strip()
1283
1284
1285def direct_main(args, verbose=False):
1286 """Run hooks directly (outside of the context of repo).
1287
1288 # Setup for doctests below.
1289 # ...note that some tests assume that running pre-upload on this CWD is fine.
1290 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1291 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1292 >>> olddir = os.getcwd()
1293
1294 # OK to run w/ no arugments; will run with CWD.
1295 >>> os.chdir(mydir)
1296 >>> direct_main(['prog_name'], verbose=True)
1297 Running hooks on chromiumos/repohooks
1298 0
1299 >>> os.chdir(olddir)
1300
1301 # Run specifying a dir
1302 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1303 Running hooks on chromiumos/repohooks
1304 0
1305
1306 # Not a problem to use a bogus project; we'll just get default settings.
1307 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1308 Running hooks on X
1309 0
1310
1311 # Run with project but no dir
1312 >>> os.chdir(mydir)
1313 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1314 Running hooks on X
1315 0
1316 >>> os.chdir(olddir)
1317
1318 # Try with a non-git CWD
1319 >>> os.chdir('/tmp')
1320 >>> direct_main(['prog_name'])
1321 Traceback (most recent call last):
1322 ...
1323 BadInvocation: The current directory is not part of a git project.
1324
1325 # Check various bad arguments...
1326 >>> direct_main(['prog_name', 'bogus'])
1327 Traceback (most recent call last):
1328 ...
1329 BadInvocation: Unexpected arguments: bogus
1330 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1331 Traceback (most recent call last):
1332 ...
1333 BadInvocation: Invalid dir: bogusdir
1334 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1335 Traceback (most recent call last):
1336 ...
1337 BadInvocation: Not a git directory: /tmp
1338
1339 Args:
1340 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001341 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001342
1343 Returns:
1344 0 if no pre-upload failures, 1 if failures.
1345
1346 Raises:
1347 BadInvocation: On some types of invocation errors.
1348 """
1349 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1350 parser = optparse.OptionParser(description=desc)
1351
1352 parser.add_option('--dir', default=None,
1353 help='The directory that the project lives in. If not '
1354 'specified, use the git project root based on the cwd.')
1355 parser.add_option('--project', default=None,
1356 help='The project repo path; this can affect how the hooks '
1357 'get run, since some hooks are project-specific. For '
1358 'chromite this is chromiumos/chromite. If not specified, '
1359 'the repo tool will be used to figure this out based on '
1360 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001361 parser.add_option('--rerun-since', default=None,
1362 help='Rerun hooks on old commits since the given date. '
1363 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001364 'e.g. 2012-06-20. This option is mutually exclusive '
1365 'with --pre-submit.')
1366 parser.add_option('--pre-submit', action="store_true",
1367 help='Run the check against the pending commit. '
1368 'This option should be used at the \'git commit\' '
1369 'phase as opposed to \'repo upload\'. This option '
1370 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001371
1372 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001373
1374 opts, args = parser.parse_args(args[1:])
1375
Doug Anderson14749562013-06-26 13:38:29 -07001376 if opts.rerun_since:
1377 if args:
1378 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1379 ' '.join(args))
1380
1381 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1382 all_commits = _run_command(cmd).splitlines()
1383 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1384
1385 # Eliminate chrome-bot commits but keep ordering the same...
1386 bot_commits = set(bot_commits)
1387 args = [c for c in all_commits if c not in bot_commits]
1388
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001389 if opts.pre_submit:
1390 raise BadInvocation('rerun-since and pre-submit can not be '
1391 'used together')
1392 if opts.pre_submit:
1393 if args:
1394 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1395 ' '.join(args))
1396 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001397
1398 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1399 # project from CWD
1400 if opts.dir is None:
1401 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1402 stderr=subprocess.PIPE).strip()
1403 if not git_dir:
1404 raise BadInvocation('The current directory is not part of a git project.')
1405 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1406 elif not os.path.isdir(opts.dir):
1407 raise BadInvocation('Invalid dir: %s' % opts.dir)
1408 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1409 raise BadInvocation('Not a git directory: %s' % opts.dir)
1410
1411 # Identify the project if it wasn't specified; this _requires_ the repo
1412 # tool to be installed and for the project to be part of a repo checkout.
1413 if not opts.project:
1414 opts.project = _identify_project(opts.dir)
1415 if not opts.project:
1416 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1417
1418 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001419 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001420
Doug Anderson14749562013-06-26 13:38:29 -07001421 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001422 commit_list=args,
1423 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001424 if found_error:
1425 return 1
1426 return 0
1427
1428
1429def _test():
1430 """Run any built-in tests."""
1431 import doctest
1432 doctest.testmod()
1433
1434
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001435if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001436 if sys.argv[1:2] == ["--test"]:
1437 _test()
1438 exit_code = 0
1439 else:
1440 prog_name = os.path.basename(sys.argv[0])
1441 try:
1442 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001443 except BadInvocation, err:
1444 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001445 exit_code = 1
1446 sys.exit(exit_code)