blob: b44cbc94d6eb70c0a2295fddbc367f6803bbfbf1 [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 Frysingerae409522014-02-01 03:16:11 -0500717def _check_license(_project, commit):
Mike Frysinger98638102014-08-28 00:15:08 -0400718 """Verifies the license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700719
Mike Frysinger98638102014-08-28 00:15:08 -0400720 Should be following the spec:
721 http://dev.chromium.org/developers/coding-style#TOC-File-headers
722 """
723 # For older years, be a bit more flexible as our policy says leave them be.
724 LICENSE_HEADER = (
725 r'.* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
726 'All rights reserved\.' '\n'
727 r'.* Use of this source code is governed by a BSD-style license that can '
728 'be\n'
729 r'.* found in the LICENSE file\.'
730 '\n'
731 )
732 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
733
734 # For newer years, be stricter.
735 COPYRIGHT_LINE = (
736 r'.* Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
737 'All rights reserved\.' '\n'
738 )
739 copyright_re = re.compile(COPYRIGHT_LINE)
740
741 bad_files = []
742 bad_copyright_files = []
743 files = _filter_files(_get_affected_files(commit, relative=True),
744 COMMON_INCLUDED_PATHS,
745 COMMON_EXCLUDED_PATHS)
746
747 for f in files:
748 contents = _get_file_content(f, commit)
749 if not contents:
750 # Ignore empty files.
751 continue
752
753 if not license_re.search(contents):
754 bad_files.append(f)
755 elif copyright_re.search(contents):
756 bad_copyright_files.append(f)
757
758 if bad_files:
759 msg = '%s:\n%s\n%s' % (
760 'License must match', license_re.pattern,
761 'Found a bad header in these files:')
762 return HookFailure(msg, bad_files)
763
764 if bad_copyright_files:
765 msg = 'Do not use (c) in copyright headers in new files:'
766 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700767
768
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400769def _check_layout_conf(_project, commit):
770 """Verifies the metadata/layout.conf file."""
771 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400772 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400773 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400774 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400775
Mike Frysinger94a670c2014-09-19 12:46:26 -0400776 # Handle multiple overlays in a single commit (like the public tree).
777 for f in _get_affected_files(commit, relative=True):
778 if f.endswith(repo_name):
779 repo_names.append(f)
780 elif f.endswith(layout_path):
781 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400782
783 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -0400784 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400785 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -0400786 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400787
Mike Frysinger94a670c2014-09-19 12:46:26 -0400788 # Gather all the errors in one pass so we show one full message.
789 all_errors = {}
790 for layout_path in layout_paths:
791 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400792
Mike Frysinger94a670c2014-09-19 12:46:26 -0400793 # Make sure the config file is sorted.
794 data = [x for x in _get_file_content(layout_path, commit).splitlines()
795 if x and x[0] != '#']
796 if sorted(data) != data:
797 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400798
Mike Frysinger94a670c2014-09-19 12:46:26 -0400799 # Require people to set specific values all the time.
800 settings = (
801 # TODO: Enable this for everyone. http://crbug.com/408038
802 #('fast caching', 'cache-format = md5-dict'),
803 ('fast manifests', 'thin-manifests = true'),
804 ('extra features', 'profile-formats = portage-2'),
805 )
806 for reason, line in settings:
807 if line not in data:
808 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400809
Mike Frysinger94a670c2014-09-19 12:46:26 -0400810 # Require one of these settings.
811 if ('use-manifests = true' not in data and
812 'use-manifests = strict' not in data):
813 errors += ['enable file checking with: use-manifests = true']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400814
Mike Frysinger94a670c2014-09-19 12:46:26 -0400815 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -0400816 for line in data:
817 if line.startswith('repo-name = '):
818 break
819 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -0400820 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400821
Mike Frysinger94a670c2014-09-19 12:46:26 -0400822 # Summarize all the errors we saw (if any).
823 lines = ''
824 for layout_path, errors in all_errors.items():
825 if errors:
826 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
827 if lines:
828 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
829 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400830
831
Ryan Cuiec4d6332011-05-02 14:15:25 -0700832# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700833
Ryan Cui1562fb82011-05-09 11:01:31 -0700834
Mike Frysingerae409522014-02-01 03:16:11 -0500835def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700836 """Runs checkpatch.pl on the given project"""
837 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700838 options = list(options)
839 if commit == PRE_SUBMIT:
840 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
841 # this case.
842 options.append('--ignore=MISSING_SIGN_OFF')
843 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700844 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700845 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700846 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -0700847 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700848
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700849
Anton Staaf815d6852011-08-22 10:08:45 -0700850def _run_checkpatch_no_tree(project, commit):
851 return _run_checkpatch(project, commit, ['--no-tree'])
852
Mike Frysingerae409522014-02-01 03:16:11 -0500853
Randall Spangler7318fd62013-11-21 12:16:58 -0800854def _run_checkpatch_ec(project, commit):
855 """Runs checkpatch with options for Chromium EC projects."""
856 return _run_checkpatch(project, commit, ['--no-tree',
857 '--ignore=MSLEEP,VOLATILE'])
858
Mike Frysingerae409522014-02-01 03:16:11 -0500859
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700860def _run_checkpatch_depthcharge(project, commit):
861 """Runs checkpatch with options for depthcharge."""
862 return _run_checkpatch(project, commit, [
863 '--no-tree',
Julius Werner83a35bb2014-06-23 12:51:48 -0700864 '--ignore=CAMELCASE,C99_COMMENTS,NEW_TYPEDEFS,CONFIG_DESCRIPTION,'
865 'SPACING,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,GLOBAL_INITIALISERS,'
866 'INITIALISED_STATIC,OPEN_BRACE,TRAILING_STATEMENTS'])
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700867
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700868def _run_checkpatch_coreboot(project, commit):
869 """Runs checkpatch with options for coreboot."""
870 return _run_checkpatch(project, commit, [
Vadim Bendeburyac4bc6c2014-08-29 19:56:43 -0700871 '--min-conf-desc-length=2',
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700872 '--no-tree',
873 '--ignore=NEW_TYPEDEFS,PREFER_PACKED,PREFER_PRINTF,PREFER_ALIGNED,'
Shawn Nematbakhsh6442c582014-08-22 14:32:06 -0700874 'GLOBAL_INITIALISERS,INITIALISED_STATIC,C99_COMMENTS'])
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -0700875
Vadim Bendeburyf8536032014-06-22 12:42:18 -0700876
Mike Frysingerae409522014-02-01 03:16:11 -0500877def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -0700878 """Makes sure kernel config changes are not mixed with code changes"""
879 files = _get_affected_files(commit)
880 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
881 return HookFailure('Changes to chromeos/config/ and regular files must '
882 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -0700883
Mike Frysingerae409522014-02-01 03:16:11 -0500884
885def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -0700886 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -0700887 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -0700888 try:
889 json.load(open(f))
890 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -0700891 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -0700892
893
Mike Frysingerae409522014-02-01 03:16:11 -0500894def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -0400895 """Make sure Manifest files only have DIST lines"""
896 paths = []
897
898 for path in _get_affected_files(commit):
899 if os.path.basename(path) != 'Manifest':
900 continue
901 if not os.path.exists(path):
902 continue
903
904 with open(path, 'r') as f:
905 for line in f.readlines():
906 if not line.startswith('DIST '):
907 paths.append(path)
908 break
909
910 if paths:
911 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
912 ('\n'.join(paths),))
913
914
Mike Frysingerae409522014-02-01 03:16:11 -0500915def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700916 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700917 if commit == PRE_SUBMIT:
918 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -0700919 BRANCH_RE = r'\nBRANCH=\S+'
920
921 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
922 msg = ('Changelist description needs BRANCH field (after first line)\n'
923 'E.g. BRANCH=none or BRANCH=link,snow')
924 return HookFailure(msg)
925
926
Mike Frysingerae409522014-02-01 03:16:11 -0500927def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800928 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700929 if commit == PRE_SUBMIT:
930 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -0800931 SIGNOFF_RE = r'\nSigned-off-by: \S+'
932
933 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
934 msg = ('Changelist description needs Signed-off-by: field\n'
935 'E.g. Signed-off-by: My Name <me@chromium.org>')
936 return HookFailure(msg)
937
938
Jon Salz3ee59de2012-08-18 13:54:22 +0800939def _run_project_hook_script(script, project, commit):
940 """Runs a project hook script.
941
942 The script is run with the following environment variables set:
943 PRESUBMIT_PROJECT: The affected project
944 PRESUBMIT_COMMIT: The affected commit
945 PRESUBMIT_FILES: A newline-separated list of affected files
946
947 The script is considered to fail if the exit code is non-zero. It should
948 write an error message to stdout.
949 """
950 env = dict(os.environ)
951 env['PRESUBMIT_PROJECT'] = project
952 env['PRESUBMIT_COMMIT'] = commit
953
954 # Put affected files in an environment variable
955 files = _get_affected_files(commit)
956 env['PRESUBMIT_FILES'] = '\n'.join(files)
957
958 process = subprocess.Popen(script, env=env, shell=True,
959 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +0800960 stdout=subprocess.PIPE,
961 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +0800962 stdout, _ = process.communicate()
963 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +0800964 if stdout:
965 stdout = re.sub('(?m)^', ' ', stdout)
966 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +0800967 (script, process.returncode,
968 ':\n' + stdout if stdout else ''))
969
970
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -0700971def _moved_to_platform2(project, _commit):
972 """Forbids commits to legacy repo in src/platform."""
973 return HookFailure('%s has been moved to platform2. This change should be '
974 'made there.' % project)
975
976
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700977def _check_project_prefix(_project, commit):
978 """Fails if the change is project specific and the commit message is not
979 prefixed by the project_name.
980 """
981
982 files = _get_affected_files(commit, relative=True)
983 prefix = os.path.commonprefix(files)
984 prefix = os.path.dirname(prefix)
985
986 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -0700987 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -0700988 return
989
990 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -0700991
992 # The common files may all be within a subdirectory of the main project
993 # directory, so walk up the tree until we find an alias file.
994 # _get_affected_files() should return relative paths, but check against '/' to
995 # ensure that this loop terminates even if it receives an absolute path.
996 while prefix and prefix != '/':
997 alias_file = os.path.join(prefix, '.project_alias')
998
999 # If an alias exists, use it.
1000 if os.path.isfile(alias_file):
1001 project_name = osutils.ReadFile(alias_file).strip()
1002
1003 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001004
1005 if not _get_commit_desc(commit).startswith(project_name + ': '):
1006 return HookFailure('The commit title for changes affecting only %s'
1007 ' should start with \"%s: \"'
1008 % (project_name, project_name))
1009
1010
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001011# Base
1012
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001013# A list of hooks which are not project specific and check patch description
1014# (as opposed to patch body).
1015_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001016 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001017 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001018 _check_change_has_test_field,
1019 _check_change_has_proper_changeid,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001020]
1021
1022
1023# A list of hooks that are not project-specific
1024_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001025 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001026 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001027 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001028 _check_ebuild_virtual_pv,
Ryan Cui9b651632011-05-11 11:38:58 -07001029 _check_no_stray_whitespace,
1030 _check_no_long_lines,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001031 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001032 _check_license,
1033 _check_no_tabs,
Doug Anderson42b8a052013-06-26 10:45:36 -07001034 _check_for_uprev,
Ryan Cui9b651632011-05-11 11:38:58 -07001035]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001036
Ryan Cui1562fb82011-05-09 11:01:31 -07001037
Ryan Cui9b651632011-05-11 11:38:58 -07001038# A dictionary of project-specific hooks(callbacks), indexed by project name.
1039# dict[project] = [callback1, callback2]
1040_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingerdf980702013-08-22 22:25:22 -04001041 "chromeos/autotest-tools": [_run_json_check],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001042 "chromeos/overlays/chromeos-overlay": [_check_manifests],
1043 "chromeos/overlays/chromeos-partner-overlay": [_check_manifests],
Randall Spangler7318fd62013-11-21 12:16:58 -08001044 "chromeos/platform/ec-private": [_run_checkpatch_ec,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001045 _check_change_has_branch_field],
Puneet Kumar57b9c092012-08-14 18:58:29 -07001046 "chromeos/third_party/intel-framework": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001047 "chromeos/vendor/kernel-exynos-staging": [_run_checkpatch,
1048 _kernel_configcheck],
Mike Frysinger52b537e2013-08-22 22:59:53 -04001049 "chromiumos/overlays/board-overlays": [_check_manifests],
1050 "chromiumos/overlays/chromiumos-overlay": [_check_manifests],
1051 "chromiumos/overlays/portage-stable": [_check_manifests],
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001052 "chromiumos/platform2": [_check_project_prefix],
Vadim Bendebury42052852014-10-06 16:02:38 -07001053 "chromiumos/platform/depthcharge": [_check_change_has_branch_field,
1054 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001055 _run_checkpatch_depthcharge],
Randall Spangler7318fd62013-11-21 12:16:58 -08001056 "chromiumos/platform/ec": [_run_checkpatch_ec,
Mike Frysingerdf980702013-08-22 22:25:22 -04001057 _check_change_has_branch_field],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001058 "chromiumos/platform/mosys": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001059 "chromiumos/platform/vboot_reference": [_check_change_has_branch_field],
Vadim Bendebury42052852014-10-06 16:02:38 -07001060 "chromiumos/third_party/coreboot": [_check_change_has_branch_field,
1061 _check_change_has_signoff_field,
Vadim Bendebury4bcd9fa2014-08-07 12:27:37 -07001062 _run_checkpatch_coreboot],
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001063 "chromiumos/third_party/flashrom": [_check_change_has_branch_field],
Mike Frysingerdf980702013-08-22 22:25:22 -04001064 "chromiumos/third_party/kernel": [_run_checkpatch, _kernel_configcheck],
1065 "chromiumos/third_party/kernel-next": [_run_checkpatch,
1066 _kernel_configcheck],
Vadim Bendebury203fbc22014-04-22 11:58:14 -07001067 "chromiumos/third_party/u-boot": [_run_checkpatch_no_tree],
Ryan Cui9b651632011-05-11 11:38:58 -07001068}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001069
Ryan Cui1562fb82011-05-09 11:01:31 -07001070
Ryan Cui9b651632011-05-11 11:38:58 -07001071# A dictionary of flags (keys) that can appear in the config file, and the hook
1072# that the flag disables (value)
1073_DISABLE_FLAGS = {
1074 'stray_whitespace_check': _check_no_stray_whitespace,
1075 'long_line_check': _check_no_long_lines,
1076 'cros_license_check': _check_license,
1077 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001078 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001079 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001080 'bug_field_check': _check_change_has_bug_field,
1081 'test_field_check': _check_change_has_test_field,
Ryan Cui9b651632011-05-11 11:38:58 -07001082}
1083
1084
Jon Salz3ee59de2012-08-18 13:54:22 +08001085def _get_disabled_hooks(config):
Ryan Cui9b651632011-05-11 11:38:58 -07001086 """Returns a set of hooks disabled by the current project's config file.
1087
1088 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001089
1090 Args:
1091 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001092 """
1093 SECTION = 'Hook Overrides'
Jon Salz3ee59de2012-08-18 13:54:22 +08001094 if not config.has_section(SECTION):
1095 return set()
Ryan Cui9b651632011-05-11 11:38:58 -07001096
1097 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001098 for flag in config.options(SECTION):
Ryan Cui9b651632011-05-11 11:38:58 -07001099 try:
Mike Frysingerae409522014-02-01 03:16:11 -05001100 if not config.getboolean(SECTION, flag):
1101 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001102 except ValueError as e:
1103 msg = "Error parsing flag \'%s\' in %s file - " % (flag, _CONFIG_FILE)
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001104 print(msg + str(e))
Ryan Cui9b651632011-05-11 11:38:58 -07001105
1106 disabled_keys = set(_DISABLE_FLAGS.iterkeys()).intersection(disable_flags)
1107 return set([_DISABLE_FLAGS[key] for key in disabled_keys])
1108
1109
Jon Salz3ee59de2012-08-18 13:54:22 +08001110def _get_project_hook_scripts(config):
1111 """Returns a list of project-specific hook scripts.
1112
1113 Args:
1114 config: A ConfigParser for the project's config file.
1115 """
1116 SECTION = 'Hook Scripts'
1117 if not config.has_section(SECTION):
1118 return []
1119
1120 hook_names_values = config.items(SECTION)
1121 hook_names_values.sort(key=lambda x: x[0])
1122 return [x[1] for x in hook_names_values]
1123
1124
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001125def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001126 """Returns a list of hooks that need to be run for a project.
1127
1128 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001129
1130 Args:
1131 project: A string, name of the project.
1132 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001133 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001134 config = ConfigParser.RawConfigParser()
1135 try:
1136 config.read(_CONFIG_FILE)
1137 except ConfigParser.Error:
1138 # Just use an empty config file
1139 config = ConfigParser.RawConfigParser()
1140
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001141 if presubmit:
1142 hook_list = _COMMON_HOOKS
1143 else:
1144 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1145
Jon Salz3ee59de2012-08-18 13:54:22 +08001146 disabled_hooks = _get_disabled_hooks(config)
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001147 hooks = [hook for hook in hook_list if hook not in disabled_hooks]
Ryan Cui9b651632011-05-11 11:38:58 -07001148
1149 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001150 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1151 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001152
Jon Salz3ee59de2012-08-18 13:54:22 +08001153 for script in _get_project_hook_scripts(config):
1154 hooks.append(functools.partial(_run_project_hook_script, script))
1155
Ryan Cui9b651632011-05-11 11:38:58 -07001156 return hooks
1157
1158
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001159def _run_project_hooks(project, proj_dir=None,
1160 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001161 """For each project run its project specific hook from the hooks dictionary.
1162
1163 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001164 project: The name of project to run hooks for.
1165 proj_dir: If non-None, this is the directory the project is in. If None,
1166 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001167 commit_list: A list of commits to run hooks against. If None or empty list
1168 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001169 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001170
1171 Returns:
1172 Boolean value of whether any errors were ecountered while running the hooks.
1173 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001174 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001175 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1176 if len(proj_dirs) == 0:
1177 print('%s cannot be found.' % project, file=sys.stderr)
1178 print('Please specify a valid project.', file=sys.stderr)
1179 return True
1180 if len(proj_dirs) > 1:
1181 print('%s is associated with multiple directories.' % project,
1182 file=sys.stderr)
1183 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1184 return True
1185 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001186
Ryan Cuiec4d6332011-05-02 14:15:25 -07001187 pwd = os.getcwd()
1188 # hooks assume they are run from the root of the project
1189 os.chdir(proj_dir)
1190
Doug Anderson14749562013-06-26 13:38:29 -07001191 if not commit_list:
1192 try:
1193 commit_list = _get_commits()
1194 except VerifyException as e:
1195 PrintErrorForProject(project, HookFailure(str(e)))
1196 os.chdir(pwd)
1197 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001198
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001199 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001200 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001201 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001202 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001203 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001204 hook_error = hook(project, commit)
1205 if hook_error:
1206 error_list.append(hook_error)
1207 error_found = True
1208 if error_list:
1209 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1210 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001211
Ryan Cuiec4d6332011-05-02 14:15:25 -07001212 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001213 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001214
Mike Frysingerae409522014-02-01 03:16:11 -05001215
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001216# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001217
Ryan Cui1562fb82011-05-09 11:01:31 -07001218
Mike Frysingerae409522014-02-01 03:16:11 -05001219def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001220 """Main function invoked directly by repo.
1221
1222 This function will exit directly upon error so that repo doesn't print some
1223 obscure error message.
1224
1225 Args:
1226 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001227 worktree_list: A list of directories. It should be the same length as
1228 project_list, so that each entry in project_list matches with a directory
1229 in worktree_list. If None, we will attempt to calculate the directories
1230 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001231 kwargs: Leave this here for forward-compatibility.
1232 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001233 found_error = False
David James2edd9002013-10-11 14:09:19 -07001234 if not worktree_list:
1235 worktree_list = [None] * len(project_list)
1236 for project, worktree in zip(project_list, worktree_list):
1237 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001238 found_error = True
1239
Mike Frysingerae409522014-02-01 03:16:11 -05001240 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001241 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001242 '- To disable some source style checks, and for other hints, see '
1243 '<checkout_dir>/src/repohooks/README\n'
1244 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001245 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001246 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001247
Ryan Cui1562fb82011-05-09 11:01:31 -07001248
Doug Anderson44a644f2011-11-02 10:37:37 -07001249def _identify_project(path):
1250 """Identify the repo project associated with the given path.
1251
1252 Returns:
1253 A string indicating what project is associated with the path passed in or
1254 a blank string upon failure.
1255 """
1256 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
1257 stderr=subprocess.PIPE, cwd=path).strip()
1258
1259
1260def direct_main(args, verbose=False):
1261 """Run hooks directly (outside of the context of repo).
1262
1263 # Setup for doctests below.
1264 # ...note that some tests assume that running pre-upload on this CWD is fine.
1265 # TODO: Use mock and actually mock out _run_project_hooks() for tests.
1266 >>> mydir = os.path.dirname(os.path.abspath(__file__))
1267 >>> olddir = os.getcwd()
1268
1269 # OK to run w/ no arugments; will run with CWD.
1270 >>> os.chdir(mydir)
1271 >>> direct_main(['prog_name'], verbose=True)
1272 Running hooks on chromiumos/repohooks
1273 0
1274 >>> os.chdir(olddir)
1275
1276 # Run specifying a dir
1277 >>> direct_main(['prog_name', '--dir=%s' % mydir], verbose=True)
1278 Running hooks on chromiumos/repohooks
1279 0
1280
1281 # Not a problem to use a bogus project; we'll just get default settings.
1282 >>> direct_main(['prog_name', '--dir=%s' % mydir, '--project=X'],verbose=True)
1283 Running hooks on X
1284 0
1285
1286 # Run with project but no dir
1287 >>> os.chdir(mydir)
1288 >>> direct_main(['prog_name', '--project=X'], verbose=True)
1289 Running hooks on X
1290 0
1291 >>> os.chdir(olddir)
1292
1293 # Try with a non-git CWD
1294 >>> os.chdir('/tmp')
1295 >>> direct_main(['prog_name'])
1296 Traceback (most recent call last):
1297 ...
1298 BadInvocation: The current directory is not part of a git project.
1299
1300 # Check various bad arguments...
1301 >>> direct_main(['prog_name', 'bogus'])
1302 Traceback (most recent call last):
1303 ...
1304 BadInvocation: Unexpected arguments: bogus
1305 >>> direct_main(['prog_name', '--project=bogus', '--dir=bogusdir'])
1306 Traceback (most recent call last):
1307 ...
1308 BadInvocation: Invalid dir: bogusdir
1309 >>> direct_main(['prog_name', '--project=bogus', '--dir=/tmp'])
1310 Traceback (most recent call last):
1311 ...
1312 BadInvocation: Not a git directory: /tmp
1313
1314 Args:
1315 args: The value of sys.argv
Mike Frysingerae409522014-02-01 03:16:11 -05001316 verbose: Log verbose info while running
Doug Anderson44a644f2011-11-02 10:37:37 -07001317
1318 Returns:
1319 0 if no pre-upload failures, 1 if failures.
1320
1321 Raises:
1322 BadInvocation: On some types of invocation errors.
1323 """
1324 desc = 'Run Chromium OS pre-upload hooks on changes compared to upstream.'
1325 parser = optparse.OptionParser(description=desc)
1326
1327 parser.add_option('--dir', default=None,
1328 help='The directory that the project lives in. If not '
1329 'specified, use the git project root based on the cwd.')
1330 parser.add_option('--project', default=None,
1331 help='The project repo path; this can affect how the hooks '
1332 'get run, since some hooks are project-specific. For '
1333 'chromite this is chromiumos/chromite. If not specified, '
1334 'the repo tool will be used to figure this out based on '
1335 'the dir.')
Doug Anderson14749562013-06-26 13:38:29 -07001336 parser.add_option('--rerun-since', default=None,
1337 help='Rerun hooks on old commits since the given date. '
1338 'The date should match git log\'s concept of a date. '
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001339 'e.g. 2012-06-20. This option is mutually exclusive '
1340 'with --pre-submit.')
1341 parser.add_option('--pre-submit', action="store_true",
1342 help='Run the check against the pending commit. '
1343 'This option should be used at the \'git commit\' '
1344 'phase as opposed to \'repo upload\'. This option '
1345 'is mutually exclusive with --rerun-since.')
Doug Anderson14749562013-06-26 13:38:29 -07001346
1347 parser.usage = "pre-upload.py [options] [commits]"
Doug Anderson44a644f2011-11-02 10:37:37 -07001348
1349 opts, args = parser.parse_args(args[1:])
1350
Doug Anderson14749562013-06-26 13:38:29 -07001351 if opts.rerun_since:
1352 if args:
1353 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
1354 ' '.join(args))
1355
1356 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1357 all_commits = _run_command(cmd).splitlines()
1358 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1359
1360 # Eliminate chrome-bot commits but keep ordering the same...
1361 bot_commits = set(bot_commits)
1362 args = [c for c in all_commits if c not in bot_commits]
1363
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001364 if opts.pre_submit:
1365 raise BadInvocation('rerun-since and pre-submit can not be '
1366 'used together')
1367 if opts.pre_submit:
1368 if args:
1369 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
1370 ' '.join(args))
1371 args = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001372
1373 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1374 # project from CWD
1375 if opts.dir is None:
1376 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1377 stderr=subprocess.PIPE).strip()
1378 if not git_dir:
1379 raise BadInvocation('The current directory is not part of a git project.')
1380 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1381 elif not os.path.isdir(opts.dir):
1382 raise BadInvocation('Invalid dir: %s' % opts.dir)
1383 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1384 raise BadInvocation('Not a git directory: %s' % opts.dir)
1385
1386 # Identify the project if it wasn't specified; this _requires_ the repo
1387 # tool to be installed and for the project to be part of a repo checkout.
1388 if not opts.project:
1389 opts.project = _identify_project(opts.dir)
1390 if not opts.project:
1391 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1392
1393 if verbose:
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001394 print("Running hooks on %s" % (opts.project))
Doug Anderson44a644f2011-11-02 10:37:37 -07001395
Doug Anderson14749562013-06-26 13:38:29 -07001396 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001397 commit_list=args,
1398 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001399 if found_error:
1400 return 1
1401 return 0
1402
1403
1404def _test():
1405 """Run any built-in tests."""
1406 import doctest
1407 doctest.testmod()
1408
1409
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001410if __name__ == '__main__':
Doug Anderson44a644f2011-11-02 10:37:37 -07001411 if sys.argv[1:2] == ["--test"]:
1412 _test()
1413 exit_code = 0
1414 else:
1415 prog_name = os.path.basename(sys.argv[0])
1416 try:
1417 exit_code = direct_main(sys.argv)
Bertrand SIMONNET6ec83f92014-05-30 10:42:34 -07001418 except BadInvocation, err:
1419 print("%s: %s" % (prog_name, str(err)), file=sys.stderr)
Doug Anderson44a644f2011-11-02 10:37:37 -07001420 exit_code = 1
1421 sys.exit(exit_code)