blob: 7bcadb207a135bb7daffdfb51d5f0dd71e83ff8d [file] [log] [blame]
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001#!/usr/bin/env python2
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002# -*- coding: utf-8 -*-
Jon Salz98255932012-08-18 14:48:02 +08003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Mike Frysingerae409522014-02-01 03:16:11 -05007"""Presubmit checks to run when doing `repo upload`.
8
9You can add new checks by adding a functions to the HOOKS constants.
10"""
11
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040012from __future__ import print_function
13
Keigo Okadd908822019-06-04 11:30:25 +090014import argparse
Alex Deymo643ac4c2015-09-03 10:40:50 -070015import collections
Ryan Cui9b651632011-05-11 11:38:58 -070016import ConfigParser
Daniel Erate3ea3fc2015-02-13 15:27:52 -070017import fnmatch
Jon Salz3ee59de2012-08-18 13:54:22 +080018import functools
Dale Curtis2975c432011-05-03 17:25:20 -070019import json
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070020import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070021import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070022import sys
Peter Ammon811f6702014-06-12 15:45:38 -070023import stat
Aviv Keshet5ac59522017-01-31 14:28:27 -080024import StringIO
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070025
Ryan Cui1562fb82011-05-09 11:01:31 -070026from errors import (VerifyException, HookFailure, PrintErrorForProject,
27 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070028
Mike Frysinger6850d512018-05-21 12:12:14 -040029# If repo imports us, the __name__ will be __builtin__, and the cwd will be in
30# the top level of the checkout (i.e. $CHROMEOS_CHECKOUT). chromite will be in
31# that directory, so add it to our path. This works whether we're running the
32# repo in $CHROMEOS_CHECKOUT/.repo/repo/ or a custom version in a completely
33# different tree.
34if __name__ == '__builtin__':
35 sys.path.insert(0, os.getcwd())
36
37# If we're run directly, we'll find chromite relative to the repohooks dir in
38# $CHROMEOS_CHECKOUT/src/repohooks, so go up two dirs.
39if __name__ == '__main__':
David Jamesc3b68b32013-04-03 09:17:03 -070040 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
41
Mike Frysinger66142932014-12-18 14:55:57 -050042from chromite.lib import commandline
Aviv Keshet5ac59522017-01-31 14:28:27 -080043from chromite.lib import constants
Rahul Chaudhry0e515342015-08-07 12:00:43 -070044from chromite.lib import cros_build_lib
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050045from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070046from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070047from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040048from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070049
Vadim Bendebury2b62d742014-06-22 13:14:51 -070050PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070051
52COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050053 # C++ and friends
54 r".*\.c$", r".*\.cc$", r".*\.cpp$", r".*\.h$", r".*\.m$", r".*\.mm$",
55 r".*\.inl$", r".*\.asm$", r".*\.hxx$", r".*\.hpp$", r".*\.s$", r".*\.S$",
56 # Scripts
57 r".*\.js$", r".*\.py$", r".*\.sh$", r".*\.rb$", r".*\.pl$", r".*\.pm$",
58 # No extension at all, note that ALL CAPS files are black listed in
59 # COMMON_EXCLUDED_LIST below.
60 r"(^|.*[\\\/])[^.]+$",
61 # Other
62 r".*\.java$", r".*\.mk$", r".*\.am$",
Keigo Oka9732e382019-06-28 17:44:59 +090063 r".*\.policy$", r".*\.conf$", r".*\.go$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070064]
65
Ryan Cui1562fb82011-05-09 11:01:31 -070066
Ryan Cuiec4d6332011-05-02 14:15:25 -070067COMMON_EXCLUDED_PATHS = [
Daniel Erate3ea3fc2015-02-13 15:27:52 -070068 # For ebuild trees, ignore any caches and manifest data.
Mike Frysingerae409522014-02-01 03:16:11 -050069 r".*/Manifest$",
70 r".*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$",
Doug Anderson5bfb6792011-10-25 16:45:41 -070071
Daniel Erate3ea3fc2015-02-13 15:27:52 -070072 # Ignore profiles data (like overlay-tegra2/profiles).
Mike Frysinger94a670c2014-09-19 12:46:26 -040073 r"(^|.*/)overlay-.*/profiles/.*",
Mike Frysinger98638102014-08-28 00:15:08 -040074 r"^profiles/.*$",
75
C Shapiro8f90e9b2017-06-28 09:54:50 -060076 # Ignore config files in ebuild setup.
77 r"(^|.*/)overlay-.*/chromeos-base/chromeos-bsp.*/files/.*",
78 r"^chromeos-base/chromeos-bsp.*/files/.*",
79
Daniel Erate3ea3fc2015-02-13 15:27:52 -070080 # Ignore minified js and jquery.
Mike Frysingerae409522014-02-01 03:16:11 -050081 r".*\.min\.js",
82 r".*jquery.*\.js",
Mike Frysinger33a458d2014-03-03 17:00:51 -050083
84 # Ignore license files as the content is often taken verbatim.
Daniel Erate3ea3fc2015-02-13 15:27:52 -070085 r".*/licenses/.*",
Alex Klein619c0912019-01-30 17:13:23 -070086
87 # Exclude generated python protobuf files.
88 r".*_pb2\.py$",
Ryan Cuiec4d6332011-05-02 14:15:25 -070089]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070090
Ken Turnerd07564b2018-02-08 17:57:59 +110091LICENSE_EXCLUDED_PATHS = [
Chris McDonald7b63c8e2019-04-25 10:27:27 -060092 r"^(.*/)?OWNERS$",
Ken Turnerd07564b2018-02-08 17:57:59 +110093]
Ryan Cui1562fb82011-05-09 11:01:31 -070094
Ryan Cui9b651632011-05-11 11:38:58 -070095_CONFIG_FILE = 'PRESUBMIT.cfg'
96
97
Daniel Erate3ea3fc2015-02-13 15:27:52 -070098# File containing wildcards, one per line, matching files that should be
99# excluded from presubmit checks. Lines beginning with '#' are ignored.
100_IGNORE_FILE = '.presubmitignore'
101
Doug Anderson44a644f2011-11-02 10:37:37 -0700102# Exceptions
103
104
105class BadInvocation(Exception):
106 """An Exception indicating a bad invocation of the program."""
107 pass
108
109
Ryan Cui1562fb82011-05-09 11:01:31 -0700110# General Helpers
111
Sean Paulba01d402011-05-05 11:36:23 -0400112
Alex Deymo643ac4c2015-09-03 10:40:50 -0700113Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
114
115
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700116# pylint: disable=redefined-builtin
117def _run_command(cmd, cwd=None, input=None,
118 redirect_stderr=False, combine_stdout_stderr=False):
Doug Anderson44a644f2011-11-02 10:37:37 -0700119 """Executes the passed in command and returns raw stdout output.
120
121 Args:
122 cmd: The command to run; should be a list of strings.
123 cwd: The directory to switch to for running the command.
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700124 input: The data to pipe into this command through stdin. If a file object
125 or file descriptor, stdin will be connected directly to that.
126 redirect_stderr: Redirect stderr away from console.
127 combine_stdout_stderr: Combines stdout and stderr streams into stdout.
Doug Anderson44a644f2011-11-02 10:37:37 -0700128
129 Returns:
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700130 The stdout from the process (discards stderr and returncode).
Doug Anderson44a644f2011-11-02 10:37:37 -0700131 """
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700132 return cros_build_lib.RunCommand(cmd=cmd,
133 cwd=cwd,
134 print_cmd=False,
135 input=input,
136 stdout_to_pipe=True,
137 redirect_stderr=redirect_stderr,
138 combine_stdout_stderr=combine_stdout_stderr,
139 error_code_ok=True).output
140# pylint: enable=redefined-builtin
Ryan Cui72834d12011-05-05 14:51:33 -0700141
Ryan Cui1562fb82011-05-09 11:01:31 -0700142
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700143def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700144 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700145 if __name__ == '__main__':
146 # Works when file is run on its own (__file__ is defined)...
147 return os.path.abspath(os.path.dirname(__file__))
148 else:
149 # We need to do this when we're run through repo. Since repo executes
150 # us with execfile(), we don't get __file__ defined.
151 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
152 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700153
Ryan Cui1562fb82011-05-09 11:01:31 -0700154
Ryan Cuiec4d6332011-05-02 14:15:25 -0700155def _match_regex_list(subject, expressions):
156 """Try to match a list of regular expressions to a string.
157
158 Args:
159 subject: The string to match regexes on
160 expressions: A list of regular expressions to check for matches with.
161
162 Returns:
163 Whether the passed in subject matches any of the passed in regexes.
164 """
165 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500166 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700167 return True
168 return False
169
Ryan Cui1562fb82011-05-09 11:01:31 -0700170
Mike Frysingerae409522014-02-01 03:16:11 -0500171def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700172 """Filter out files based on the conditions passed in.
173
174 Args:
175 files: list of filepaths to filter
176 include_list: list of regex that when matched with a file path will cause it
177 to be added to the output list unless the file is also matched with a
178 regex in the exclude_list.
179 exclude_list: list of regex that when matched with a file will prevent it
180 from being added to the output list, even if it is also matched with a
181 regex in the include_list.
182
183 Returns:
184 A list of filepaths that contain files matched in the include_list and not
185 in the exclude_list.
186 """
187 filtered = []
188 for f in files:
189 if (_match_regex_list(f, include_list) and
190 not _match_regex_list(f, exclude_list)):
191 filtered.append(f)
192 return filtered
193
Ryan Cuiec4d6332011-05-02 14:15:25 -0700194
195# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700196
197
Ryan Cui4725d952011-05-05 15:41:19 -0700198def _get_upstream_branch():
199 """Returns the upstream tracking branch of the current branch.
200
201 Raises:
202 Error if there is no tracking branch
203 """
204 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
205 current_branch = current_branch.replace('refs/heads/', '')
206 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700207 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700208
209 cfg_option = 'branch.' + current_branch + '.%s'
210 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
211 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
212 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700213 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700214
215 return full_upstream.replace('heads', 'remotes/' + remote)
216
Ryan Cui1562fb82011-05-09 11:01:31 -0700217
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700218def _get_patch(commit):
219 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700220 if commit == PRE_SUBMIT:
221 return _run_command(['git', 'diff', '--cached', 'HEAD'])
222 else:
223 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700224
Ryan Cui1562fb82011-05-09 11:01:31 -0700225
Jon Salz98255932012-08-18 14:48:02 +0800226def _try_utf8_decode(data):
227 """Attempts to decode a string as UTF-8.
228
229 Returns:
230 The decoded Unicode object, or the original string if parsing fails.
231 """
232 try:
233 return unicode(data, 'utf-8', 'strict')
234 except UnicodeDecodeError:
235 return data
236
237
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500238def _get_file_content(path, commit):
239 """Returns the content of a file at a specific commit.
240
241 We can't rely on the file as it exists in the filesystem as people might be
242 uploading a series of changes which modifies the file multiple times.
243
244 Note: The "content" of a symlink is just the target. So if you're expecting
245 a full file, you should check that first. One way to detect is that the
246 content will not have any newlines.
247 """
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700248 if commit == PRE_SUBMIT:
249 return _run_command(['git', 'diff', 'HEAD', path])
250 else:
Mike Frysingerd0523442018-01-03 17:05:29 -0500251 # Make sure people don't accidentally pass in full paths which will never
252 # work. You need to use relative=True with _get_affected_files.
253 if path.startswith('/'):
254 raise ValueError('_get_file_content must be called with relative paths: '
255 + path)
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700256 return _run_command(['git', 'show', '%s:%s' % (commit, path)])
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500257
258
Mike Frysingerae409522014-02-01 03:16:11 -0500259def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700260 """Returns a list of (linenum, lines) tuples that the commit touched."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700261 if commit == PRE_SUBMIT:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800262 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff', 'HEAD',
263 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700264 else:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800265 command = ['git', 'show', '-p', '--pretty=format:', '--no-ext-diff', commit,
266 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700267 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700268
269 new_lines = []
270 line_num = 0
271 for line in output.splitlines():
272 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
273 if m:
274 line_num = int(m.groups(1)[0])
275 continue
276 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800277 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700278 if not line.startswith('-'):
279 line_num += 1
280 return new_lines
281
Ryan Cui1562fb82011-05-09 11:01:31 -0700282
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700283def _get_ignore_wildcards(directory, cache):
284 """Get wildcards listed in a directory's _IGNORE_FILE.
285
286 Args:
287 directory: A string containing a directory path.
288 cache: A dictionary (opaque to caller) caching previously-read wildcards.
289
290 Returns:
291 A list of wildcards from _IGNORE_FILE or an empty list if _IGNORE_FILE
292 wasn't present.
293 """
294 # In the cache, keys are directories and values are lists of wildcards from
295 # _IGNORE_FILE within those directories (and empty if no file was present).
296 if directory not in cache:
297 wildcards = []
298 dotfile_path = os.path.join(directory, _IGNORE_FILE)
299 if os.path.exists(dotfile_path):
300 # TODO(derat): Consider using _get_file_content() to get the file as of
301 # this commit instead of the on-disk version. This may have a noticeable
302 # performance impact, as each call to _get_file_content() runs git.
303 with open(dotfile_path, 'r') as dotfile:
304 for line in dotfile.readlines():
305 line = line.strip()
306 if line.startswith('#'):
307 continue
308 if line.endswith('/'):
309 line += '*'
310 wildcards.append(line)
311 cache[directory] = wildcards
312
313 return cache[directory]
314
315
316def _path_is_ignored(path, cache):
317 """Check whether a path is ignored by _IGNORE_FILE.
318
319 Args:
320 path: A string containing a path.
321 cache: A dictionary (opaque to caller) caching previously-read wildcards.
322
323 Returns:
324 True if a file named _IGNORE_FILE in one of the passed-in path's parent
325 directories contains a wildcard matching the path.
326 """
327 # Skip ignore files.
328 if os.path.basename(path) == _IGNORE_FILE:
329 return True
330
331 path = os.path.abspath(path)
332 base = os.getcwd()
333
334 prefix = os.path.dirname(path)
335 while prefix.startswith(base):
336 rel_path = path[len(prefix) + 1:]
337 for wildcard in _get_ignore_wildcards(prefix, cache):
338 if fnmatch.fnmatch(rel_path, wildcard):
339 return True
340 prefix = os.path.dirname(prefix)
341
342 return False
343
344
Mike Frysinger292b45d2014-11-25 01:17:10 -0500345def _get_affected_files(commit, include_deletes=False, relative=False,
346 include_symlinks=False, include_adds=True,
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700347 full_details=False, use_ignore_files=True):
Peter Ammon811f6702014-06-12 15:45:38 -0700348 """Returns list of file paths that were modified/added, excluding symlinks.
349
350 Args:
351 commit: The commit
352 include_deletes: If true, we'll include deleted files in the result
353 relative: Whether to return relative or full paths to files
Mike Frysinger292b45d2014-11-25 01:17:10 -0500354 include_symlinks: If true, we'll include symlinks in the result
355 include_adds: If true, we'll include new files in the result
356 full_details: If False, return filenames, else return structured results.
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700357 use_ignore_files: Whether we ignore files matched by _IGNORE_FILE files.
Peter Ammon811f6702014-06-12 15:45:38 -0700358
359 Returns:
360 A list of modified/added (and perhaps deleted) files
361 """
Mike Frysinger292b45d2014-11-25 01:17:10 -0500362 if not relative and full_details:
363 raise ValueError('full_details only supports relative paths currently')
364
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700365 if commit == PRE_SUBMIT:
366 return _run_command(['git', 'diff-index', '--cached',
367 '--name-only', 'HEAD']).split()
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500368
369 path = os.getcwd()
370 files = git.RawDiff(path, '%s^!' % commit)
371
372 # Filter out symlinks.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500373 if not include_symlinks:
374 files = [x for x in files if not stat.S_ISLNK(int(x.dst_mode, 8))]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500375
376 if not include_deletes:
377 files = [x for x in files if x.status != 'D']
378
Mike Frysinger292b45d2014-11-25 01:17:10 -0500379 if not include_adds:
380 files = [x for x in files if x.status != 'A']
381
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700382 if use_ignore_files:
383 cache = {}
384 is_ignored = lambda x: _path_is_ignored(x.dst_file or x.src_file, cache)
385 files = [x for x in files if not is_ignored(x)]
386
Mike Frysinger292b45d2014-11-25 01:17:10 -0500387 if full_details:
388 # Caller wants the raw objects to parse status/etc... themselves.
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500389 return files
390 else:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500391 # Caller only cares about filenames.
392 files = [x.dst_file if x.dst_file else x.src_file for x in files]
393 if relative:
394 return files
395 else:
396 return [os.path.join(path, x) for x in files]
Peter Ammon811f6702014-06-12 15:45:38 -0700397
398
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700399def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700400 """Returns a list of commits for this review."""
Ryan Cui4725d952011-05-05 15:41:19 -0700401 cmd = ['git', 'log', '%s..' % _get_upstream_branch(), '--format=%H']
Ryan Cui72834d12011-05-05 14:51:33 -0700402 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700403
Ryan Cui1562fb82011-05-09 11:01:31 -0700404
Ryan Cuiec4d6332011-05-02 14:15:25 -0700405def _get_commit_desc(commit):
406 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700407 if commit == PRE_SUBMIT:
408 return ''
Sean Paul23a2c582011-05-06 13:10:44 -0400409 return _run_command(['git', 'log', '--format=%s%n%n%b', commit + '^!'])
Ryan Cuiec4d6332011-05-02 14:15:25 -0700410
411
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800412def _check_lines_in_diff(commit, files, check_callable, error_description):
413 """Checks given file for errors via the given check.
414
415 This is a convenience function for common per-line checks. It goes through all
416 files and returns a HookFailure with the error description listing all the
417 failures.
418
419 Args:
420 commit: The commit we're working on.
421 files: The files to check.
422 check_callable: A callable that takes a line and returns True if this line
423 _fails_ the check.
424 error_description: A string describing the error.
425 """
426 errors = []
427 for afile in files:
428 for line_num, line in _get_file_diff(afile, commit):
429 if check_callable(line):
430 errors.append('%s, line %s' % (afile, line_num))
431 if errors:
432 return HookFailure(error_description, errors)
433
434
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900435def _parse_common_inclusion_options(options):
436 """Parses common hook options for including/excluding files.
437
438 Args:
439 options: Option string list.
440
441 Returns:
442 (included, excluded) where each one is a list of regex strings.
443 """
444 parser = argparse.ArgumentParser()
445 parser.add_argument('--exclude_regex', action='append')
446 parser.add_argument('--include_regex', action='append')
447 opts = parser.parse_args(options)
448 included = opts.include_regex or []
449 excluded = opts.exclude_regex or []
450 return included, excluded
451
452
Ryan Cuiec4d6332011-05-02 14:15:25 -0700453# Common Hooks
454
Ryan Cui1562fb82011-05-09 11:01:31 -0700455
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900456def _check_no_long_lines(_project, commit, options=()):
Mike Frysinger55f85b52014-12-18 14:45:21 -0500457 """Checks there are no lines longer than MAX_LEN in any of the text files."""
Keigo Oka9732e382019-06-28 17:44:59 +0900458 LONG_LINE_OK_PATHS = [
459 # Go has no line length limit.
460 # https://golang.org/doc/effective_go.html#formatting
461 r".*\.go$",
462 ]
Mike Frysinger55f85b52014-12-18 14:45:21 -0500463
Ryan Cuiec4d6332011-05-02 14:15:25 -0700464 MAX_LEN = 80
Jon Salz98255932012-08-18 14:48:02 +0800465 SKIP_REGEXP = re.compile('|'.join([
466 r'https?://',
Maksim Ivanov3f677b92018-06-05 16:10:24 +0200467 r'^#\s*(define|include|import|pragma|if|ifndef|endif)\b']))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700468
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900469 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700470 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900471 included + COMMON_INCLUDED_PATHS,
Keigo Oka9732e382019-06-28 17:44:59 +0900472 excluded + COMMON_EXCLUDED_PATHS + LONG_LINE_OK_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700473
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900474 errors = []
Ryan Cuiec4d6332011-05-02 14:15:25 -0700475 for afile in files:
476 for line_num, line in _get_file_diff(afile, commit):
477 # Allow certain lines to exceed the maxlen rule.
Mike Frysingerae409522014-02-01 03:16:11 -0500478 if len(line) <= MAX_LEN or SKIP_REGEXP.search(line):
Jon Salz98255932012-08-18 14:48:02 +0800479 continue
480
481 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
482 if len(errors) == 5: # Just show the first 5 errors.
483 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700484
485 if errors:
486 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700487 return HookFailure(msg, errors)
488
Ryan Cuiec4d6332011-05-02 14:15:25 -0700489
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900490def _check_no_stray_whitespace(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700491 """Checks that there is no stray whitespace at source lines end."""
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900492 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700493 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900494 included + COMMON_INCLUDED_PATHS,
495 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800496 return _check_lines_in_diff(commit, files,
497 lambda line: line.rstrip() != line,
498 'Found line ending with white space in:')
Ryan Cui1562fb82011-05-09 11:01:31 -0700499
Ryan Cuiec4d6332011-05-02 14:15:25 -0700500
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900501def _check_no_tabs(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700502 """Checks there are no unexpanded tabs."""
Mike Frysingercd134512017-10-26 04:36:33 -0400503 # Don't add entire repos here. Update the PRESUBMIT.cfg in each repo instead.
504 # We only whitelist known specific filetypes here that show up in all repos.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700505 TAB_OK_PATHS = [
Ryan Cuiec4d6332011-05-02 14:15:25 -0700506 r".*\.ebuild$",
507 r".*\.eclass$",
Keigo Oka9732e382019-06-28 17:44:59 +0900508 r".*\.go$",
Elly Jones5ab34192011-11-15 14:57:06 -0500509 r".*/[M|m]akefile$",
Keigo Oka9732e382019-06-28 17:44:59 +0900510 r".*\.mk$",
Ryan Cuiec4d6332011-05-02 14:15:25 -0700511 ]
512
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900513 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700514 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900515 included + COMMON_INCLUDED_PATHS,
516 excluded + COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800517 return _check_lines_in_diff(commit, files,
518 lambda line: '\t' in line,
519 'Found a tab character in:')
Ryan Cuiec4d6332011-05-02 14:15:25 -0700520
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800521
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900522def _check_tabbed_indents(_project, commit, options=()):
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800523 """Checks that indents use tabs only."""
524 TABS_REQUIRED_PATHS = [
525 r".*\.ebuild$",
526 r".*\.eclass$",
527 ]
528 LEADING_SPACE_RE = re.compile('[\t]* ')
529
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900530 included, excluded = _parse_common_inclusion_options(options)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800531 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900532 included + TABS_REQUIRED_PATHS,
533 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800534 return _check_lines_in_diff(
535 commit, files,
536 lambda line: LEADING_SPACE_RE.match(line) is not None,
537 'Found a space in indentation (must be all tabs):')
Ryan Cui1562fb82011-05-09 11:01:31 -0700538
Ryan Cuiec4d6332011-05-02 14:15:25 -0700539
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700540def _check_gofmt(_project, commit):
541 """Checks that Go files are formatted with gofmt."""
542 errors = []
543 files = _filter_files(_get_affected_files(commit, relative=True),
544 [r'\.go$'])
545
546 for gofile in files:
547 contents = _get_file_content(gofile, commit)
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700548 output = _run_command(cmd=['gofmt', '-l'], input=contents,
549 combine_stdout_stderr=True)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700550 if output:
551 errors.append(gofile)
552 if errors:
553 return HookFailure('Files not formatted with gofmt:', errors)
554
555
Mike Frysingerae409522014-02-01 03:16:11 -0500556def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700557 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700558 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700559
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700560 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700561 msg = 'Changelist description needs TEST field (after first line)'
562 return HookFailure(msg)
563
Ryan Cuiec4d6332011-05-02 14:15:25 -0700564
Mike Frysingerae409522014-02-01 03:16:11 -0500565def _check_change_has_valid_cq_depend(_project, commit):
Jason D. Clinton299e3222019-05-23 09:42:03 -0600566 """Check for a correctly formatted Cq-Depend field in the commit message."""
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700567 desc = _get_commit_desc(commit)
Jason D. Clinton299e3222019-05-23 09:42:03 -0600568 msg = 'Changelist has invalid Cq-Depend target.'
569 example = 'Example: Cq-Depend: chromium:1234, chrome-internal:2345'
David Jamesc3b68b32013-04-03 09:17:03 -0700570 try:
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700571 patch.GetPaladinDeps(desc)
David Jamesc3b68b32013-04-03 09:17:03 -0700572 except ValueError as ex:
573 return HookFailure(msg, [example, str(ex)])
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700574 # Check that Cq-Depend is in the same paragraph as Change-Id.
575 msg = 'Cq-Depend (or CQ-DEPEND) is not in the same paragraph as Change-Id.'
576 paragraphs = desc.split('\n\n')
577 for paragraph in paragraphs:
578 if (re.search(r'^Cq-Depend:', paragraph, re.M) or
579 re.search(r'^CQ-DEPEND=', paragraph, re.M)) \
580 and not re.search('^Change-Id:', paragraph, re.M):
581 return HookFailure(msg)
David Jamesc3b68b32013-04-03 09:17:03 -0700582
583
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800584def _check_change_is_contribution(_project, commit):
585 """Check that the change is a contribution."""
586 NO_CONTRIB = 'not a contribution'
587 if NO_CONTRIB in _get_commit_desc(commit).lower():
588 msg = ('Changelist is not a contribution, this cannot be accepted.\n'
589 'Please remove the "%s" text from the commit message.') % NO_CONTRIB
590 return HookFailure(msg)
591
592
Alex Deymo643ac4c2015-09-03 10:40:50 -0700593def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700594 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700595 OLD_BUG_RE = r'\nBUG=.*chromium-os'
596 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
597 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
598 'the chromium tracker in your BUG= line now.')
599 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700600
Alex Deymo643ac4c2015-09-03 10:40:50 -0700601 # Android internal and external projects use "Bug: " to track bugs in
602 # buganizer.
603 BUG_COLON_REMOTES = (
604 'aosp',
605 'goog',
606 )
607 if project.remote in BUG_COLON_REMOTES:
608 BUG_RE = r'\nBug: ?([Nn]one|\d+)'
609 if not re.search(BUG_RE, _get_commit_desc(commit)):
610 msg = ('Changelist description needs BUG field (after first line):\n'
611 'Bug: 9999 (for buganizer)\n'
612 'BUG=None')
613 return HookFailure(msg)
614 else:
Jorge Lucangeli Obesdce214e2017-10-25 15:04:39 -0400615 BUG_RE = r'\nBUG=([Nn]one|(chromium|b):\d+)'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700616 if not re.search(BUG_RE, _get_commit_desc(commit)):
617 msg = ('Changelist description needs BUG field (after first line):\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700618 'BUG=chromium:9999 (for public tracker)\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700619 'BUG=b:9999 (for buganizer)\n'
620 'BUG=None')
621 return HookFailure(msg)
Ryan Cui1562fb82011-05-09 11:01:31 -0700622
Ryan Cuiec4d6332011-05-02 14:15:25 -0700623
Mike Frysinger292b45d2014-11-25 01:17:10 -0500624def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -0700625 """Check that we're not missing a revbump of an ebuild in the given commit.
626
627 If the given commit touches files in a directory that has ebuilds somewhere
628 up the directory hierarchy, it's very likely that we need an ebuild revbump
629 in order for those changes to take effect.
630
631 It's not totally trivial to detect a revbump, so at least detect that an
632 ebuild with a revision number in it was touched. This should handle the
633 common case where we use a symlink to do the revbump.
634
635 TODO: it would be nice to enhance this hook to:
636 * Handle cases where people revbump with a slightly different syntax. I see
637 one ebuild (puppy) that revbumps with _pN. This is a false positive.
638 * Catches cases where people aren't using symlinks for revbumps. If they
639 edit a revisioned file directly (and are expected to rename it for revbump)
640 we'll miss that. Perhaps we could detect that the file touched is a
641 symlink?
642
643 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
644 still better off than without this check.
645
646 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700647 project: The Project to look at
Doug Anderson42b8a052013-06-26 10:45:36 -0700648 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -0500649 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -0700650
651 Returns:
652 A HookFailure or None.
653 """
Mike Frysinger011af942014-01-17 16:12:22 -0500654 # If this is the portage-stable overlay, then ignore the check. It's rare
655 # that we're doing anything other than importing files from upstream, so
656 # forcing a rev bump makes no sense.
657 whitelist = (
658 'chromiumos/overlays/portage-stable',
659 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700660 if project.name in whitelist:
Mike Frysinger011af942014-01-17 16:12:22 -0500661 return None
662
Mike Frysinger292b45d2014-11-25 01:17:10 -0500663 def FinalName(obj):
664 # If the file is being deleted, then the dst_file is not set.
665 if obj.dst_file is None:
666 return obj.src_file
667 else:
668 return obj.dst_file
669
670 affected_path_objs = _get_affected_files(
671 commit, include_deletes=True, include_symlinks=True, relative=True,
672 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -0700673
674 # Don't yell about changes to whitelisted files...
Aviv Keshet272f2e52016-04-25 14:49:44 -0700675 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml', 'COMMIT-QUEUE.ini')
Mike Frysinger292b45d2014-11-25 01:17:10 -0500676 affected_path_objs = [x for x in affected_path_objs
677 if os.path.basename(FinalName(x)) not in whitelist]
678 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -0700679 return None
680
681 # If we've touched any file named with a -rN.ebuild then we'll say we're
682 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500683 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
684 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700685 if touched_revved_ebuild:
686 return None
687
Mike Frysinger292b45d2014-11-25 01:17:10 -0500688 # If we're creating new ebuilds from scratch, then we don't need an uprev.
689 # Find all the dirs that new ebuilds and ignore their files/.
690 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
691 if FinalName(x).endswith('.ebuild') and x.status == 'A']
692 affected_path_objs = [obj for obj in affected_path_objs
693 if not any(FinalName(obj).startswith(x)
694 for x in ebuild_dirs)]
695 if not affected_path_objs:
696 return
697
Doug Anderson42b8a052013-06-26 10:45:36 -0700698 # We want to examine the current contents of all directories that are parents
699 # of files that were touched (up to the top of the project).
700 #
701 # ...note: we use the current directory contents even though it may have
702 # changed since the commit we're looking at. This is just a heuristic after
703 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500704 if project_top is None:
705 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -0700706 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -0500707 for obj in affected_path_objs:
708 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -0700709 while os.path.exists(path) and not os.path.samefile(path, project_top):
710 dirs_to_check.add(path)
711 path = os.path.dirname(path)
712
713 # Look through each directory. If it's got an ebuild in it then we'll
714 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -0800715 affected_paths = set(os.path.join(project_top, FinalName(x))
716 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700717 for dir_path in dirs_to_check:
718 contents = os.listdir(dir_path)
719 ebuilds = [os.path.join(dir_path, path)
720 for path in contents if path.endswith('.ebuild')]
721 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
722
C Shapiroae157ae2017-09-18 16:24:03 -0600723 affected_paths_under_9999_ebuilds = set()
724 for affected_path in affected_paths:
725 for ebuild_9999 in ebuilds_9999:
726 ebuild_dir = os.path.dirname(ebuild_9999)
727 if affected_path.startswith(ebuild_dir):
728 affected_paths_under_9999_ebuilds.add(affected_path)
729
730 # If every file changed exists under a 9999 ebuild, then skip
731 if len(affected_paths_under_9999_ebuilds) == len(affected_paths):
732 continue
733
Doug Anderson42b8a052013-06-26 10:45:36 -0700734 # If the -9999.ebuild file was touched the bot will uprev for us.
735 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -0500736 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -0700737 continue
738
739 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500740 return HookFailure('Changelist probably needs a revbump of an ebuild, '
741 'or a -r1.ebuild symlink if this is a new ebuild:\n'
742 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -0700743
744 return None
745
746
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500747def _check_ebuild_eapi(project, commit):
Mike Frysinger948284a2018-02-01 15:22:56 -0500748 """Make sure we have people use EAPI=5 or newer with custom ebuilds.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500749
750 We want to get away from older EAPI's as it makes life confusing and they
751 have less builtin error checking.
752
753 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700754 project: The Project to look at
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500755 commit: The commit to look at
756
757 Returns:
758 A HookFailure or None.
759 """
760 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500761 # that we're doing anything other than importing files from upstream, and
762 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500763 whitelist = (
764 'chromiumos/overlays/portage-stable',
765 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700766 if project.name in whitelist:
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500767 return None
768
Mike Frysinger948284a2018-02-01 15:22:56 -0500769 BAD_EAPIS = ('0', '1', '2', '3', '4')
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500770
771 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
772
773 ebuilds_re = [r'\.ebuild$']
774 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
775 ebuilds_re)
776 bad_ebuilds = []
777
778 for ebuild in ebuilds:
779 # If the ebuild does not specify an EAPI, it defaults to 0.
780 eapi = '0'
781
782 lines = _get_file_content(ebuild, commit).splitlines()
783 if len(lines) == 1:
784 # This is most likely a symlink, so skip it entirely.
785 continue
786
787 for line in lines:
788 m = get_eapi.match(line)
789 if m:
790 # Once we hit the first EAPI line in this ebuild, stop processing.
791 # The spec requires that there only be one and it be first, so
792 # checking all possible values is pointless. We also assume that
793 # it's "the" EAPI line and not something in the middle of a heredoc.
794 eapi = m.group(1)
795 break
796
797 if eapi in BAD_EAPIS:
798 bad_ebuilds.append((ebuild, eapi))
799
800 if bad_ebuilds:
801 # pylint: disable=C0301
802 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
803 # pylint: enable=C0301
804 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500805 'These ebuilds are using old EAPIs. If these are imported from\n'
806 'Gentoo, then you may ignore and upload once with the --no-verify\n'
Mike Frysinger948284a2018-02-01 15:22:56 -0500807 'flag. Otherwise, please update to 5 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500808 '\t%s\n'
809 'See this guide for more details:\n%s\n' %
810 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
811
812
Mike Frysinger89bdb852014-02-01 05:26:26 -0500813def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500814 """Make sure we use the new style KEYWORDS when possible in ebuilds.
815
816 If an ebuild generally does not care about the arch it is running on, then
817 ebuilds should flag it with one of:
818 KEYWORDS="*" # A stable ebuild.
819 KEYWORDS="~*" # An unstable ebuild.
820 KEYWORDS="-* ..." # Is known to only work on specific arches.
821
822 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700823 project: The Project to look at
Mike Frysingerc51ece72014-01-17 16:23:40 -0500824 commit: The commit to look at
825
826 Returns:
827 A HookFailure or None.
828 """
829 WHITELIST = set(('*', '-*', '~*'))
830
831 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
832
Mike Frysinger89bdb852014-02-01 05:26:26 -0500833 ebuilds_re = [r'\.ebuild$']
834 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
835 ebuilds_re)
836
Mike Frysinger8d42d742014-09-22 15:50:21 -0400837 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500838 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400839 # We get the full content rather than a diff as the latter does not work
840 # on new files (like when adding new ebuilds).
841 lines = _get_file_content(ebuild, commit).splitlines()
842 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500843 m = get_keywords.match(line)
844 if m:
845 keywords = set(m.group(1).split())
846 if not keywords or WHITELIST - keywords != WHITELIST:
847 continue
848
Mike Frysinger8d42d742014-09-22 15:50:21 -0400849 bad_ebuilds.append(ebuild)
850
851 if bad_ebuilds:
852 return HookFailure(
853 '%s\n'
854 'Please update KEYWORDS to use a glob:\n'
855 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
856 ' KEYWORDS="*"\n'
857 'If the ebuild should be marked unstable (normal for '
858 'cros-workon / 9999 ebuilds):\n'
859 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -0400860 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -0400861 'then use -* like so:\n'
862 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500863
864
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800865def _check_ebuild_licenses(_project, commit):
866 """Check if the LICENSE field in the ebuild is correct."""
Brian Norris7a610e82016-02-17 12:24:54 -0800867 affected_paths = _get_affected_files(commit, relative=True)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800868 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
869
870 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800871 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800872
873 for ebuild in touched_ebuilds:
874 # Skip virutal packages.
875 if ebuild.split('/')[-3] == 'virtual':
876 continue
877
Alex Kleinb5953522018-08-03 11:44:21 -0600878 # e.g. path/to/overlay/category/package/package.ebuild -> path/to/overlay
879 overlay_path = os.sep.join(ebuild.split(os.sep)[:-3])
880
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800881 try:
Brian Norris7a610e82016-02-17 12:24:54 -0800882 ebuild_content = _get_file_content(ebuild, commit)
Alex Kleinb5953522018-08-03 11:44:21 -0600883 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
884 overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800885 except ValueError as e:
886 return HookFailure(e.message, [ebuild])
887
888 # Also ignore licenses ending with '?'
889 for license_type in [x for x in license_types
890 if x not in LICENSES_IGNORE and not x.endswith('?')]:
891 try:
Alex Kleinb5953522018-08-03 11:44:21 -0600892 licenses_lib.Licensing.FindLicenseType(license_type,
893 overlay_path=overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800894 except AssertionError as e:
895 return HookFailure(e.message, [ebuild])
896
897
Mike Frysingercd363c82014-02-01 05:20:18 -0500898def _check_ebuild_virtual_pv(project, commit):
899 """Enforce the virtual PV policies."""
900 # If this is the portage-stable overlay, then ignore the check.
901 # We want to import virtuals as-is from upstream Gentoo.
902 whitelist = (
903 'chromiumos/overlays/portage-stable',
904 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700905 if project.name in whitelist:
Mike Frysingercd363c82014-02-01 05:20:18 -0500906 return None
907
908 # We assume the repo name is the same as the dir name on disk.
909 # It would be dumb to not have them match though.
Alex Deymo643ac4c2015-09-03 10:40:50 -0700910 project_base = os.path.basename(project.name)
Mike Frysingercd363c82014-02-01 05:20:18 -0500911
912 is_variant = lambda x: x.startswith('overlay-variant-')
913 is_board = lambda x: x.startswith('overlay-')
Douglas Andersonb43df7f2018-06-25 13:40:50 -0700914 is_baseboard = lambda x: x.startswith('baseboard-')
915 is_chipset = lambda x: x.startswith('chipset-')
916 is_project = lambda x: x.startswith('project-')
Mike Frysingercd363c82014-02-01 05:20:18 -0500917 is_private = lambda x: x.endswith('-private')
918
Douglas Andersonb43df7f2018-06-25 13:40:50 -0700919 is_special_overlay = lambda x: (is_board(x) or is_chipset(x) or
920 is_baseboard(x) or is_project(x))
921
Mike Frysingercd363c82014-02-01 05:20:18 -0500922 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
923
924 ebuilds_re = [r'\.ebuild$']
925 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
926 ebuilds_re)
927 bad_ebuilds = []
928
929 for ebuild in ebuilds:
930 m = get_pv.match(ebuild)
931 if m:
932 overlay = m.group(1)
Douglas Andersonb43df7f2018-06-25 13:40:50 -0700933 if not overlay or not is_special_overlay(overlay):
Alex Deymo643ac4c2015-09-03 10:40:50 -0700934 overlay = project_base
Mike Frysingercd363c82014-02-01 05:20:18 -0500935
936 pv = m.group(3).split('-', 1)[0]
937
Bernie Thompsone5ee1822016-01-12 14:22:23 -0800938 # Virtual versions >= 4 are special cases used above the standard
939 # versioning structure, e.g. if one has a board inheriting a board.
940 if float(pv) >= 4:
941 want_pv = pv
Mike Frysingercd363c82014-02-01 05:20:18 -0500942 elif is_board(overlay):
Douglas Andersonb43df7f2018-06-25 13:40:50 -0700943 if is_private(overlay):
944 want_pv = '3.5' if is_variant(overlay) else '3'
945 elif is_board(overlay):
946 want_pv = '2.5' if is_variant(overlay) else '2'
947 elif is_baseboard(overlay):
948 want_pv = '1.9'
949 elif is_chipset(overlay):
950 want_pv = '1.8'
951 elif is_project(overlay):
952 want_pv = '1.7' if is_private(overlay) else '1.5'
Mike Frysingercd363c82014-02-01 05:20:18 -0500953 else:
954 want_pv = '1'
955
956 if pv != want_pv:
957 bad_ebuilds.append((ebuild, pv, want_pv))
958
959 if bad_ebuilds:
960 # pylint: disable=C0301
961 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
962 # pylint: enable=C0301
963 return HookFailure(
964 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
965 '\t%s\n'
966 'If this is an upstream Gentoo virtual, then you may ignore this\n'
967 'check (and re-run w/--no-verify). Otherwise, please see this\n'
968 'page for more details:\n%s\n' %
969 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
970 for x in bad_ebuilds]), url))
971
972
Daniel Erat9d203ff2015-02-17 10:12:21 -0700973def _check_portage_make_use_var(_project, commit):
974 """Verify that $USE is set correctly in make.conf and make.defaults."""
975 files = _filter_files(_get_affected_files(commit, relative=True),
976 [r'(^|/)make.(conf|defaults)$'])
977
978 errors = []
979 for path in files:
980 basename = os.path.basename(path)
981
982 # Has a USE= line already been encountered in this file?
983 saw_use = False
984
985 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
986 if not line.startswith('USE='):
987 continue
988
989 preserves_use = '${USE}' in line or '$USE' in line
990
991 if (basename == 'make.conf' or
992 (basename == 'make.defaults' and saw_use)) and not preserves_use:
993 errors.append('%s:%d: missing ${USE}' % (path, i))
994 elif basename == 'make.defaults' and not saw_use and preserves_use:
995 errors.append('%s:%d: ${USE} referenced in initial declaration' %
996 (path, i))
997
998 saw_use = True
999
1000 if errors:
1001 return HookFailure(
1002 'One or more Portage make files appear to set USE incorrectly.\n'
1003 '\n'
1004 'All USE assignments in make.conf and all assignments after the\n'
1005 'initial declaration in make.defaults should contain "${USE}" to\n'
1006 'preserve previously-set flags.\n'
1007 '\n'
1008 'The initial USE declaration in make.defaults should not contain\n'
1009 '"${USE}".\n',
1010 errors)
1011
1012
Mike Frysingerae409522014-02-01 03:16:11 -05001013def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001014 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001015 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001016 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001017 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001018 if not m:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001019 return HookFailure('Last paragraph of description must include Change-Id.')
Ryan Cui1562fb82011-05-09 11:01:31 -07001020
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001021 # S-o-b tags always allowed to follow Change-ID.
1022 allowed_tags = ['Signed-off-by']
1023
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001024 end = desc[m.end():].strip().splitlines()
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001025 cherry_pick_marker = 'cherry picked from commit'
1026
1027 if end and cherry_pick_marker in end[-1]:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001028 # Cherry picked patches allow more tags in the last paragraph.
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001029 allowed_tags += ['Commit-Queue', 'Commit-Ready', 'Reviewed-by',
1030 'Reviewed-on', 'Tested-by']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001031 end = end[:-1]
1032
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001033 # Note that descriptions could have multiple cherry pick markers.
1034 tag_search = r'^(%s:|\(%s) ' % (':|'.join(allowed_tags), cherry_pick_marker)
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001035
1036 if [x for x in end if not re.search(tag_search, x)]:
1037 return HookFailure('Only "%s:" tag(s) may follow the Change-Id.' %
1038 ':", "'.join(allowed_tags))
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001039
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001040
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001041def _check_commit_message_style(_project, commit):
1042 """Verify that the commit message matches our style.
1043
1044 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
1045 commit hooks.
1046 """
1047 desc = _get_commit_desc(commit)
1048
1049 # The first line should be by itself.
1050 lines = desc.splitlines()
1051 if len(lines) > 1 and lines[1]:
1052 return HookFailure('The second line of the commit message must be blank.')
1053
1054 # The first line should be one sentence.
1055 if '. ' in lines[0]:
1056 return HookFailure('The first line cannot be more than one sentence.')
1057
1058 # The first line cannot be too long.
1059 MAX_FIRST_LINE_LEN = 100
1060 if len(lines[0]) > MAX_FIRST_LINE_LEN:
1061 return HookFailure('The first line must be less than %i chars.' %
1062 MAX_FIRST_LINE_LEN)
1063
1064
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001065def _check_cros_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001066 """Verifies the Chromium OS license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -07001067
Mike Frysinger98638102014-08-28 00:15:08 -04001068 Should be following the spec:
1069 http://dev.chromium.org/developers/coding-style#TOC-File-headers
1070 """
1071 # For older years, be a bit more flexible as our policy says leave them be.
1072 LICENSE_HEADER = (
Brian Norris68838dd2018-09-26 18:30:24 -07001073 r'.*Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001074 r'All rights reserved\.' r'\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001075 r'.*Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001076 r'be\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001077 r'.*found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -05001078 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001079 )
1080 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1081
1082 # For newer years, be stricter.
1083 COPYRIGHT_LINE = (
Brian Norris68838dd2018-09-26 18:30:24 -07001084 r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001085 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001086 )
1087 copyright_re = re.compile(COPYRIGHT_LINE)
1088
Shuhei Takahashiabc20f32017-07-10 19:35:45 +09001089 included, excluded = _parse_common_inclusion_options(options)
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001090
Mike Frysinger98638102014-08-28 00:15:08 -04001091 bad_files = []
1092 bad_copyright_files = []
Ken Turnerd07564b2018-02-08 17:57:59 +11001093 files = _filter_files(
1094 _get_affected_files(commit, relative=True),
1095 included + COMMON_INCLUDED_PATHS,
1096 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Mike Frysinger98638102014-08-28 00:15:08 -04001097
1098 for f in files:
1099 contents = _get_file_content(f, commit)
1100 if not contents:
1101 # Ignore empty files.
1102 continue
1103
1104 if not license_re.search(contents):
1105 bad_files.append(f)
1106 elif copyright_re.search(contents):
1107 bad_copyright_files.append(f)
1108
1109 if bad_files:
1110 msg = '%s:\n%s\n%s' % (
1111 'License must match', license_re.pattern,
1112 'Found a bad header in these files:')
1113 return HookFailure(msg, bad_files)
1114
1115 if bad_copyright_files:
1116 msg = 'Do not use (c) in copyright headers in new files:'
1117 return HookFailure(msg, bad_copyright_files)
Ryan Cuiec4d6332011-05-02 14:15:25 -07001118
1119
Amin Hassani391efa92018-01-26 17:58:05 -08001120def _check_aosp_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001121 """Verifies the AOSP license/copyright header.
1122
1123 AOSP uses the Apache2 License:
1124 https://source.android.com/source/licenses.html
1125 """
1126 LICENSE_HEADER = (
1127 r"""^[#/\*]*
1128[#/\*]* ?Copyright( \([cC]\))? 20[-0-9]{2,7} The Android Open Source Project
1129[#/\*]* ?
1130[#/\*]* ?Licensed under the Apache License, Version 2.0 \(the "License"\);
1131[#/\*]* ?you may not use this file except in compliance with the License\.
1132[#/\*]* ?You may obtain a copy of the License at
1133[#/\*]* ?
1134[#/\*]* ? http://www\.apache\.org/licenses/LICENSE-2\.0
1135[#/\*]* ?
1136[#/\*]* ?Unless required by applicable law or agreed to in writing, software
1137[#/\*]* ?distributed under the License is distributed on an "AS IS" BASIS,
1138[#/\*]* ?WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or """
1139 r"""implied\.
1140[#/\*]* ?See the License for the specific language governing permissions and
1141[#/\*]* ?limitations under the License\.
1142[#/\*]*$
1143"""
1144 )
1145 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1146
Amin Hassani391efa92018-01-26 17:58:05 -08001147 included, excluded = _parse_common_inclusion_options(options)
1148
Ken Turnerd07564b2018-02-08 17:57:59 +11001149 files = _filter_files(
1150 _get_affected_files(commit, relative=True),
1151 included + COMMON_INCLUDED_PATHS,
1152 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Alex Deymof5792ce2015-08-24 22:50:08 -07001153
1154 bad_files = []
1155 for f in files:
1156 contents = _get_file_content(f, commit)
1157 if not contents:
1158 # Ignore empty files.
1159 continue
1160
1161 if not license_re.search(contents):
1162 bad_files.append(f)
1163
1164 if bad_files:
1165 msg = ('License must match:\n%s\nFound a bad header in these files:' %
1166 license_re.pattern)
1167 return HookFailure(msg, bad_files)
1168
1169
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001170def _check_layout_conf(_project, commit):
1171 """Verifies the metadata/layout.conf file."""
1172 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001173 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001174 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001175 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001176
Mike Frysinger94a670c2014-09-19 12:46:26 -04001177 # Handle multiple overlays in a single commit (like the public tree).
1178 for f in _get_affected_files(commit, relative=True):
1179 if f.endswith(repo_name):
1180 repo_names.append(f)
1181 elif f.endswith(layout_path):
1182 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001183
1184 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -04001185 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001186 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -04001187 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001188
Mike Frysinger94a670c2014-09-19 12:46:26 -04001189 # Gather all the errors in one pass so we show one full message.
1190 all_errors = {}
1191 for layout_path in layout_paths:
1192 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001193
Mike Frysinger94a670c2014-09-19 12:46:26 -04001194 # Make sure the config file is sorted.
1195 data = [x for x in _get_file_content(layout_path, commit).splitlines()
1196 if x and x[0] != '#']
1197 if sorted(data) != data:
1198 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001199
Mike Frysinger94a670c2014-09-19 12:46:26 -04001200 # Require people to set specific values all the time.
1201 settings = (
1202 # TODO: Enable this for everyone. http://crbug.com/408038
1203 #('fast caching', 'cache-format = md5-dict'),
1204 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -05001205 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
1206 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001207 )
1208 for reason, line in settings:
1209 if line not in data:
1210 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001211
Mike Frysinger94a670c2014-09-19 12:46:26 -04001212 # Require one of these settings.
Mike Frysinger5fae62d2015-11-11 20:12:15 -05001213 if 'use-manifests = strict' not in data:
1214 errors += ['enable file checking with: use-manifests = strict']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001215
Mike Frysinger94a670c2014-09-19 12:46:26 -04001216 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -04001217 for line in data:
1218 if line.startswith('repo-name = '):
1219 break
1220 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001221 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001222
Mike Frysinger94a670c2014-09-19 12:46:26 -04001223 # Summarize all the errors we saw (if any).
1224 lines = ''
1225 for layout_path, errors in all_errors.items():
1226 if errors:
1227 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1228 if lines:
1229 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1230 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001231
1232
Keigo Oka4a09bd92019-05-07 14:01:00 +09001233def _check_no_new_gyp(_project, commit):
1234 """Verifies no project starts to use GYP."""
1235 whitelist = [
Keigo Oka4a09bd92019-05-07 14:01:00 +09001236 'chromeos/ap',
1237 'chromeos/ap-daemons',
Keigo Oka150a6fd2019-06-04 11:30:25 +09001238 'chromeos/ap/security',
1239 'chromeos/ap/wireless',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001240 'chromeos/platform/actions',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001241 'chromeos/platform/drivefs-google3',
1242 'chromeos/platform/experimental-touch-fw',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001243 'chromeos/thermald',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001244 'chromiumos/platform2',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001245 'weave/libweave',
1246 ]
1247 if _project.name in whitelist:
1248 return None
1249
1250 gypfiles = _filter_files(
1251 _get_affected_files(commit, include_symlinks=True, relative=True),
1252 [r'\.gyp$'])
1253
1254 if gypfiles:
1255 return HookFailure('GYP is deprecated and not allowed in a new project:',
1256 gypfiles)
1257
1258
Ryan Cuiec4d6332011-05-02 14:15:25 -07001259# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001260
Ryan Cui1562fb82011-05-09 11:01:31 -07001261
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001262def _check_clang_format(_project, commit, options=()):
1263 """Runs clang-format on the given project"""
1264 hooks_dir = _get_hooks_dir()
1265 options = list(options)
1266 if commit == PRE_SUBMIT:
1267 options.append('--commit=HEAD')
1268 else:
1269 options.extend(['--commit', commit])
Brian Norris0c62a142018-12-11 13:24:29 -08001270 cmd = [os.path.join(hooks_dir, 'clang-format.py')] + options
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001271 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1272 print_cmd=False,
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001273 stdout_to_pipe=True,
1274 combine_stdout_stderr=True,
1275 error_code_ok=True)
1276 if cmd_result.returncode:
1277 return HookFailure('clang-format.py errors/warnings\n\n' +
1278 cmd_result.output)
1279
1280
Mike Frysingerae409522014-02-01 03:16:11 -05001281def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001282 """Runs checkpatch.pl on the given project"""
1283 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001284 options = list(options)
1285 if commit == PRE_SUBMIT:
1286 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1287 # this case.
1288 options.append('--ignore=MISSING_SIGN_OFF')
Filipe Brandenburger28d48d62015-10-07 09:48:54 -07001289 # Always ignore the check for the MAINTAINERS file. We do not track that
1290 # information on that file in our source trees, so let's suppress the
1291 # warning.
1292 options.append('--ignore=FILE_PATH_CHANGES')
Filipe Brandenburgerce978322015-10-09 10:04:15 -07001293 # Do not complain about the Change-Id: fields, since we use Gerrit.
1294 # Upstream does not want those lines (since they do not use Gerrit), but
1295 # we always do, so disable the check globally.
Daisuke Nojiri4844f892015-10-08 09:54:33 -07001296 options.append('--ignore=GERRIT_CHANGE_ID')
Brian Norris0c62a142018-12-11 13:24:29 -08001297 cmd = [os.path.join(hooks_dir, 'checkpatch.pl')] + options + ['-']
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001298 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1299 print_cmd=False,
1300 input=_get_patch(commit),
1301 stdout_to_pipe=True,
1302 combine_stdout_stderr=True,
1303 error_code_ok=True)
1304 if cmd_result.returncode:
1305 return HookFailure('checkpatch.pl errors/warnings\n\n' + cmd_result.output)
Ryan Cuiec4d6332011-05-02 14:15:25 -07001306
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001307
Brian Norris23c62e92018-11-14 12:25:51 -08001308def _run_kerneldoc(_project, commit, options=()):
1309 """Runs kernel-doc validator on the given project"""
1310 included, excluded = _parse_common_inclusion_options(options)
1311 files = _filter_files(_get_affected_files(commit, relative=True),
1312 included, excluded)
1313 if files:
1314 hooks_dir = _get_hooks_dir()
Brian Norris0c62a142018-12-11 13:24:29 -08001315 cmd = [os.path.join(hooks_dir, 'kernel-doc'), '-none'] + files
Brian Norris23c62e92018-11-14 12:25:51 -08001316 output = _run_command(cmd=cmd, combine_stdout_stderr=True)
1317 if output:
Brian Norris0c62a142018-12-11 13:24:29 -08001318 return HookFailure('kernel-doc errors/warnings:',
1319 items=output.splitlines())
Brian Norris23c62e92018-11-14 12:25:51 -08001320
1321
Mike Frysingerae409522014-02-01 03:16:11 -05001322def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001323 """Makes sure kernel config changes are not mixed with code changes"""
1324 files = _get_affected_files(commit)
1325 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
1326 return HookFailure('Changes to chromeos/config/ and regular files must '
1327 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -07001328
Mike Frysingerae409522014-02-01 03:16:11 -05001329
1330def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001331 """Checks that all JSON files are syntactically valid."""
Mike Frysinger908be682018-01-04 02:21:50 -05001332 ret = []
1333
1334 files = _filter_files(_get_affected_files(commit, relative=True),
1335 [r'.*\.json$'])
1336 for f in files:
1337 data = _get_file_content(f, commit)
Dale Curtis2975c432011-05-03 17:25:20 -07001338 try:
Mike Frysinger908be682018-01-04 02:21:50 -05001339 json.loads(data)
1340 except Exception as e:
1341 ret.append('%s: Invalid JSON: %s' % (f, e))
1342
1343 if ret:
1344 return HookFailure('\n'.join(ret))
Dale Curtis2975c432011-05-03 17:25:20 -07001345
1346
Mike Frysingerae409522014-02-01 03:16:11 -05001347def _check_manifests(_project, commit):
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001348 """Make sure Manifest files only have comments & DIST lines."""
1349 ret = []
Mike Frysinger52b537e2013-08-22 22:59:53 -04001350
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001351 manifests = _filter_files(_get_affected_files(commit, relative=True),
1352 [r'.*/Manifest$'])
1353 for path in manifests:
1354 data = _get_file_content(path, commit)
1355
1356 # Disallow blank files.
1357 if not data.strip():
1358 ret.append('%s: delete empty file' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001359 continue
1360
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001361 # Make sure the last newline isn't omitted.
1362 if data[-1] != '\n':
1363 ret.append('%s: missing trailing newline' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001364
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001365 # Do not allow leading or trailing blank lines.
1366 lines = data.splitlines()
1367 if not lines[0]:
1368 ret.append('%s: delete leading blank lines' % (path,))
1369 if not lines[-1]:
1370 ret.append('%s: delete trailing blank lines' % (path,))
1371
1372 for line in lines:
1373 # Disallow leading/trailing whitespace.
1374 if line != line.strip():
1375 ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
1376
1377 # Allow blank lines & comments.
1378 line = line.split('#', 1)[0]
1379 if not line:
1380 continue
1381
1382 # All other linse should start with DIST.
1383 if not line.startswith('DIST '):
1384 ret.append('%s: remove non-DIST lines: %s' % (path, line))
1385 break
1386
1387 if ret:
1388 return HookFailure('\n'.join(ret))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001389
1390
Mike Frysingerae409522014-02-01 03:16:11 -05001391def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001392 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001393 if commit == PRE_SUBMIT:
1394 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001395 BRANCH_RE = r'\nBRANCH=\S+'
1396
1397 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1398 msg = ('Changelist description needs BRANCH field (after first line)\n'
1399 'E.g. BRANCH=none or BRANCH=link,snow')
1400 return HookFailure(msg)
1401
1402
Mike Frysingerae409522014-02-01 03:16:11 -05001403def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001404 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001405 if commit == PRE_SUBMIT:
1406 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001407 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1408
1409 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1410 msg = ('Changelist description needs Signed-off-by: field\n'
1411 'E.g. Signed-off-by: My Name <me@chromium.org>')
1412 return HookFailure(msg)
1413
1414
Aviv Keshet5ac59522017-01-31 14:28:27 -08001415def _check_cq_ini_well_formed(_project, commit):
1416 """Check that any modified COMMIT-QUEUE.ini files are well formed."""
1417 pattern = '.*' + constants.CQ_CONFIG_FILENAME
Mike Frysingerd0523442018-01-03 17:05:29 -05001418 files = _filter_files(_get_affected_files(commit, relative=True), (pattern,))
Aviv Keshet5ac59522017-01-31 14:28:27 -08001419
1420 # TODO(akeshet): Check not only that the file is parseable, but that all the
1421 # pre-cq configs it requests are existing ones.
1422 for f in files:
1423 try:
1424 parser = ConfigParser.SafeConfigParser()
1425 # Prior to python3, ConfigParser has no read_string method, so we must
1426 # pass it either a file path or file like object. And we must use
1427 # _get_file_content to fetch file contents to ensure we are examining the
1428 # commit diff, rather than whatever's on disk.
1429 contents = _get_file_content(f, commit)
1430 parser.readfp(StringIO.StringIO(contents))
1431 except ConfigParser.Error as e:
1432 msg = ('Unable to parse COMMIT-QUEUE.ini file at %s due to %s.' %
1433 (f, e))
1434 return HookFailure(msg)
1435
1436
Jon Salz3ee59de2012-08-18 13:54:22 +08001437def _run_project_hook_script(script, project, commit):
1438 """Runs a project hook script.
1439
1440 The script is run with the following environment variables set:
1441 PRESUBMIT_PROJECT: The affected project
1442 PRESUBMIT_COMMIT: The affected commit
1443 PRESUBMIT_FILES: A newline-separated list of affected files
1444
1445 The script is considered to fail if the exit code is non-zero. It should
1446 write an error message to stdout.
1447 """
1448 env = dict(os.environ)
Alex Deymo643ac4c2015-09-03 10:40:50 -07001449 env['PRESUBMIT_PROJECT'] = project.name
Jon Salz3ee59de2012-08-18 13:54:22 +08001450 env['PRESUBMIT_COMMIT'] = commit
1451
1452 # Put affected files in an environment variable
1453 files = _get_affected_files(commit)
1454 env['PRESUBMIT_FILES'] = '\n'.join(files)
1455
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001456 cmd_result = cros_build_lib.RunCommand(cmd=script,
1457 env=env,
1458 shell=True,
1459 print_cmd=False,
1460 input=os.devnull,
1461 stdout_to_pipe=True,
1462 combine_stdout_stderr=True,
1463 error_code_ok=True)
1464 if cmd_result.returncode:
1465 stdout = cmd_result.output
Jon Salz7b618af2012-08-31 06:03:16 +08001466 if stdout:
1467 stdout = re.sub('(?m)^', ' ', stdout)
1468 return HookFailure('Hook script "%s" failed with code %d%s' %
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001469 (script, cmd_result.returncode,
Jon Salz3ee59de2012-08-18 13:54:22 +08001470 ':\n' + stdout if stdout else ''))
1471
1472
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001473def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05001474 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001475
Brian Norris77608e12018-04-06 10:38:43 -07001476 files = _get_affected_files(commit, include_deletes=True, relative=True)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001477 prefix = os.path.commonprefix(files)
1478 prefix = os.path.dirname(prefix)
1479
1480 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07001481 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001482 return
1483
1484 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07001485
1486 # The common files may all be within a subdirectory of the main project
1487 # directory, so walk up the tree until we find an alias file.
1488 # _get_affected_files() should return relative paths, but check against '/' to
1489 # ensure that this loop terminates even if it receives an absolute path.
1490 while prefix and prefix != '/':
1491 alias_file = os.path.join(prefix, '.project_alias')
1492
1493 # If an alias exists, use it.
1494 if os.path.isfile(alias_file):
1495 project_name = osutils.ReadFile(alias_file).strip()
1496
1497 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001498
1499 if not _get_commit_desc(commit).startswith(project_name + ': '):
1500 return HookFailure('The commit title for changes affecting only %s'
1501 ' should start with \"%s: \"'
1502 % (project_name, project_name))
1503
1504
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001505def _check_filepath_chartype(_project, commit):
1506 """Checks that FilePath::CharType stuff is not used."""
1507
1508 FILEPATH_REGEXP = re.compile('|'.join(
1509 [r'(?:base::)?FilePath::(?:Char|String|StringPiece)Type',
Satoru Takabayashi4ca37922018-08-08 10:16:38 +09001510 r'(?:base::)?FilePath::FromUTF8Unsafe',
1511 r'AsUTF8Unsafe',
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001512 r'FILE_PATH_LITERAL']))
1513 files = _filter_files(_get_affected_files(commit, relative=True),
1514 [r'.*\.(cc|h)$'])
1515
1516 errors = []
1517 for afile in files:
1518 for line_num, line in _get_file_diff(afile, commit):
1519 m = re.search(FILEPATH_REGEXP, line)
1520 if m:
1521 errors.append('%s, line %s has %s' % (afile, line_num, m.group(0)))
1522
1523 if errors:
1524 msg = 'Please assume FilePath::CharType is char (crbug.com/870621):'
1525 return HookFailure(msg, errors)
1526
1527
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001528def _check_exec_files(_project, commit):
1529 """Make +x bits on files."""
1530 # List of files that should never be +x.
1531 NO_EXEC = (
1532 'ChangeLog*',
1533 'COPYING',
1534 'make.conf',
1535 'make.defaults',
1536 'Manifest',
1537 'OWNERS',
1538 'package.use',
1539 'package.keywords',
1540 'package.mask',
1541 'parent',
1542 'README',
1543 'TODO',
1544 '.gitignore',
1545 '*.[achly]',
1546 '*.[ch]xx',
1547 '*.boto',
1548 '*.cc',
1549 '*.cfg',
1550 '*.conf',
1551 '*.config',
1552 '*.cpp',
1553 '*.css',
1554 '*.ebuild',
1555 '*.eclass',
Tatsuhisa Yamaguchi3b053632019-01-10 14:45:14 +09001556 '*.gn',
1557 '*.gni',
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001558 '*.gyp',
1559 '*.gypi',
1560 '*.htm',
1561 '*.html',
1562 '*.ini',
1563 '*.js',
1564 '*.json',
1565 '*.md',
1566 '*.mk',
1567 '*.patch',
1568 '*.policy',
1569 '*.proto',
1570 '*.raw',
1571 '*.rules',
1572 '*.service',
1573 '*.target',
1574 '*.txt',
1575 '*.xml',
1576 '*.yaml',
1577 )
1578
1579 def FinalName(obj):
1580 # If the file is being deleted, then the dst_file is not set.
1581 if obj.dst_file is None:
1582 return obj.src_file
1583 else:
1584 return obj.dst_file
1585
1586 bad_files = []
1587 files = _get_affected_files(commit, relative=True, full_details=True)
1588 for f in files:
1589 mode = int(f.dst_mode, 8)
1590 if not mode & 0o111:
1591 continue
1592 name = FinalName(f)
1593 for no_exec in NO_EXEC:
1594 if fnmatch.fnmatch(name, no_exec):
1595 bad_files.append(name)
1596 break
1597
1598 if bad_files:
1599 return HookFailure('These files should not be executable. '
1600 'Please `chmod -x` them.', bad_files)
1601
1602
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001603# Base
1604
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001605# A list of hooks which are not project specific and check patch description
1606# (as opposed to patch body).
1607_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001608 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001609 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001610 _check_change_has_test_field,
1611 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001612 _check_commit_message_style,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001613 _check_change_is_contribution,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001614]
1615
1616
1617# A list of hooks that are not project-specific
1618_COMMON_HOOKS = [
Aviv Keshet5ac59522017-01-31 14:28:27 -08001619 _check_cq_ini_well_formed,
1620 _check_cros_license,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001621 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001622 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001623 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001624 _check_ebuild_virtual_pv,
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001625 _check_exec_files,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001626 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07001627 _check_gofmt,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001628 _check_layout_conf,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001629 _check_no_long_lines,
Keigo Oka4a09bd92019-05-07 14:01:00 +09001630 _check_no_new_gyp,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001631 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07001632 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001633 _check_portage_make_use_var,
Aviv Keshet5ac59522017-01-31 14:28:27 -08001634 _check_tabbed_indents,
Ryan Cui9b651632011-05-11 11:38:58 -07001635]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001636
Ryan Cui1562fb82011-05-09 11:01:31 -07001637
Ryan Cui9b651632011-05-11 11:38:58 -07001638# A dictionary of project-specific hooks(callbacks), indexed by project name.
1639# dict[project] = [callback1, callback2]
1640_PROJECT_SPECIFIC_HOOKS = {
Mike Frysingeraf891292015-03-25 19:46:53 -04001641 "chromiumos/third_party/kernel": [_kernel_configcheck],
1642 "chromiumos/third_party/kernel-next": [_kernel_configcheck],
Ryan Cui9b651632011-05-11 11:38:58 -07001643}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001644
Ryan Cui1562fb82011-05-09 11:01:31 -07001645
Ryan Cui9b651632011-05-11 11:38:58 -07001646# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04001647# that the flag controls (value).
1648_HOOK_FLAGS = {
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001649 'clang_format_check': _check_clang_format,
Mike Frysingera7642f52015-03-25 18:31:42 -04001650 'checkpatch_check': _run_checkpatch,
Brian Norris23c62e92018-11-14 12:25:51 -08001651 'kerneldoc_check': _run_kerneldoc,
Ryan Cui9b651632011-05-11 11:38:58 -07001652 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04001653 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07001654 'long_line_check': _check_no_long_lines,
Alex Deymof5792ce2015-08-24 22:50:08 -07001655 'cros_license_check': _check_cros_license,
1656 'aosp_license_check': _check_aosp_license,
Ryan Cui9b651632011-05-11 11:38:58 -07001657 'tab_check': _check_no_tabs,
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001658 'tabbed_indent_required_check': _check_tabbed_indents,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001659 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001660 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001661 'bug_field_check': _check_change_has_bug_field,
1662 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07001663 'manifest_check': _check_manifests,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001664 'contribution_check': _check_change_is_contribution,
Mike Frysinger47bd1252018-06-11 12:12:20 -04001665 'project_prefix_check': _check_project_prefix,
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001666 'filepath_chartype_check': _check_filepath_chartype,
Ryan Cui9b651632011-05-11 11:38:58 -07001667}
1668
1669
Mike Frysinger3554bc92015-03-11 04:59:21 -04001670def _get_override_hooks(config):
1671 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001672
1673 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001674
1675 Args:
1676 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001677 """
1678 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001679 SECTION_OPTIONS = 'Hook Overrides Options'
Jon Salz3ee59de2012-08-18 13:54:22 +08001680 if not config.has_section(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001681 return set(), set()
Ryan Cui9b651632011-05-11 11:38:58 -07001682
Mike Frysinger3554bc92015-03-11 04:59:21 -04001683 valid_keys = set(_HOOK_FLAGS.iterkeys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001684 hooks = _HOOK_FLAGS.copy()
Mike Frysinger3554bc92015-03-11 04:59:21 -04001685
1686 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07001687 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001688 for flag in config.options(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001689 if flag not in valid_keys:
1690 raise ValueError('Error: unknown key "%s" in hook section of "%s"' %
1691 (flag, _CONFIG_FILE))
1692
Ryan Cui9b651632011-05-11 11:38:58 -07001693 try:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001694 enabled = config.getboolean(SECTION, flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001695 except ValueError as e:
Mike Frysinger3554bc92015-03-11 04:59:21 -04001696 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
1697 (flag, _CONFIG_FILE, e))
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001698 if enabled:
1699 enable_flags.append(flag)
1700 else:
1701 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001702
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001703 # See if this hook has custom options.
1704 if enabled:
1705 try:
1706 options = config.get(SECTION_OPTIONS, flag)
1707 hooks[flag] = functools.partial(hooks[flag], options=options.split())
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001708 hooks[flag].__name__ = flag
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001709 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
1710 pass
1711
1712 enabled_hooks = set(hooks[x] for x in enable_flags)
1713 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger3554bc92015-03-11 04:59:21 -04001714 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001715
1716
Jon Salz3ee59de2012-08-18 13:54:22 +08001717def _get_project_hook_scripts(config):
1718 """Returns a list of project-specific hook scripts.
1719
1720 Args:
1721 config: A ConfigParser for the project's config file.
1722 """
1723 SECTION = 'Hook Scripts'
1724 if not config.has_section(SECTION):
1725 return []
1726
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001727 return config.items(SECTION)
Jon Salz3ee59de2012-08-18 13:54:22 +08001728
1729
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001730def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001731 """Returns a list of hooks that need to be run for a project.
1732
1733 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001734
1735 Args:
1736 project: A string, name of the project.
1737 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001738 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001739 config = ConfigParser.RawConfigParser()
1740 try:
1741 config.read(_CONFIG_FILE)
1742 except ConfigParser.Error:
1743 # Just use an empty config file
1744 config = ConfigParser.RawConfigParser()
1745
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001746 if presubmit:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001747 hooks = _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001748 else:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001749 hooks = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001750
Mike Frysinger3554bc92015-03-11 04:59:21 -04001751 enabled_hooks, disabled_hooks = _get_override_hooks(config)
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001752 hooks = [hook for hook in hooks if hook not in disabled_hooks]
1753
1754 # If a list is both in _COMMON_HOOKS and also enabled explicitly through an
1755 # override, keep the override only. Note that the override may end up being
1756 # a functools.partial, in which case we need to extract the .func to compare
1757 # it to the common hooks.
1758 unwrapped_hooks = [getattr(hook, 'func', hook) for hook in enabled_hooks]
1759 hooks = [hook for hook in hooks if hook not in unwrapped_hooks]
1760
1761 hooks = list(enabled_hooks) + hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001762
1763 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001764 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1765 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001766
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001767 for name, script in _get_project_hook_scripts(config):
1768 func = functools.partial(_run_project_hook_script, script)
1769 func.__name__ = name
1770 hooks.append(func)
Jon Salz3ee59de2012-08-18 13:54:22 +08001771
Ryan Cui9b651632011-05-11 11:38:58 -07001772 return hooks
1773
1774
Alex Deymo643ac4c2015-09-03 10:40:50 -07001775def _run_project_hooks(project_name, proj_dir=None,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001776 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001777 """For each project run its project specific hook from the hooks dictionary.
1778
1779 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001780 project_name: The name of project to run hooks for.
Doug Anderson44a644f2011-11-02 10:37:37 -07001781 proj_dir: If non-None, this is the directory the project is in. If None,
1782 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001783 commit_list: A list of commits to run hooks against. If None or empty list
1784 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001785 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001786
1787 Returns:
1788 Boolean value of whether any errors were ecountered while running the hooks.
1789 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001790 if proj_dir is None:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001791 proj_dirs = _run_command(
1792 ['repo', 'forall', project_name, '-c', 'pwd']).split()
David James2edd9002013-10-11 14:09:19 -07001793 if len(proj_dirs) == 0:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001794 print('%s cannot be found.' % project_name, file=sys.stderr)
David James2edd9002013-10-11 14:09:19 -07001795 print('Please specify a valid project.', file=sys.stderr)
1796 return True
1797 if len(proj_dirs) > 1:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001798 print('%s is associated with multiple directories.' % project_name,
David James2edd9002013-10-11 14:09:19 -07001799 file=sys.stderr)
1800 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1801 return True
1802 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001803
Ryan Cuiec4d6332011-05-02 14:15:25 -07001804 pwd = os.getcwd()
1805 # hooks assume they are run from the root of the project
1806 os.chdir(proj_dir)
1807
Alex Deymo643ac4c2015-09-03 10:40:50 -07001808 remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
1809 '--symbolic-full-name', '@{u}']).strip()
1810 if not remote_branch:
1811 print('Your project %s doesn\'t track any remote repo.' % project_name,
1812 file=sys.stderr)
1813 remote = None
1814 else:
Josh Pratt15d13ab2018-08-13 11:52:48 +10001815 branch_items = remote_branch.split('/', 1)
1816 if len(branch_items) != 2:
1817 PrintErrorForProject(
1818 project_name,
1819 HookFailure(
1820 'Cannot get remote and branch name (%s)' % remote_branch))
1821 os.chdir(pwd)
1822 return True
1823 remote, _branch = branch_items
Alex Deymo643ac4c2015-09-03 10:40:50 -07001824
1825 project = Project(name=project_name, dir=proj_dir, remote=remote)
1826
Doug Anderson14749562013-06-26 13:38:29 -07001827 if not commit_list:
1828 try:
1829 commit_list = _get_commits()
1830 except VerifyException as e:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001831 PrintErrorForProject(project.name, HookFailure(str(e)))
Doug Anderson14749562013-06-26 13:38:29 -07001832 os.chdir(pwd)
1833 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001834
Alex Deymo643ac4c2015-09-03 10:40:50 -07001835 hooks = _get_project_hooks(project.name, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001836 error_found = False
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001837 commit_count = len(commit_list)
1838 for i, commit in enumerate(commit_list):
Ryan Cui1562fb82011-05-09 11:01:31 -07001839 error_list = []
Ryan Cui9b651632011-05-11 11:38:58 -07001840 for hook in hooks:
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001841 output = ('PRESUBMIT.cfg: [%i/%i]: %s: Running %s' %
1842 (i + 1, commit_count, commit, hook.__name__))
1843 print(output, end='\r')
1844 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001845 hook_error = hook(project, commit)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001846 print(' ' * len(output), end='\r')
1847 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001848 if hook_error:
1849 error_list.append(hook_error)
1850 error_found = True
1851 if error_list:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001852 PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
Ryan Cui1562fb82011-05-09 11:01:31 -07001853 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001854
Ryan Cuiec4d6332011-05-02 14:15:25 -07001855 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001856 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001857
Mike Frysingerae409522014-02-01 03:16:11 -05001858
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001859# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001860
Ryan Cui1562fb82011-05-09 11:01:31 -07001861
Mike Frysingerae409522014-02-01 03:16:11 -05001862def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001863 """Main function invoked directly by repo.
1864
1865 This function will exit directly upon error so that repo doesn't print some
1866 obscure error message.
1867
1868 Args:
1869 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001870 worktree_list: A list of directories. It should be the same length as
1871 project_list, so that each entry in project_list matches with a directory
1872 in worktree_list. If None, we will attempt to calculate the directories
1873 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08001874 kwargs: Leave this here for forward-compatibility.
1875 """
Ryan Cui1562fb82011-05-09 11:01:31 -07001876 found_error = False
David James2edd9002013-10-11 14:09:19 -07001877 if not worktree_list:
1878 worktree_list = [None] * len(project_list)
1879 for project, worktree in zip(project_list, worktree_list):
1880 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07001881 found_error = True
1882
Mike Frysingerae409522014-02-01 03:16:11 -05001883 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07001884 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07001885 '- To disable some source style checks, and for other hints, see '
1886 '<checkout_dir>/src/repohooks/README\n'
1887 '- To upload only current project, run \'repo upload .\'')
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04001888 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07001889 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07001890
Ryan Cui1562fb82011-05-09 11:01:31 -07001891
Doug Anderson44a644f2011-11-02 10:37:37 -07001892def _identify_project(path):
1893 """Identify the repo project associated with the given path.
1894
1895 Returns:
1896 A string indicating what project is associated with the path passed in or
1897 a blank string upon failure.
1898 """
1899 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001900 redirect_stderr=True, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07001901
1902
Mike Frysinger55f85b52014-12-18 14:45:21 -05001903def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07001904 """Run hooks directly (outside of the context of repo).
1905
Doug Anderson44a644f2011-11-02 10:37:37 -07001906 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05001907 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07001908
1909 Returns:
1910 0 if no pre-upload failures, 1 if failures.
1911
1912 Raises:
1913 BadInvocation: On some types of invocation errors.
1914 """
Mike Frysinger66142932014-12-18 14:55:57 -05001915 parser = commandline.ArgumentParser(description=__doc__)
1916 parser.add_argument('--dir', default=None,
1917 help='The directory that the project lives in. If not '
1918 'specified, use the git project root based on the cwd.')
1919 parser.add_argument('--project', default=None,
1920 help='The project repo path; this can affect how the '
1921 'hooks get run, since some hooks are project-specific. '
1922 'For chromite this is chromiumos/chromite. If not '
1923 'specified, the repo tool will be used to figure this '
1924 'out based on the dir.')
1925 parser.add_argument('--rerun-since', default=None,
Vadim Bendebury75447b92018-01-10 12:06:01 -08001926 help='Rerun hooks on old commits since some point '
1927 'in the past. The argument could be a date (should '
1928 'match git log\'s concept of a date, e.g. 2012-06-20), '
1929 'or a SHA1, or just a number of commits to check (from 1 '
1930 'to 99). This option is mutually exclusive with '
1931 '--pre-submit.')
Mike Frysinger66142932014-12-18 14:55:57 -05001932 parser.add_argument('--pre-submit', action="store_true",
1933 help='Run the check against the pending commit. '
1934 'This option should be used at the \'git commit\' '
1935 'phase as opposed to \'repo upload\'. This option '
1936 'is mutually exclusive with --rerun-since.')
1937 parser.add_argument('commits', nargs='*',
1938 help='Check specific commits')
1939 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07001940
Doug Anderson14749562013-06-26 13:38:29 -07001941 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05001942 if opts.commits:
Doug Anderson14749562013-06-26 13:38:29 -07001943 raise BadInvocation('Can\'t pass commits and use rerun-since: %s' %
Mike Frysinger66142932014-12-18 14:55:57 -05001944 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07001945
Vadim Bendebury75447b92018-01-10 12:06:01 -08001946 if len(opts.rerun_since) < 3 and opts.rerun_since.isdigit():
1947 # This must be the number of commits to check. We don't expect the user
1948 # to want to check more than 99 commits.
1949 limit = '-n%s' % opts.rerun_since
1950 elif git.IsSHA1(opts.rerun_since, False):
1951 limit = '%s..' % opts.rerun_since
1952 else:
1953 # This better be a date.
1954 limit = '--since=%s' % opts.rerun_since
1955 cmd = ['git', 'log', limit, '--pretty=%H']
Doug Anderson14749562013-06-26 13:38:29 -07001956 all_commits = _run_command(cmd).splitlines()
1957 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
1958
1959 # Eliminate chrome-bot commits but keep ordering the same...
1960 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05001961 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07001962
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001963 if opts.pre_submit:
1964 raise BadInvocation('rerun-since and pre-submit can not be '
1965 'used together')
1966 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05001967 if opts.commits:
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001968 raise BadInvocation('Can\'t pass commits and use pre-submit: %s' %
Mike Frysinger66142932014-12-18 14:55:57 -05001969 ' '.join(opts.commits))
1970 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07001971
1972 # Check/normlaize git dir; if unspecified, we'll use the root of the git
1973 # project from CWD
1974 if opts.dir is None:
1975 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001976 redirect_stderr=True).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07001977 if not git_dir:
1978 raise BadInvocation('The current directory is not part of a git project.')
1979 opts.dir = os.path.dirname(os.path.abspath(git_dir))
1980 elif not os.path.isdir(opts.dir):
1981 raise BadInvocation('Invalid dir: %s' % opts.dir)
1982 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
1983 raise BadInvocation('Not a git directory: %s' % opts.dir)
1984
1985 # Identify the project if it wasn't specified; this _requires_ the repo
1986 # tool to be installed and for the project to be part of a repo checkout.
1987 if not opts.project:
1988 opts.project = _identify_project(opts.dir)
1989 if not opts.project:
1990 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
1991
Doug Anderson14749562013-06-26 13:38:29 -07001992 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05001993 commit_list=opts.commits,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001994 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07001995 if found_error:
1996 return 1
1997 return 0
1998
1999
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002000if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05002001 sys.exit(direct_main(sys.argv[1:]))