blob: 611aa74a6bc85d821e121ba6a52b945e65c07b7b [file] [log] [blame]
Daniel Erat9d203ff2015-02-17 10:12:21 -07001#!/usr/bin/python2
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
Daniel Erate3ea3fc2015-02-13 15:27:52 -070014import fnmatch
Jon Salz3ee59de2012-08-18 13:54:22 +080015import functools
Dale Curtis2975c432011-05-03 17:25:20 -070016import json
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
Peter Ammon811f6702014-06-12 15:45:38 -070020import stat
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050021import subprocess
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
Mike Frysinger66142932014-12-18 14:55:57 -050032from chromite.lib import commandline
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050033from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070034from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070035from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040036from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070037
Vadim Bendebury2b62d742014-06-22 13:14:51 -070038PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070039
40COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050041 # C++ and friends
42 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
43 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
44 # Scripts
45 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
46 # No extension at all, note that ALL CAPS files are black listed in
47 # COMMON_EXCLUDED_LIST below.
48 r"(^|.*[\\\/])[^.]+$",
49 # Other
50 r".*\.java$", r".*\.mk$", r".*\.am$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070051]
52
Ryan Cui1562fb82011-05-09 11:01:31 -070053
Ryan Cuiec4d6332011-05-02 14:15:25 -070054COMMON_EXCLUDED_PATHS = [
Daniel Erate3ea3fc2015-02-13 15:27:52 -070055 # Avoid doing source file checks for kernel.
Mike Frysingerae409522014-02-01 03:16:11 -050056 r"/src/third_party/kernel/",
57 r"/src/third_party/kernel-next/",
58 r"/src/third_party/ktop/",
59 r"/src/third_party/punybench/",
60 r".*\bexperimental[\\\/].*",
61 r".*\b[A-Z0-9_]{2,}$",
62 r".*[\\\/]debian[\\\/]rules$",
Daniel Erate3ea3fc2015-02-13 15:27:52 -070063
64 # For ebuild trees, ignore any caches and manifest data.
Mike Frysingerae409522014-02-01 03:16:11 -050065 r".*/Manifest$",
66 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070067
Daniel Erate3ea3fc2015-02-13 15:27:52 -070068 # Ignore profiles data (like overlay-tegra2/profiles).
Mike Frysinger94a670c2014-09-19 12:46:26 -040069 r"(^|.*/)overlay-.*/profiles/.*",
Mike Frysinger98638102014-08-28 00:15:08 -040070 r"^profiles/.*$",
71
Daniel Erate3ea3fc2015-02-13 15:27:52 -070072 # Ignore minified js and jquery.
Mike Frysingerae409522014-02-01 03:16:11 -050073 r".*\.min\.js",
74 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050075
76 # Ignore license files as the content is often taken verbatim.
Daniel Erate3ea3fc2015-02-13 15:27:52 -070077 r".*/licenses/.*",
Ryan Cuiec4d6332011-05-02 14:15:25 -070078]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070079
Ryan Cui1562fb82011-05-09 11:01:31 -070080
Ryan Cui9b651632011-05-11 11:38:58 -070081_CONFIG_FILE = 'PRESUBMIT.cfg'
82
83
Daniel Erate3ea3fc2015-02-13 15:27:52 -070084# File containing wildcards, one per line, matching files that should be
85# excluded from presubmit checks. Lines beginning with '#' are ignored.
86_IGNORE_FILE = '.presubmitignore'
87
88
Doug Anderson44a644f2011-11-02 10:37:37 -070089# Exceptions
90
91
92class BadInvocation(Exception):
93 """An Exception indicating a bad invocation of the program."""
94 pass
95
96
Ryan Cui1562fb82011-05-09 11:01:31 -070097# General Helpers
98
Sean Paulba01d402011-05-05 11:36:23 -040099
Doug Anderson44a644f2011-11-02 10:37:37 -0700100def _run_command(cmd, cwd=None, stderr=None):
101 """Executes the passed in command and returns raw stdout output.
102
103 Args:
104 cmd: The command to run; should be a list of strings.
105 cwd: The directory to switch to for running the command.
106 stderr: Can be one of None (print stderr to console), subprocess.STDOUT
107 (combine stderr with stdout), or subprocess.PIPE (ignore stderr).
108
109 Returns:
110 The standard out from the process.
111 """
112 p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=stderr, cwd=cwd)
113 return p.communicate()[0]
Ryan Cui72834d12011-05-05 14:51:33 -0700114
Ryan Cui1562fb82011-05-09 11:01:31 -0700115
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700116def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700117 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700118 if __name__ == '__main__':
119 # Works when file is run on its own (__file__ is defined)...
120 return os.path.abspath(os.path.dirname(__file__))
121 else:
122 # We need to do this when we're run through repo. Since repo executes
123 # us with execfile(), we don't get __file__ defined.
124 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
125 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700126
Ryan Cui1562fb82011-05-09 11:01:31 -0700127
Ryan Cuiec4d6332011-05-02 14:15:25 -0700128def _match_regex_list(subject, expressions):
129 """Try to match a list of regular expressions to a string.
130
131 Args:
132 subject: The string to match regexes on
133 expressions: A list of regular expressions to check for matches with.
134
135 Returns:
136 Whether the passed in subject matches any of the passed in regexes.
137 """
138 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500139 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700140 return True
141 return False
142
Ryan Cui1562fb82011-05-09 11:01:31 -0700143
Mike Frysingerae409522014-02-01 03:16:11 -0500144def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700145 """Filter out files based on the conditions passed in.
146
147 Args:
148 files: list of filepaths to filter
149 include_list: list of regex that when matched with a file path will cause it
150 to be added to the output list unless the file is also matched with a
151 regex in the exclude_list.
152 exclude_list: list of regex that when matched with a file will prevent it
153 from being added to the output list, even if it is also matched with a
154 regex in the include_list.
155
156 Returns:
157 A list of filepaths that contain files matched in the include_list and not
158 in the exclude_list.
159 """
160 filtered = []
161 for f in files:
162 if (_match_regex_list(f, include_list) and
163 not _match_regex_list(f, exclude_list)):
164 filtered.append(f)
165 return filtered
166
Ryan Cuiec4d6332011-05-02 14:15:25 -0700167
168# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700169
170
Ryan Cui4725d952011-05-05 15:41:19 -0700171def _get_upstream_branch():
172 """Returns the upstream tracking branch of the current branch.
173
174 Raises:
175 Error if there is no tracking branch
176 """
177 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
178 current_branch = current_branch.replace('refs/heads/', '')
179 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700180 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700181
182 cfg_option = 'branch.' + current_branch + '.%s'
183 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
184 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
185 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700186 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700187
188 return full_upstream.replace('heads', 'remotes/' + remote)
189
Ryan Cui1562fb82011-05-09 11:01:31 -0700190
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700191def _get_patch(commit):
192 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700193 if commit == PRE_SUBMIT:
194 return _run_command(['git', 'diff', '--cached', 'HEAD'])
195 else:
196 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700197
Ryan Cui1562fb82011-05-09 11:01:31 -0700198
Jon Salz98255932012-08-18 14:48:02 +0800199def _try_utf8_decode(data):
200 """Attempts to decode a string as UTF-8.
201
202 Returns:
203 The decoded Unicode object, or the original string if parsing fails.
204 """
205 try:
206 return unicode(data, 'utf-8', 'strict')
207 except UnicodeDecodeError:
208 return data
209
210
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500211def _get_file_content(path, commit):
212 """Returns the content of a file at a specific commit.
213
214 We can't rely on the file as it exists in the filesystem as people might be
215 uploading a series of changes which modifies the file multiple times.
216
217 Note: The "content" of a symlink is just the target. So if you're expecting
218 a full file, you should check that first. One way to detect is that the
219 content will not have any newlines.
220 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700221 if commit == PRE_SUBMIT:
222 return _run_command(['git', 'diff', 'HEAD', path])
223 else:
224 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500225
226
Mike Frysingerae409522014-02-01 03:16:11 -0500227def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700228 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700229 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff']
230 if commit == PRE_SUBMIT:
231 command += ['HEAD', path]
232 else:
233 command += [commit, path]
234 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700235
236 new_lines = []
237 line_num = 0
238 for line in output.splitlines():
239 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
240 if m:
241 line_num = int(m.groups(1)[0])
242 continue
243 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800244 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700245 if not line.startswith('-'):
246 line_num += 1
247 return new_lines
248
Ryan Cui1562fb82011-05-09 11:01:31 -0700249
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700250def _get_ignore_wildcards(directory, cache):
251 """Get wildcards listed in a directory's _IGNORE_FILE.
252
253 Args:
254 directory: A string containing a directory path.
255 cache: A dictionary (opaque to caller) caching previously-read wildcards.
256
257 Returns:
258 A list of wildcards from _IGNORE_FILE or an empty list if _IGNORE_FILE
259 wasn't present.
260 """
261 # In the cache, keys are directories and values are lists of wildcards from
262 # _IGNORE_FILE within those directories (and empty if no file was present).
263 if directory not in cache:
264 wildcards = []
265 dotfile_path = os.path.join(directory, _IGNORE_FILE)
266 if os.path.exists(dotfile_path):
267 # TODO(derat): Consider using _get_file_content() to get the file as of
268 # this commit instead of the on-disk version. This may have a noticeable
269 # performance impact, as each call to _get_file_content() runs git.
270 with open(dotfile_path, 'r') as dotfile:
271 for line in dotfile.readlines():
272 line = line.strip()
273 if line.startswith('#'):
274 continue
275 if line.endswith('/'):
276 line += '*'
277 wildcards.append(line)
278 cache[directory] = wildcards
279
280 return cache[directory]
281
282
283def _path_is_ignored(path, cache):
284 """Check whether a path is ignored by _IGNORE_FILE.
285
286 Args:
287 path: A string containing a path.
288 cache: A dictionary (opaque to caller) caching previously-read wildcards.
289
290 Returns:
291 True if a file named _IGNORE_FILE in one of the passed-in path's parent
292 directories contains a wildcard matching the path.
293 """
294 # Skip ignore files.
295 if os.path.basename(path) == _IGNORE_FILE:
296 return True
297
298 path = os.path.abspath(path)
299 base = os.getcwd()
300
301 prefix = os.path.dirname(path)
302 while prefix.startswith(base):
303 rel_path = path[len(prefix) + 1:]
304 for wildcard in _get_ignore_wildcards(prefix, cache):
305 if fnmatch.fnmatch(rel_path, wildcard):
306 return True
307 prefix = os.path.dirname(prefix)
308
309 return False
310
311
Mike Frysinger292b45d2014-11-25 01:17:10 -0500312def _get_affected_files(commit, include_deletes=False, relative=False,
313 include_symlinks=False, include_adds=True,
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700314 full_details=False, use_ignore_files=True):
Peter Ammon811f6702014-06-12 15:45:38 -0700315 """Returns list of file paths that were modified/added, excluding symlinks.
316
317 Args:
318 commit: The commit
319 include_deletes: If true, we'll include deleted files in the result
320 relative: Whether to return relative or full paths to files
Mike Frysinger292b45d2014-11-25 01:17:10 -0500321 include_symlinks: If true, we'll include symlinks in the result
322 include_adds: If true, we'll include new files in the result
323 full_details: If False, return filenames, else return structured results.
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700324 use_ignore_files: Whether we ignore files matched by _IGNORE_FILE files.
Peter Ammon811f6702014-06-12 15:45:38 -0700325
326 Returns:
327 A list of modified/added (and perhaps deleted) files
328 """
Mike Frysinger292b45d2014-11-25 01:17:10 -0500329 if not relative and full_details:
330 raise ValueError('full_details only supports relative paths currently')
331
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700332 if commit == PRE_SUBMIT:
333 return _run_command(['git', 'diff-index', '--cached',
334 '--name-only', 'HEAD']).split()
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500335
336 path = os.getcwd()
337 files = git.RawDiff(path, '%s^!' % commit)
338
339 # Filter out symlinks.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500340 if not include_symlinks:
341 files = [x for x in files if not stat.S_ISLNK(int(x.dst_mode, 8))]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500342
343 if not include_deletes:
344 files = [x for x in files if x.status != 'D']
345
Mike Frysinger292b45d2014-11-25 01:17:10 -0500346 if not include_adds:
347 files = [x for x in files if x.status != 'A']
348
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700349 if use_ignore_files:
350 cache = {}
351 is_ignored = lambda x: _path_is_ignored(x.dst_file or x.src_file, cache)
352 files = [x for x in files if not is_ignored(x)]
353
Mike Frysinger292b45d2014-11-25 01:17:10 -0500354 if full_details:
355 # Caller wants the raw objects to parse status/etc... themselves.
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500356 return files
357 else:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500358 # Caller only cares about filenames.
359 files = [x.dst_file if x.dst_file else x.src_file for x in files]
360 if relative:
361 return files
362 else:
363 return [os.path.join(path, x) for x in files]
Peter Ammon811f6702014-06-12 15:45:38 -0700364
365
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700366def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700367 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700368 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700369 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700370
Ryan Cui1562fb82011-05-09 11:01:31 -0700371
Ryan Cuiec4d6332011-05-02 14:15:25 -0700372def _get_commit_desc(commit):
373 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700374 if commit == PRE_SUBMIT:
375 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400376 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700377
378
379# Common Hooks
380
Ryan Cui1562fb82011-05-09 11:01:31 -0700381
Mike Frysingerae409522014-02-01 03:16:11 -0500382def _check_no_long_lines(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -0500383 """Checks there are no lines longer than MAX_LEN in any of the text files."""
384
Ryan Cuiec4d6332011-05-02 14:15:25 -0700385 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800386 SKIP_REGEXP = re.compile('|'.join([
387 r'https?://',
388 r'^#\s*(define|include|import|pragma|if|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700389
390 errors = []
391 files = _filter_files(_get_affected_files(commit),
392 COMMON_INCLUDED_PATHS,
393 COMMON_EXCLUDED_PATHS)
394
395 for afile in files:
396 for line_num, line in _get_file_diff(afile, commit):
397 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500398 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800399 continue
400
401 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
402 if len(errors) == 5: # Just show the first 5 errors.
403 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700404
405 if errors:
406 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700407 return HookFailure(msg, errors)
408
Ryan Cuiec4d6332011-05-02 14:15:25 -0700409
Mike Frysingerae409522014-02-01 03:16:11 -0500410def _check_no_stray_whitespace(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700411 """Checks that there is no stray whitespace at source lines end."""
412 errors = []
413 files = _filter_files(_get_affected_files(commit),
Mike Frysingerae409522014-02-01 03:16:11 -0500414 COMMON_INCLUDED_PATHS,
415 COMMON_EXCLUDED_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700416 for afile in files:
417 for line_num, line in _get_file_diff(afile, commit):
418 if line.rstrip() != line:
419 errors.append('%s, line %s' % (afile, line_num))
420 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700421 return HookFailure('Found line ending with white space in:', errors)
422
Ryan Cuiec4d6332011-05-02 14:15:25 -0700423
Mike Frysingerae409522014-02-01 03:16:11 -0500424def _check_no_tabs(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700425 """Checks there are no unexpanded tabs."""
426 TAB_OK_PATHS = [
Ryan Cui31e0c172011-05-04 21:00:45 -0700427 r"/src/third_party/u-boot/",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700428 r".*\.ebuild$",
429 r".*\.eclass$",
Elly Jones5ab34192011-11-15 14:57:06 -0500430 r".*/[M|m]akefile$",
431 r".*\.mk$"
Ryan Cuiec4d6332011-05-02 14:15:25 -0700432 ]
433
434 errors = []
435 files = _filter_files(_get_affected_files(commit),
436 COMMON_INCLUDED_PATHS,
437 COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
438
439 for afile in files:
440 for line_num, line in _get_file_diff(afile, commit):
441 if '\t' in line:
Mike Frysingerae409522014-02-01 03:16:11 -0500442 errors.append('%s, line %s' % (afile, line_num))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700443 if errors:
Ryan Cui1562fb82011-05-09 11:01:31 -0700444 return HookFailure('Found a tab character in:', errors)
445
Ryan Cuiec4d6332011-05-02 14:15:25 -0700446
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700447def _check_gofmt(_project, commit):
448 """Checks that Go files are formatted with gofmt."""
449 errors = []
450 files = _filter_files(_get_affected_files(commit, relative=True),
451 [r'\.go$'])
452
453 for gofile in files:
454 contents = _get_file_content(gofile, commit)
455 p = subprocess.Popen(['gofmt', '-l'],
456 stdin=subprocess.PIPE,
457 stdout=subprocess.PIPE,
458 stderr=subprocess.STDOUT)
459 output = p.communicate(contents)[0]
460 if output:
461 errors.append(gofile)
462 if errors:
463 return HookFailure('Files not formatted with gofmt:', errors)
464
465
Mike Frysingerae409522014-02-01 03:16:11 -0500466def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700467 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700468 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700469
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700470 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700471 msg = 'Changelist description needs TEST field (after first line)'
472 return HookFailure(msg)
473
Ryan Cuiec4d6332011-05-02 14:15:25 -0700474
Mike Frysingerae409522014-02-01 03:16:11 -0500475def _check_change_has_valid_cq_depend(_project, commit):
David Jamesc3b68b32013-04-03 09:17:03 -0700476 """Check for a correctly formatted CQ-DEPEND field in the commit message."""
477 msg = 'Changelist has invalid CQ-DEPEND target.'
478 example = 'Example: CQ-DEPEND=CL:1234, CL:2345'
479 try:
480 patch.GetPaladinDeps(_get_commit_desc(commit))
481 except ValueError as ex:
482 return HookFailure(msg, [example, str(ex)])
483
484
Mike Frysingerae409522014-02-01 03:16:11 -0500485def _check_change_has_bug_field(_project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700486 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700487 OLD_BUG_RE = r'\nBUG=.*chromium-os'
488 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
489 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
490 'the chromium tracker in your BUG= line now.')
491 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700492
Stefan Sauerd74ad562015-03-10 10:14:28 +0100493 BUG_RE = r'\nBUG=([Nn]one|(chrome-os-partner|chromium|brillo|b):\d+)'
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700494 if not re.search(BUG_RE, _get_commit_desc(commit)):
David McMahon8f6553e2011-06-10 15:46:36 -0700495 msg = ('Changelist description needs BUG field (after first line):\n'
Christopher Wileyc92721b2015-01-26 13:07:39 -0800496 'BUG=brillo:9999 (for Brillo tracker)\n'
David James5c0073d2013-04-03 08:48:52 -0700497 'BUG=chromium:9999 (for public tracker)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700498 'BUG=chrome-os-partner:9999 (for partner tracker)\n'
Stefan Sauerd74ad562015-03-10 10:14:28 +0100499 'BUG=b:9999 (for buganizer)\n'
David McMahon8f6553e2011-06-10 15:46:36 -0700500 'BUG=None')
Ryan Cui1562fb82011-05-09 11:01:31 -0700501 return HookFailure(msg)
502
Ryan Cuiec4d6332011-05-02 14:15:25 -0700503
Mike Frysinger292b45d2014-11-25 01:17:10 -0500504def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -0700505 """Check that we're not missing a revbump of an ebuild in the given commit.
506
507 If the given commit touches files in a directory that has ebuilds somewhere
508 up the directory hierarchy, it's very likely that we need an ebuild revbump
509 in order for those changes to take effect.
510
511 It's not totally trivial to detect a revbump, so at least detect that an
512 ebuild with a revision number in it was touched. This should handle the
513 common case where we use a symlink to do the revbump.
514
515 TODO: it would be nice to enhance this hook to:
516 * Handle cases where people revbump with a slightly different syntax. I see
517 one ebuild (puppy) that revbumps with _pN. This is a false positive.
518 * Catches cases where people aren't using symlinks for revbumps. If they
519 edit a revisioned file directly (and are expected to rename it for revbump)
520 we'll miss that. Perhaps we could detect that the file touched is a
521 symlink?
522
523 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
524 still better off than without this check.
525
526 Args:
527 project: The project to look at
528 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -0500529 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -0700530
531 Returns:
532 A HookFailure or None.
533 """
Mike Frysinger011af942014-01-17 16:12:22 -0500534 # If this is the portage-stable overlay, then ignore the check. It's rare
535 # that we're doing anything other than importing files from upstream, so
536 # forcing a rev bump makes no sense.
537 whitelist = (
538 'chromiumos/overlays/portage-stable',
539 )
540 if project in whitelist:
541 return None
542
Mike Frysinger292b45d2014-11-25 01:17:10 -0500543 def FinalName(obj):
544 # If the file is being deleted, then the dst_file is not set.
545 if obj.dst_file is None:
546 return obj.src_file
547 else:
548 return obj.dst_file
549
550 affected_path_objs = _get_affected_files(
551 commit, include_deletes=True, include_symlinks=True, relative=True,
552 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -0700553
554 # Don't yell about changes to whitelisted files...
Mike Frysinger011af942014-01-17 16:12:22 -0500555 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml')
Mike Frysinger292b45d2014-11-25 01:17:10 -0500556 affected_path_objs = [x for x in affected_path_objs
557 if os.path.basename(FinalName(x)) not in whitelist]
558 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -0700559 return None
560
561 # If we've touched any file named with a -rN.ebuild then we'll say we're
562 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500563 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
564 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700565 if touched_revved_ebuild:
566 return None
567
Mike Frysinger292b45d2014-11-25 01:17:10 -0500568 # If we're creating new ebuilds from scratch, then we don't need an uprev.
569 # Find all the dirs that new ebuilds and ignore their files/.
570 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
571 if FinalName(x).endswith('.ebuild') and x.status == 'A']
572 affected_path_objs = [obj for obj in affected_path_objs
573 if not any(FinalName(obj).startswith(x)
574 for x in ebuild_dirs)]
575 if not affected_path_objs:
576 return
577
Doug Anderson42b8a052013-06-26 10:45:36 -0700578 # We want to examine the current contents of all directories that are parents
579 # of files that were touched (up to the top of the project).
580 #
581 # ...note: we use the current directory contents even though it may have
582 # changed since the commit we're looking at. This is just a heuristic after
583 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500584 if project_top is None:
585 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -0700586 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -0500587 for obj in affected_path_objs:
588 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -0700589 while os.path.exists(path) and not os.path.samefile(path, project_top):
590 dirs_to_check.add(path)
591 path = os.path.dirname(path)
592
593 # Look through each directory. If it's got an ebuild in it then we'll
594 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -0800595 affected_paths = set(os.path.join(project_top, FinalName(x))
596 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700597 for dir_path in dirs_to_check:
598 contents = os.listdir(dir_path)
599 ebuilds = [os.path.join(dir_path, path)
600 for path in contents if path.endswith('.ebuild')]
601 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
602
603 # If the -9999.ebuild file was touched the bot will uprev for us.
604 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -0500605 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -0700606 continue
607
608 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500609 return HookFailure('Changelist probably needs a revbump of an ebuild, '
610 'or a -r1.ebuild symlink if this is a new ebuild:\n'
611 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -0700612
613 return None
614
615
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500616def _check_ebuild_eapi(project, commit):
617 """Make sure we have people use EAPI=4 or newer with custom ebuilds.
618
619 We want to get away from older EAPI's as it makes life confusing and they
620 have less builtin error checking.
621
622 Args:
623 project: The project to look at
624 commit: The commit to look at
625
626 Returns:
627 A HookFailure or None.
628 """
629 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500630 # that we're doing anything other than importing files from upstream, and
631 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500632 whitelist = (
633 'chromiumos/overlays/portage-stable',
634 )
635 if project in whitelist:
636 return None
637
638 BAD_EAPIS = ('0', '1', '2', '3')
639
640 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
641
642 ebuilds_re = [r'\.ebuild$']
643 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
644 ebuilds_re)
645 bad_ebuilds = []
646
647 for ebuild in ebuilds:
648 # If the ebuild does not specify an EAPI, it defaults to 0.
649 eapi = '0'
650
651 lines = _get_file_content(ebuild, commit).splitlines()
652 if len(lines) == 1:
653 # This is most likely a symlink, so skip it entirely.
654 continue
655
656 for line in lines:
657 m = get_eapi.match(line)
658 if m:
659 # Once we hit the first EAPI line in this ebuild, stop processing.
660 # The spec requires that there only be one and it be first, so
661 # checking all possible values is pointless. We also assume that
662 # it's "the" EAPI line and not something in the middle of a heredoc.
663 eapi = m.group(1)
664 break
665
666 if eapi in BAD_EAPIS:
667 bad_ebuilds.append((ebuild, eapi))
668
669 if bad_ebuilds:
670 # pylint: disable=C0301
671 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
672 # pylint: enable=C0301
673 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500674 'These ebuilds are using old EAPIs. If these are imported from\n'
675 'Gentoo, then you may ignore and upload once with the --no-verify\n'
676 'flag. Otherwise, please update to 4 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500677 '\t%s\n'
678 'See this guide for more details:\n%s\n' %
679 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
680
681
Mike Frysinger89bdb852014-02-01 05:26:26 -0500682def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500683 """Make sure we use the new style KEYWORDS when possible in ebuilds.
684
685 If an ebuild generally does not care about the arch it is running on, then
686 ebuilds should flag it with one of:
687 KEYWORDS="*" # A stable ebuild.
688 KEYWORDS="~*" # An unstable ebuild.
689 KEYWORDS="-* ..." # Is known to only work on specific arches.
690
691 Args:
692 project: The project to look at
693 commit: The commit to look at
694
695 Returns:
696 A HookFailure or None.
697 """
698 WHITELIST = set(('*', '-*', '~*'))
699
700 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
701
Mike Frysinger89bdb852014-02-01 05:26:26 -0500702 ebuilds_re = [r'\.ebuild$']
703 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
704 ebuilds_re)
705
Mike Frysinger8d42d742014-09-22 15:50:21 -0400706 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500707 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400708 # We get the full content rather than a diff as the latter does not work
709 # on new files (like when adding new ebuilds).
710 lines = _get_file_content(ebuild, commit).splitlines()
711 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500712 m = get_keywords.match(line)
713 if m:
714 keywords = set(m.group(1).split())
715 if not keywords or WHITELIST - keywords != WHITELIST:
716 continue
717
Mike Frysinger8d42d742014-09-22 15:50:21 -0400718 bad_ebuilds.append(ebuild)
719
720 if bad_ebuilds:
721 return HookFailure(
722 '%s\n'
723 'Please update KEYWORDS to use a glob:\n'
724 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
725 ' KEYWORDS="*"\n'
726 'If the ebuild should be marked unstable (normal for '
727 'cros-workon / 9999 ebuilds):\n'
728 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -0400729 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -0400730 'then use -* like so:\n'
731 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500732
733
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800734def _check_ebuild_licenses(_project, commit):
735 """Check if the LICENSE field in the ebuild is correct."""
736 affected_paths = _get_affected_files(commit)
737 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
738
739 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800740 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800741
742 for ebuild in touched_ebuilds:
743 # Skip virutal packages.
744 if ebuild.split('/')[-3] == 'virtual':
745 continue
746
747 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400748 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800749 except ValueError as e:
750 return HookFailure(e.message, [ebuild])
751
752 # Also ignore licenses ending with '?'
753 for license_type in [x for x in license_types
754 if x not in LICENSES_IGNORE and not x.endswith('?')]:
755 try:
Mike Frysinger2ec70ed2014-08-17 19:28:34 -0400756 licenses_lib.Licensing.FindLicenseType(license_type)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800757 except AssertionError as e:
758 return HookFailure(e.message, [ebuild])
759
760
Mike Frysingercd363c82014-02-01 05:20:18 -0500761def _check_ebuild_virtual_pv(project, commit):
762 """Enforce the virtual PV policies."""
763 # If this is the portage-stable overlay, then ignore the check.
764 # We want to import virtuals as-is from upstream Gentoo.
765 whitelist = (
766 'chromiumos/overlays/portage-stable',
767 )
768 if project in whitelist:
769 return None
770
771 # We assume the repo name is the same as the dir name on disk.
772 # It would be dumb to not have them match though.
773 project = os.path.basename(project)
774
775 is_variant = lambda x: x.startswith('overlay-variant-')
776 is_board = lambda x: x.startswith('overlay-')
777 is_private = lambda x: x.endswith('-private')
778
779 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
780
781 ebuilds_re = [r'\.ebuild$']
782 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
783 ebuilds_re)
784 bad_ebuilds = []
785
786 for ebuild in ebuilds:
787 m = get_pv.match(ebuild)
788 if m:
789 overlay = m.group(1)
790 if not overlay or not is_board(overlay):
791 overlay = project
792
793 pv = m.group(3).split('-', 1)[0]
794
795 if is_private(overlay):
796 want_pv = '3.5' if is_variant(overlay) else '3'
797 elif is_board(overlay):
798 want_pv = '2.5' if is_variant(overlay) else '2'
799 else:
800 want_pv = '1'
801
802 if pv != want_pv:
803 bad_ebuilds.append((ebuild, pv, want_pv))
804
805 if bad_ebuilds:
806 # pylint: disable=C0301
807 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
808 # pylint: enable=C0301
809 return HookFailure(
810 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
811 '\t%s\n'
812 'If this is an upstream Gentoo virtual, then you may ignore this\n'
813 'check (and re-run w/--no-verify). Otherwise, please see this\n'
814 'page for more details:\n%s\n' %
815 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
816 for x in bad_ebuilds]), url))
817
818
Daniel Erat9d203ff2015-02-17 10:12:21 -0700819def _check_portage_make_use_var(_project, commit):
820 """Verify that $USE is set correctly in make.conf and make.defaults."""
821 files = _filter_files(_get_affected_files(commit, relative=True),
822 [r'(^|/)make.(conf|defaults)$'])
823
824 errors = []
825 for path in files:
826 basename = os.path.basename(path)
827
828 # Has a USE= line already been encountered in this file?
829 saw_use = False
830
831 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
832 if not line.startswith('USE='):
833 continue
834
835 preserves_use = '${USE}' in line or '$USE' in line
836
837 if (basename == 'make.conf' or
838 (basename == 'make.defaults' and saw_use)) and not preserves_use:
839 errors.append('%s:%d: missing ${USE}' % (path, i))
840 elif basename == 'make.defaults' and not saw_use and preserves_use:
841 errors.append('%s:%d: ${USE} referenced in initial declaration' %
842 (path, i))
843
844 saw_use = True
845
846 if errors:
847 return HookFailure(
848 'One or more Portage make files appear to set USE incorrectly.\n'
849 '\n'
850 'All USE assignments in make.conf and all assignments after the\n'
851 'initial declaration in make.defaults should contain "${USE}" to\n'
852 'preserve previously-set flags.\n'
853 '\n'
854 'The initial USE declaration in make.defaults should not contain\n'
855 '"${USE}".\n',
856 errors)
857
858
Mike Frysingerae409522014-02-01 03:16:11 -0500859def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700860 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -0400861 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700862 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -0400863 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -0500864 if not m:
Ryan Cui1562fb82011-05-09 11:01:31 -0700865 return HookFailure('Change-Id must be in last paragraph of description.')
866
Mike Frysinger02b88bd2014-11-21 00:29:38 -0500867 # Allow s-o-b tags to follow it, but only those.
868 end = desc[m.end():].strip().splitlines()
869 if [x for x in end if not x.startswith('Signed-off-by: ')]:
870 return HookFailure('Only "Signed-off-by:" tags may follow the Change-Id.')
871
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -0700872
Mike Frysinger36b2ebc2014-10-31 14:02:03 -0400873def _check_commit_message_style(_project, commit):
874 """Verify that the commit message matches our style.
875
876 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
877 commit hooks.
878 """
879 desc = _get_commit_desc(commit)
880
881 # The first line should be by itself.
882 lines = desc.splitlines()
883 if len(lines) > 1 and lines[1]:
884 return HookFailure('The second line of the commit message must be blank.')
885
886 # The first line should be one sentence.
887 if '. ' in lines[0]:
888 return HookFailure('The first line cannot be more than one sentence.')
889
890 # The first line cannot be too long.
891 MAX_FIRST_LINE_LEN = 100
892 if len(lines[0]) > MAX_FIRST_LINE_LEN:
893 return HookFailure('The first line must be less than %i chars.' %
894 MAX_FIRST_LINE_LEN)
895
896
Mike Frysingerae409522014-02-01 03:16:11 -0500897def _check_license(_project, commit):
Mike Frysinger98638102014-08-28 00:15:08 -0400898 """Verifies the license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700899
Mike Frysinger98638102014-08-28 00:15:08 -0400900 Should be following the spec:
901 http://dev.chromium.org/developers/coding-style#TOC-File-headers
902 """
903 # For older years, be a bit more flexible as our policy says leave them be.
904 LICENSE_HEADER = (
905 r'.* Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -0500906 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400907 r'.* Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -0500908 r'be\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400909 r'.* found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -0500910 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400911 )
912 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
913
914 # For newer years, be stricter.
915 COPYRIGHT_LINE = (
916 r'.* Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -0500917 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -0400918 )
919 copyright_re = re.compile(COPYRIGHT_LINE)
920
921 bad_files = []
922 bad_copyright_files = []
923 files = _filter_files(_get_affected_files(commit, relative=True),
924 COMMON_INCLUDED_PATHS,
925 COMMON_EXCLUDED_PATHS)
926
927 for f in files:
928 contents = _get_file_content(f, commit)
929 if not contents:
930 # Ignore empty files.
931 continue
932
933 if not license_re.search(contents):
934 bad_files.append(f)
935 elif copyright_re.search(contents):
936 bad_copyright_files.append(f)
937
938 if bad_files:
939 msg = '%s:\n%s\n%s' % (
940 'License must match', license_re.pattern,
941 'Found a bad header in these files:')
942 return HookFailure(msg, bad_files)
943
944 if bad_copyright_files:
945 msg = 'Do not use (c) in copyright headers in new files:'
946 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700947
948
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400949def _check_layout_conf(_project, commit):
950 """Verifies the metadata/layout.conf file."""
951 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400952 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400953 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -0400954 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400955
Mike Frysinger94a670c2014-09-19 12:46:26 -0400956 # Handle multiple overlays in a single commit (like the public tree).
957 for f in _get_affected_files(commit, relative=True):
958 if f.endswith(repo_name):
959 repo_names.append(f)
960 elif f.endswith(layout_path):
961 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400962
963 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -0400964 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400965 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -0400966 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400967
Mike Frysinger94a670c2014-09-19 12:46:26 -0400968 # Gather all the errors in one pass so we show one full message.
969 all_errors = {}
970 for layout_path in layout_paths:
971 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400972
Mike Frysinger94a670c2014-09-19 12:46:26 -0400973 # Make sure the config file is sorted.
974 data = [x for x in _get_file_content(layout_path, commit).splitlines()
975 if x and x[0] != '#']
976 if sorted(data) != data:
977 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400978
Mike Frysinger94a670c2014-09-19 12:46:26 -0400979 # Require people to set specific values all the time.
980 settings = (
981 # TODO: Enable this for everyone. http://crbug.com/408038
982 #('fast caching', 'cache-format = md5-dict'),
983 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -0500984 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
985 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -0400986 )
987 for reason, line in settings:
988 if line not in data:
989 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400990
Mike Frysinger94a670c2014-09-19 12:46:26 -0400991 # Require one of these settings.
992 if ('use-manifests = true' not in data and
993 'use-manifests = strict' not in data):
994 errors += ['enable file checking with: use-manifests = true']
Mike Frysinger998c2cc2014-08-27 05:20:23 -0400995
Mike Frysinger94a670c2014-09-19 12:46:26 -0400996 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -0400997 for line in data:
998 if line.startswith('repo-name = '):
999 break
1000 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001001 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001002
Mike Frysinger94a670c2014-09-19 12:46:26 -04001003 # Summarize all the errors we saw (if any).
1004 lines = ''
1005 for layout_path, errors in all_errors.items():
1006 if errors:
1007 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1008 if lines:
1009 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1010 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001011
1012
Ryan Cuiec4d6332011-05-02 14:15:25 -07001013# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001014
Ryan Cui1562fb82011-05-09 11:01:31 -07001015
Mike Frysingerae409522014-02-01 03:16:11 -05001016def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001017 """Runs checkpatch.pl on the given project"""
1018 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001019 options = list(options)
1020 if commit == PRE_SUBMIT:
1021 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1022 # this case.
1023 options.append('--ignore=MISSING_SIGN_OFF')
1024 cmd = ['%s/checkpatch.pl' % hooks_dir] + options + ['-']
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001025 p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -07001026 output = p.communicate(_get_patch(commit))[0]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001027 if p.returncode:
Ryan Cui1562fb82011-05-09 11:01:31 -07001028 return HookFailure('checkpatch.pl errors/warnings\n\n' + output)
Ryan Cuiec4d6332011-05-02 14:15:25 -07001029
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001030
Mike Frysingerae409522014-02-01 03:16:11 -05001031def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001032 """Makes sure kernel config changes are not mixed with code changes"""
1033 files = _get_affected_files(commit)
1034 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
1035 return HookFailure('Changes to chromeos/config/ and regular files must '
1036 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -07001037
Mike Frysingerae409522014-02-01 03:16:11 -05001038
1039def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001040 """Checks that all JSON files are syntactically valid."""
Dale Curtisa039cfd2011-05-04 12:01:05 -07001041 for f in _filter_files(_get_affected_files(commit), [r'.*\.json']):
Dale Curtis2975c432011-05-03 17:25:20 -07001042 try:
1043 json.load(open(f))
1044 except Exception, e:
Ryan Cui1562fb82011-05-09 11:01:31 -07001045 return HookFailure('Invalid JSON in %s: %s' % (f, e))
Dale Curtis2975c432011-05-03 17:25:20 -07001046
1047
Mike Frysingerae409522014-02-01 03:16:11 -05001048def _check_manifests(_project, commit):
Mike Frysinger52b537e2013-08-22 22:59:53 -04001049 """Make sure Manifest files only have DIST lines"""
1050 paths = []
1051
1052 for path in _get_affected_files(commit):
1053 if os.path.basename(path) != 'Manifest':
1054 continue
1055 if not os.path.exists(path):
1056 continue
1057
1058 with open(path, 'r') as f:
1059 for line in f.readlines():
1060 if not line.startswith('DIST '):
1061 paths.append(path)
1062 break
1063
1064 if paths:
1065 return HookFailure('Please remove lines that do not start with DIST:\n%s' %
1066 ('\n'.join(paths),))
1067
1068
Mike Frysingerae409522014-02-01 03:16:11 -05001069def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001070 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001071 if commit == PRE_SUBMIT:
1072 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001073 BRANCH_RE = r'\nBRANCH=\S+'
1074
1075 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1076 msg = ('Changelist description needs BRANCH field (after first line)\n'
1077 'E.g. BRANCH=none or BRANCH=link,snow')
1078 return HookFailure(msg)
1079
1080
Mike Frysingerae409522014-02-01 03:16:11 -05001081def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001082 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001083 if commit == PRE_SUBMIT:
1084 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001085 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1086
1087 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1088 msg = ('Changelist description needs Signed-off-by: field\n'
1089 'E.g. Signed-off-by: My Name <me@chromium.org>')
1090 return HookFailure(msg)
1091
1092
Jon Salz3ee59de2012-08-18 13:54:22 +08001093def _run_project_hook_script(script, project, commit):
1094 """Runs a project hook script.
1095
1096 The script is run with the following environment variables set:
1097 PRESUBMIT_PROJECT: The affected project
1098 PRESUBMIT_COMMIT: The affected commit
1099 PRESUBMIT_FILES: A newline-separated list of affected files
1100
1101 The script is considered to fail if the exit code is non-zero. It should
1102 write an error message to stdout.
1103 """
1104 env = dict(os.environ)
1105 env['PRESUBMIT_PROJECT'] = project
1106 env['PRESUBMIT_COMMIT'] = commit
1107
1108 # Put affected files in an environment variable
1109 files = _get_affected_files(commit)
1110 env['PRESUBMIT_FILES'] = '\n'.join(files)
1111
1112 process = subprocess.Popen(script, env=env, shell=True,
1113 stdin=open(os.devnull),
Jon Salz7b618af2012-08-31 06:03:16 +08001114 stdout=subprocess.PIPE,
1115 stderr=subprocess.STDOUT)
Jon Salz3ee59de2012-08-18 13:54:22 +08001116 stdout, _ = process.communicate()
1117 if process.wait():
Jon Salz7b618af2012-08-31 06:03:16 +08001118 if stdout:
1119 stdout = re.sub('(?m)^', ' ', stdout)
1120 return HookFailure('Hook script "%s" failed with code %d%s' %
Jon Salz3ee59de2012-08-18 13:54:22 +08001121 (script, process.returncode,
1122 ':\n' + stdout if stdout else ''))
1123
1124
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001125def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05001126 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001127
1128 files = _get_affected_files(commit, relative=True)
1129 prefix = os.path.commonprefix(files)
1130 prefix = os.path.dirname(prefix)
1131
1132 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07001133 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001134 return
1135
1136 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07001137
1138 # The common files may all be within a subdirectory of the main project
1139 # directory, so walk up the tree until we find an alias file.
1140 # _get_affected_files() should return relative paths, but check against '/' to
1141 # ensure that this loop terminates even if it receives an absolute path.
1142 while prefix and prefix != '/':
1143 alias_file = os.path.join(prefix, '.project_alias')
1144
1145 # If an alias exists, use it.
1146 if os.path.isfile(alias_file):
1147 project_name = osutils.ReadFile(alias_file).strip()
1148
1149 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001150
1151 if not _get_commit_desc(commit).startswith(project_name + ': '):
1152 return HookFailure('The commit title for changes affecting only %s'
1153 ' should start with \"%s: \"'
1154 % (project_name, project_name))
1155
1156
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001157# Base
1158
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001159# A list of hooks which are not project specific and check patch description
1160# (as opposed to patch body).
1161_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001162 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001163 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001164 _check_change_has_test_field,
1165 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001166 _check_commit_message_style,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001167]
1168
1169
1170# A list of hooks that are not project-specific
1171_COMMON_HOOKS = [
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001172 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001173 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001174 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001175 _check_ebuild_virtual_pv,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001176 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07001177 _check_gofmt,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001178 _check_layout_conf,
Ryan Cui9b651632011-05-11 11:38:58 -07001179 _check_license,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001180 _check_no_long_lines,
1181 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07001182 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001183 _check_portage_make_use_var,
Ryan Cui9b651632011-05-11 11:38:58 -07001184]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001185
Ryan Cui1562fb82011-05-09 11:01:31 -07001186
Ryan Cui9b651632011-05-11 11:38:58 -07001187# A dictionary of project-specific hooks(callbacks), indexed by project name.
1188# dict[project] = [callback1, callback2]
1189_PROJECT_SPECIFIC_HOOKS = {
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001190 "chromiumos/platform2": [_check_project_prefix],
Mike Frysingeraf891292015-03-25 19:46:53 -04001191 "chromiumos/third_party/kernel": [_kernel_configcheck],
1192 "chromiumos/third_party/kernel-next": [_kernel_configcheck],
Ryan Cui9b651632011-05-11 11:38:58 -07001193}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001194
Ryan Cui1562fb82011-05-09 11:01:31 -07001195
Ryan Cui9b651632011-05-11 11:38:58 -07001196# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04001197# that the flag controls (value).
1198_HOOK_FLAGS = {
Mike Frysingera7642f52015-03-25 18:31:42 -04001199 'checkpatch_check': _run_checkpatch,
Ryan Cui9b651632011-05-11 11:38:58 -07001200 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04001201 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07001202 'long_line_check': _check_no_long_lines,
1203 'cros_license_check': _check_license,
1204 'tab_check': _check_no_tabs,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001205 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001206 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001207 'bug_field_check': _check_change_has_bug_field,
1208 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07001209 'manifest_check': _check_manifests,
Ryan Cui9b651632011-05-11 11:38:58 -07001210}
1211
1212
Mike Frysinger3554bc92015-03-11 04:59:21 -04001213def _get_override_hooks(config):
1214 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001215
1216 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001217
1218 Args:
1219 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001220 """
1221 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001222 SECTION_OPTIONS = 'Hook Overrides Options'
Jon Salz3ee59de2012-08-18 13:54:22 +08001223 if not config.has_section(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001224 return set(), set()
Ryan Cui9b651632011-05-11 11:38:58 -07001225
Mike Frysinger3554bc92015-03-11 04:59:21 -04001226 valid_keys = set(_HOOK_FLAGS.iterkeys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001227 hooks = _HOOK_FLAGS.copy()
Mike Frysinger3554bc92015-03-11 04:59:21 -04001228
1229 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07001230 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001231 for flag in config.options(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001232 if flag not in valid_keys:
1233 raise ValueError('Error: unknown key "%s" in hook section of "%s"' %
1234 (flag, _CONFIG_FILE))
1235
Ryan Cui9b651632011-05-11 11:38:58 -07001236 try:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001237 enabled = config.getboolean(SECTION, flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001238 except ValueError as e:
Mike Frysinger3554bc92015-03-11 04:59:21 -04001239 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
1240 (flag, _CONFIG_FILE, e))
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001241 if enabled:
1242 enable_flags.append(flag)
1243 else:
1244 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001245
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001246 # See if this hook has custom options.
1247 if enabled:
1248 try:
1249 options = config.get(SECTION_OPTIONS, flag)
1250 hooks[flag] = functools.partial(hooks[flag], options=options.split())
1251 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
1252 pass
1253
1254 enabled_hooks = set(hooks[x] for x in enable_flags)
1255 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger3554bc92015-03-11 04:59:21 -04001256 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001257
1258
Jon Salz3ee59de2012-08-18 13:54:22 +08001259def _get_project_hook_scripts(config):
1260 """Returns a list of project-specific hook scripts.
1261
1262 Args:
1263 config: A ConfigParser for the project's config file.
1264 """
1265 SECTION = 'Hook Scripts'
1266 if not config.has_section(SECTION):
1267 return []
1268
1269 hook_names_values = config.items(SECTION)
1270 hook_names_values.sort(key=lambda x: x[0])
1271 return [x[1] for x in hook_names_values]
1272
1273
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001274def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001275 """Returns a list of hooks that need to be run for a project.
1276
1277 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001278
1279 Args:
1280 project: A string, name of the project.
1281 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001282 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001283 config = ConfigParser.RawConfigParser()
1284 try:
1285 config.read(_CONFIG_FILE)
1286 except ConfigParser.Error:
1287 # Just use an empty config file
1288 config = ConfigParser.RawConfigParser()
1289
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001290 if presubmit:
1291 hook_list = _COMMON_HOOKS
1292 else:
1293 hook_list = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
1294
Mike Frysinger3554bc92015-03-11 04:59:21 -04001295 enabled_hooks, disabled_hooks = _get_override_hooks(config)
1296 hooks = (list(enabled_hooks) +
1297 [hook for hook in hook_list if hook not in disabled_hooks])
Ryan Cui9b651632011-05-11 11:38:58 -07001298
1299 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001300 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1301 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001302
Jon Salz3ee59de2012-08-18 13:54:22 +08001303 for script in _get_project_hook_scripts(config):
1304 hooks.append(functools.partial(_run_project_hook_script, script))
1305
Ryan Cui9b651632011-05-11 11:38:58 -07001306 return hooks
1307
1308
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001309def _run_project_hooks(project, proj_dir=None,
1310 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001311 """For each project run its project specific hook from the hooks dictionary.
1312
1313 Args:
Doug Anderson44a644f2011-11-02 10:37:37 -07001314 project: The name of project to run hooks for.
1315 proj_dir: If non-None, this is the directory the project is in. If None,
1316 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001317 commit_list: A list of commits to run hooks against. If None or empty list
1318 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001319 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001320
1321 Returns:
1322 Boolean value of whether any errors were ecountered while running the hooks.
1323 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001324 if proj_dir is None:
David James2edd9002013-10-11 14:09:19 -07001325 proj_dirs = _run_command(['repo', 'forall', project, '-c', 'pwd']).split()
1326 if len(proj_dirs) == 0:
1327 print('%s cannot be found.' % project, file=sys.stderr)
1328 print('Please specify a valid project.', file=sys.stderr)
1329 return True
1330 if len(proj_dirs) > 1:
1331 print('%s is associated with multiple directories.' % project,
1332 file=sys.stderr)
1333 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1334 return True
1335 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001336
Ryan Cuiec4d6332011-05-02 14:15:25 -07001337 pwd = os.getcwd()
1338 # hooks assume they are run from the root of the project
1339 os.chdir(proj_dir)
1340
Doug Anderson14749562013-06-26 13:38:29 -07001341 if not commit_list:
1342 try:
1343 commit_list = _get_commits()
1344 except VerifyException as e:
1345 PrintErrorForProject(project, HookFailure(str(e)))
1346 os.chdir(pwd)
1347 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001348
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001349 hooks = _get_project_hooks(project, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001350 error_found = False
Ryan Cuifa55df52011-05-06 11:16:55 -07001351 for commit in commit_list:
Ryan Cui1562fb82011-05-09 11:01:31 -07001352 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001353 for hook in hooks:
Ryan Cui1562fb82011-05-09 11:01:31 -07001354 hook_error = hook(project, commit)
1355 if hook_error:
1356 error_list.append(hook_error)
1357 error_found = True
1358 if error_list:
1359 PrintErrorsForCommit(project, commit, _get_commit_desc(commit),
1360 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001361
Ryan Cuiec4d6332011-05-02 14:15:25 -07001362 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001363 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001364
Mike Frysingerae409522014-02-01 03:16:11 -05001365
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001366# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001367
Ryan Cui1562fb82011-05-09 11:01:31 -07001368
Mike Frysingerae409522014-02-01 03:16:11 -05001369def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001370 """Main function invoked directly by repo.
1371
1372 This function will exit directly upon error so that repo doesn't print some
1373 obscure error message.
1374
1375 Args:
1376 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001377 worktree_list: A list of directories. It should be the same length as
1378 project_list, so that each entry in project_list matches with a directory
1379 in worktree_list. If None, we will attempt to calculate the directories
1380 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001381 kwargs: Leave this here for forward-compatibility.
1382 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001383 found_error = False
David James2edd9002013-10-11 14:09:19 -07001384 if not worktree_list:
1385 worktree_list = [None] * len(project_list)
1386 for project, worktree in zip(project_list, worktree_list):
1387 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001388 found_error = True
1389
Mike Frysingerae409522014-02-01 03:16:11 -05001390 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001391 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001392 '- To disable some source style checks, and for other hints, see '
1393 '<checkout_dir>/src/repohooks/README\n'
1394 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001395 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001396 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001397
Ryan Cui1562fb82011-05-09 11:01:31 -07001398
Doug Anderson44a644f2011-11-02 10:37:37 -07001399def _identify_project(path):
1400 """Identify the repo project associated with the given path.
1401
1402 Returns:
1403 A string indicating what project is associated with the path passed in or
1404 a blank string upon failure.
1405 """
1406 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Mike Frysingerb81102f2014-11-21 00:33:35 -05001407 stderr=subprocess.PIPE, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07001408
1409
Mike Frysinger55f85b52014-12-18 14:45:21 -05001410def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07001411 """Run hooks directly (outside of the context of repo).
1412
Doug Anderson44a644f2011-11-02 10:37:37 -07001413 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05001414 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07001415
1416 Returns:
1417 0 if no pre-upload failures, 1 if failures.
1418
1419 Raises:
1420 BadInvocation: On some types of invocation errors.
1421 """
Mike Frysinger66142932014-12-18 14:55:57 -05001422 parser = commandline.ArgumentParser(description=__doc__)
1423 parser.add_argument('--dir', default=None,
1424 help='The directory that the project lives in. If not '
1425 'specified, use the git project root based on the cwd.')
1426 parser.add_argument('--project', default=None,
1427 help='The project repo path; this can affect how the '
1428 'hooks get run, since some hooks are project-specific. '
1429 'For chromite this is chromiumos/chromite. If not '
1430 'specified, the repo tool will be used to figure this '
1431 'out based on the dir.')
1432 parser.add_argument('--rerun-since', default=None,
1433 help='Rerun hooks on old commits since the given date. '
1434 'The date should match git log\'s concept of a date. '
1435 'e.g. 2012-06-20. This option is mutually exclusive '
1436 'with --pre-submit.')
1437 parser.add_argument('--pre-submit', action="store_true",
1438 help='Run the check against the pending commit. '
1439 'This option should be used at the \'git commit\' '
1440 'phase as opposed to \'repo upload\'. This option '
1441 'is mutually exclusive with --rerun-since.')
1442 parser.add_argument('commits', nargs='*',
1443 help='Check specific commits')
1444 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07001445
Doug Anderson14749562013-06-26 13:38:29 -07001446 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05001447 if opts.commits:
Doug Anderson14749562013-06-26 13:38:29 -07001448 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
Mike Frysinger66142932014-12-18 14:55:57 -05001449 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07001450
1451 cmd = ['git', 'log', '--since="%s"' % opts.rerun_since, '--pretty=%H']
1452 all_commits = _run_command(cmd).splitlines()
1453 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1454
1455 # Eliminate chrome-bot commits but keep ordering the same...
1456 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05001457 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07001458
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001459 if opts.pre_submit:
1460 raise BadInvocation('rerun-since and pre-submit can not be '
1461 'used together')
1462 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05001463 if opts.commits:
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001464 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
Mike Frysinger66142932014-12-18 14:55:57 -05001465 ' '.join(opts.commits))
1466 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001467
1468 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1469 # project from CWD
1470 if opts.dir is None:
1471 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
1472 stderr=subprocess.PIPE).strip()
1473 if not git_dir:
1474 raise BadInvocation('The current directory is not part of a git project.')
1475 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1476 elif not os.path.isdir(opts.dir):
1477 raise BadInvocation('Invalid dir: %s' % opts.dir)
1478 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1479 raise BadInvocation('Not a git directory: %s' % opts.dir)
1480
1481 # Identify the project if it wasn't specified; this _requires_ the repo
1482 # tool to be installed and for the project to be part of a repo checkout.
1483 if not opts.project:
1484 opts.project = _identify_project(opts.dir)
1485 if not opts.project:
1486 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1487
Doug Anderson14749562013-06-26 13:38:29 -07001488 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05001489 commit_list=opts.commits,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001490 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001491 if found_error:
1492 return 1
1493 return 0
1494
1495
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001496if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05001497 sys.exit(direct_main(sys.argv[1:]))