blob: 19d5d0304903ef7374154ff016a491e254a52171 [file] [log] [blame]
Mike Frysinger4f994402019-09-13 17:40:45 -04001#!/usr/bin/env python3
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
Tom Hughes40bdce52020-08-25 08:57:03 -07009You can add new checks by adding a function to the HOOKS constants.
Mike Frysingerae409522014-02-01 03:16:11 -050010"""
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
Mike Frysingerf252cfc2019-12-09 17:34:28 -050016import configparser
Keigo Oka7e880ac2019-07-03 15:03:43 +090017import datetime
Daniel Erate3ea3fc2015-02-13 15:27:52 -070018import fnmatch
Jon Salz3ee59de2012-08-18 13:54:22 +080019import functools
Dale Curtis2975c432011-05-03 17:25:20 -070020import json
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070021import os
Stephen Boyd6bf5ea82020-10-15 00:02:07 -070022import pathlib
Ryan Cuiec4d6332011-05-02 14:15:25 -070023import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070024import sys
Peter Ammon811f6702014-06-12 15:45:38 -070025import stat
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -050026import subprocess
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070027
Ryan Cui1562fb82011-05-09 11:01:31 -070028from errors import (VerifyException, HookFailure, PrintErrorForProject,
29 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070030
Mike Frysinger919c7032019-09-13 17:48:08 -040031if __name__ in ('__builtin__', 'builtins'):
Mike Frysinger653cd262019-09-20 14:05:02 -040032 # If repo imports us, the __name__ will be __builtin__, and the cwd will be in
33 # the top level of the checkout (i.e. $CHROMEOS_CHECKOUT). chromite will be
34 # in that directory, so add it to our path. This works whether we're running
35 # the repo in $CHROMEOS_CHECKOUT/.repo/repo/ or a custom version in a
36 # completely different tree.
37 # TODO(vapier): Python 2 used "__builtin__" while Python 3 uses "builtins".
Mike Frysinger6850d512018-05-21 12:12:14 -040038 sys.path.insert(0, os.getcwd())
39
Mike Frysinger653cd262019-09-20 14:05:02 -040040elif __name__ == '__main__':
41 # If we're run directly, we'll find chromite relative to the repohooks dir in
42 # $CHROMEOS_CHECKOUT/src/repohooks, so go up two dirs.
David Jamesc3b68b32013-04-03 09:17:03 -070043 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
44
Mike Frysingerfd481ce2019-09-13 18:14:48 -040045# The sys.path monkey patching confuses the linter.
46# pylint: disable=wrong-import-position
Mike Frysinger66142932014-12-18 14:55:57 -050047from chromite.lib import commandline
Rahul Chaudhry0e515342015-08-07 12:00:43 -070048from chromite.lib import cros_build_lib
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050049from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070050from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070051from chromite.lib import patch
Mike Frysingered1b95a2019-12-12 19:04:51 -050052from chromite.lib import terminal
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040053from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070054
Mike Frysingerff4768e2020-02-27 18:48:13 -050055
56assert sys.version_info >= (3, 6), 'This module requires Python 3.6+'
57
58
Vadim Bendebury2b62d742014-06-22 13:14:51 -070059PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070060
Mike Frysingered9b2a02019-12-12 18:52:32 -050061
62# Link to commit message documentation for users.
63DOC_COMMIT_MSG_URL = ('https://chromium.googlesource.com/chromiumos/docs/+/HEAD'
64 '/contributing.md#commit-messages')
65
66
Ryan Cuiec4d6332011-05-02 14:15:25 -070067COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050068 # C++ and friends
Mike Frysinger24dd3c52019-08-17 14:22:48 -040069 r'.*\.c$', r'.*\.cc$', r'.*\.cpp$', r'.*\.h$', r'.*\.m$', r'.*\.mm$',
70 r'.*\.inl$', r'.*\.asm$', r'.*\.hxx$', r'.*\.hpp$', r'.*\.s$', r'.*\.S$',
Mike Frysingerae409522014-02-01 03:16:11 -050071 # Scripts
Mike Frysinger24dd3c52019-08-17 14:22:48 -040072 r'.*\.js$', r'.*\.py$', r'.*\.sh$', r'.*\.rb$', r'.*\.pl$', r'.*\.pm$',
Bob Haarman0dc1f942020-10-03 00:06:59 +000073 # No extension at all, note that ALL CAPS files are excluded by
Mike Frysingerae409522014-02-01 03:16:11 -050074 # COMMON_EXCLUDED_LIST below.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040075 r'(^|.*[\\\/])[^.]+$',
Mike Frysingerae409522014-02-01 03:16:11 -050076 # Other
Mike Frysinger24dd3c52019-08-17 14:22:48 -040077 r'.*\.java$', r'.*\.mk$', r'.*\.am$',
Harry Cutts24a75b62020-12-04 12:03:14 -080078 r'.*\.policy$', r'.*\.rules$', r'.*\.conf$', r'.*\.go$',
Mike Frysinger24dd3c52019-08-17 14:22:48 -040079 r'(^OWNERS|/OWNERS)',
Ryan Cuiec4d6332011-05-02 14:15:25 -070080]
81
Ryan Cui1562fb82011-05-09 11:01:31 -070082
Ryan Cuiec4d6332011-05-02 14:15:25 -070083COMMON_EXCLUDED_PATHS = [
Daniel Erate3ea3fc2015-02-13 15:27:52 -070084 # For ebuild trees, ignore any caches and manifest data.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040085 r'.*/Manifest$',
Mike Frysingereb6b60d2021-02-10 01:16:49 -050086 r'.*/files/srcuris$',
Mike Frysinger24dd3c52019-08-17 14:22:48 -040087 r'.*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$',
Doug Anderson5bfb6792011-10-25 16:45:41 -070088
Daniel Erate3ea3fc2015-02-13 15:27:52 -070089 # Ignore profiles data (like overlay-tegra2/profiles).
Mike Frysinger24dd3c52019-08-17 14:22:48 -040090 r'(^|.*/)overlay-.*/profiles/.*',
91 r'^profiles/.*$',
Mike Frysinger98638102014-08-28 00:15:08 -040092
C Shapiro8f90e9b2017-06-28 09:54:50 -060093 # Ignore config files in ebuild setup.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040094 r'(^|.*/)overlay-.*/chromeos-base/chromeos-bsp.*/files/.*',
95 r'^chromeos-base/chromeos-bsp.*/files/.*',
C Shapiro8f90e9b2017-06-28 09:54:50 -060096
Daniel Erate3ea3fc2015-02-13 15:27:52 -070097 # Ignore minified js and jquery.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040098 r'.*\.min\.js',
99 r'.*jquery.*\.js',
Mike Frysinger33a458d2014-03-03 17:00:51 -0500100
101 # Ignore license files as the content is often taken verbatim.
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400102 r'.*/licenses/.*',
Alex Klein619c0912019-01-30 17:13:23 -0700103
Mike Frysinger13650402019-07-31 14:31:46 -0400104 # Exclude generated protobuf bindings.
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400105 r'.*_pb2\.py$',
106 r'.*\.pb\.go$',
Ryan Cuiec4d6332011-05-02 14:15:25 -0700107]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700108
Ken Turnerd07564b2018-02-08 17:57:59 +1100109LICENSE_EXCLUDED_PATHS = [
LaMont Jones775c2b32020-02-19 17:32:54 -0700110 r'^(.*/)?OWNERS(\..*)?$',
Tom Hughes90b7bd42020-11-10 10:31:49 -0800111 r'^(.*/)?DIR_METADATA(\..*)?$',
Ken Turnerd07564b2018-02-08 17:57:59 +1100112]
Ryan Cui1562fb82011-05-09 11:01:31 -0700113
Ryan Cui9b651632011-05-11 11:38:58 -0700114_CONFIG_FILE = 'PRESUBMIT.cfg'
115
116
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700117# File containing wildcards, one per line, matching files that should be
118# excluded from presubmit checks. Lines beginning with '#' are ignored.
119_IGNORE_FILE = '.presubmitignore'
120
Cheng Yuehb707c522020-01-02 14:06:59 +0800121
122TEST_FIELD_RE = r'\nTEST=\S+'
123
Daisuke Nojiri2089e012020-08-20 15:12:36 -0700124BLOCKED_TERMS_FILE = 'blocked_terms.txt'
125UNBLOCKED_TERMS_FILE = 'unblocked_terms.txt'
126
Doug Anderson44a644f2011-11-02 10:37:37 -0700127# Exceptions
128
129
130class BadInvocation(Exception):
131 """An Exception indicating a bad invocation of the program."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700132
133
Ryan Cui1562fb82011-05-09 11:01:31 -0700134# General Helpers
135
Sean Paulba01d402011-05-05 11:36:23 -0400136
Mike Frysingerb2496652019-09-12 23:35:46 -0400137class Cache(object):
138 """General helper for caching git content."""
139
140 def __init__(self):
141 self._cache = {}
142
143 def get_subcache(self, scope):
144 return self._cache.setdefault(scope, {})
145
146 def clear(self):
147 self._cache.clear()
148
149CACHE = Cache()
150
151
Alex Deymo643ac4c2015-09-03 10:40:50 -0700152Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
153
154
Mike Frysinger526a5f82019-09-13 18:05:30 -0400155def _run_command(cmd, **kwargs):
Doug Anderson44a644f2011-11-02 10:37:37 -0700156 """Executes the passed in command and returns raw stdout output.
157
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400158 This is a convenience func to set some run defaults differently.
Mike Frysinger526a5f82019-09-13 18:05:30 -0400159
Doug Anderson44a644f2011-11-02 10:37:37 -0700160 Args:
161 cmd: The command to run; should be a list of strings.
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400162 **kwargs: Same as cros_build_lib.run.
Doug Anderson44a644f2011-11-02 10:37:37 -0700163
164 Returns:
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700165 The stdout from the process (discards stderr and returncode).
Doug Anderson44a644f2011-11-02 10:37:37 -0700166 """
Mike Frysinger526a5f82019-09-13 18:05:30 -0400167 kwargs.setdefault('print_cmd', False)
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400168 kwargs.setdefault('stdout', True)
169 kwargs.setdefault('check', False)
Mike Frysinger054c1592019-11-18 03:51:54 -0500170 result = cros_build_lib.run(cmd, **kwargs)
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400171 # NB: We decode this directly rather than through kwargs as our tests rely
172 # on this post-processing behavior currently.
Mike Frysinger71e643e2019-09-13 17:26:39 -0400173 return result.output.decode('utf-8', 'replace')
Ryan Cui72834d12011-05-05 14:51:33 -0700174
Ryan Cui1562fb82011-05-09 11:01:31 -0700175
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700176def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700177 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700178 if __name__ == '__main__':
179 # Works when file is run on its own (__file__ is defined)...
180 return os.path.abspath(os.path.dirname(__file__))
181 else:
182 # We need to do this when we're run through repo. Since repo executes
183 # us with execfile(), we don't get __file__ defined.
184 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
185 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700186
Ryan Cui1562fb82011-05-09 11:01:31 -0700187
Ryan Cuiec4d6332011-05-02 14:15:25 -0700188def _match_regex_list(subject, expressions):
189 """Try to match a list of regular expressions to a string.
190
191 Args:
192 subject: The string to match regexes on
193 expressions: A list of regular expressions to check for matches with.
194
195 Returns:
196 Whether the passed in subject matches any of the passed in regexes.
197 """
198 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500199 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700200 return True
201 return False
202
Ryan Cui1562fb82011-05-09 11:01:31 -0700203
Mike Frysingerae409522014-02-01 03:16:11 -0500204def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700205 """Filter out files based on the conditions passed in.
206
207 Args:
208 files: list of filepaths to filter
209 include_list: list of regex that when matched with a file path will cause it
210 to be added to the output list unless the file is also matched with a
211 regex in the exclude_list.
212 exclude_list: list of regex that when matched with a file will prevent it
213 from being added to the output list, even if it is also matched with a
214 regex in the include_list.
215
216 Returns:
217 A list of filepaths that contain files matched in the include_list and not
218 in the exclude_list.
219 """
220 filtered = []
221 for f in files:
222 if (_match_regex_list(f, include_list) and
223 not _match_regex_list(f, exclude_list)):
224 filtered.append(f)
225 return filtered
226
Ryan Cuiec4d6332011-05-02 14:15:25 -0700227
228# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700229
230
Ryan Cui4725d952011-05-05 15:41:19 -0700231def _get_upstream_branch():
232 """Returns the upstream tracking branch of the current branch.
233
234 Raises:
235 Error if there is no tracking branch
236 """
237 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
238 current_branch = current_branch.replace('refs/heads/', '')
239 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700240 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700241
242 cfg_option = 'branch.' + current_branch + '.%s'
243 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
244 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
245 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700246 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700247
248 return full_upstream.replace('heads', 'remotes/' + remote)
249
Ryan Cui1562fb82011-05-09 11:01:31 -0700250
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700251def _get_patch(commit):
252 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700253 if commit == PRE_SUBMIT:
254 return _run_command(['git', 'diff', '--cached', 'HEAD'])
255 else:
256 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700257
Ryan Cui1562fb82011-05-09 11:01:31 -0700258
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500259def _get_file_content(path, commit):
260 """Returns the content of a file at a specific commit.
261
262 We can't rely on the file as it exists in the filesystem as people might be
263 uploading a series of changes which modifies the file multiple times.
264
265 Note: The "content" of a symlink is just the target. So if you're expecting
266 a full file, you should check that first. One way to detect is that the
267 content will not have any newlines.
268 """
Mike Frysingerb2496652019-09-12 23:35:46 -0400269 # Make sure people don't accidentally pass in full paths which will never
270 # work. You need to use relative=True with _get_affected_files.
271 if path.startswith('/'):
272 raise ValueError('_get_file_content must be called with relative paths: %s'
273 % (path,))
274
275 # {<commit>: {<path1>: <content>, <path2>: <content>}}
276 cache = CACHE.get_subcache('get_file_content')
277 if path in cache:
278 return cache[path]
279
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700280 if commit == PRE_SUBMIT:
Mike Frysingerd9ed85f2021-02-03 12:38:24 -0500281 content = _run_command(['git', 'diff', 'HEAD', '--', path], stderr=True)
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700282 else:
Mike Frysingerd9ed85f2021-02-03 12:38:24 -0500283 content = _run_command(['git', 'show', '%s:%s' % (commit, path)],
284 stderr=True)
Mike Frysingerb2496652019-09-12 23:35:46 -0400285 cache[path] = content
286 return content
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500287
288
Mike Frysingerae409522014-02-01 03:16:11 -0500289def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700290 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysingerb2496652019-09-12 23:35:46 -0400291 # {<commit>: {<path1>: <content>, <path2>: <content>}}
292 cache = CACHE.get_subcache('get_file_diff')
293 if path in cache:
294 return cache[path]
295
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700296 if commit == PRE_SUBMIT:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800297 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff', 'HEAD',
Yu-Ping Wue3e47362020-09-22 15:54:40 +0800298 '--', path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700299 else:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800300 command = ['git', 'show', '-p', '--pretty=format:', '--no-ext-diff', commit,
Yu-Ping Wue3e47362020-09-22 15:54:40 +0800301 '--', path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700302 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700303
304 new_lines = []
305 line_num = 0
306 for line in output.splitlines():
307 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
308 if m:
309 line_num = int(m.groups(1)[0])
310 continue
311 if line.startswith('+') and not line.startswith('++'):
Mike Frysinger71e643e2019-09-13 17:26:39 -0400312 new_lines.append((line_num, line[1:]))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700313 if not line.startswith('-'):
314 line_num += 1
Mike Frysingerb2496652019-09-12 23:35:46 -0400315 cache[path] = new_lines
Ryan Cuiec4d6332011-05-02 14:15:25 -0700316 return new_lines
317
Ryan Cui1562fb82011-05-09 11:01:31 -0700318
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700319def _get_ignore_wildcards(directory, cache):
320 """Get wildcards listed in a directory's _IGNORE_FILE.
321
322 Args:
323 directory: A string containing a directory path.
324 cache: A dictionary (opaque to caller) caching previously-read wildcards.
325
326 Returns:
327 A list of wildcards from _IGNORE_FILE or an empty list if _IGNORE_FILE
328 wasn't present.
329 """
330 # In the cache, keys are directories and values are lists of wildcards from
331 # _IGNORE_FILE within those directories (and empty if no file was present).
332 if directory not in cache:
333 wildcards = []
334 dotfile_path = os.path.join(directory, _IGNORE_FILE)
335 if os.path.exists(dotfile_path):
336 # TODO(derat): Consider using _get_file_content() to get the file as of
337 # this commit instead of the on-disk version. This may have a noticeable
338 # performance impact, as each call to _get_file_content() runs git.
339 with open(dotfile_path, 'r') as dotfile:
340 for line in dotfile.readlines():
341 line = line.strip()
342 if line.startswith('#'):
343 continue
344 if line.endswith('/'):
345 line += '*'
346 wildcards.append(line)
347 cache[directory] = wildcards
348
349 return cache[directory]
350
351
352def _path_is_ignored(path, cache):
353 """Check whether a path is ignored by _IGNORE_FILE.
354
355 Args:
356 path: A string containing a path.
357 cache: A dictionary (opaque to caller) caching previously-read wildcards.
358
359 Returns:
360 True if a file named _IGNORE_FILE in one of the passed-in path's parent
361 directories contains a wildcard matching the path.
362 """
363 # Skip ignore files.
364 if os.path.basename(path) == _IGNORE_FILE:
365 return True
366
367 path = os.path.abspath(path)
368 base = os.getcwd()
369
370 prefix = os.path.dirname(path)
371 while prefix.startswith(base):
372 rel_path = path[len(prefix) + 1:]
373 for wildcard in _get_ignore_wildcards(prefix, cache):
374 if fnmatch.fnmatch(rel_path, wildcard):
375 return True
376 prefix = os.path.dirname(prefix)
377
378 return False
379
380
Mike Frysinger292b45d2014-11-25 01:17:10 -0500381def _get_affected_files(commit, include_deletes=False, relative=False,
382 include_symlinks=False, include_adds=True,
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700383 full_details=False, use_ignore_files=True):
Peter Ammon811f6702014-06-12 15:45:38 -0700384 """Returns list of file paths that were modified/added, excluding symlinks.
385
386 Args:
387 commit: The commit
388 include_deletes: If true, we'll include deleted files in the result
389 relative: Whether to return relative or full paths to files
Mike Frysinger292b45d2014-11-25 01:17:10 -0500390 include_symlinks: If true, we'll include symlinks in the result
391 include_adds: If true, we'll include new files in the result
392 full_details: If False, return filenames, else return structured results.
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700393 use_ignore_files: Whether we ignore files matched by _IGNORE_FILE files.
Peter Ammon811f6702014-06-12 15:45:38 -0700394
395 Returns:
396 A list of modified/added (and perhaps deleted) files
397 """
Mike Frysinger292b45d2014-11-25 01:17:10 -0500398 if not relative and full_details:
399 raise ValueError('full_details only supports relative paths currently')
400
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700401 if commit == PRE_SUBMIT:
402 return _run_command(['git', 'diff-index', '--cached',
403 '--name-only', 'HEAD']).split()
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500404
405 path = os.getcwd()
Mike Frysingerb2496652019-09-12 23:35:46 -0400406 # {<commit>: {<path1>: <content>, <path2>: <content>}}
407 cache = CACHE.get_subcache('get_affected_files')
408 if path not in cache:
409 cache[path] = git.RawDiff(path, '%s^!' % commit)
410 files = cache[path]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500411
412 # Filter out symlinks.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500413 if not include_symlinks:
414 files = [x for x in files if not stat.S_ISLNK(int(x.dst_mode, 8))]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500415
416 if not include_deletes:
417 files = [x for x in files if x.status != 'D']
418
Mike Frysinger292b45d2014-11-25 01:17:10 -0500419 if not include_adds:
420 files = [x for x in files if x.status != 'A']
421
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700422 if use_ignore_files:
423 cache = {}
424 is_ignored = lambda x: _path_is_ignored(x.dst_file or x.src_file, cache)
425 files = [x for x in files if not is_ignored(x)]
426
Mike Frysinger292b45d2014-11-25 01:17:10 -0500427 if full_details:
428 # Caller wants the raw objects to parse status/etc... themselves.
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500429 return files
430 else:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500431 # Caller only cares about filenames.
432 files = [x.dst_file if x.dst_file else x.src_file for x in files]
433 if relative:
434 return files
435 else:
436 return [os.path.join(path, x) for x in files]
Peter Ammon811f6702014-06-12 15:45:38 -0700437
438
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700439def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700440 """Returns a list of commits for this review."""
Mike Frysingere300c7d2019-09-12 23:33:52 -0400441 cmd = ['git', 'log', '--no-merges', '--format=%H',
442 '%s..' % _get_upstream_branch()]
Ryan Cui72834d12011-05-05 14:51:33 -0700443 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700444
Ryan Cui1562fb82011-05-09 11:01:31 -0700445
Ryan Cuiec4d6332011-05-02 14:15:25 -0700446def _get_commit_desc(commit):
447 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700448 if commit == PRE_SUBMIT:
449 return ''
Mike Frysingerb2496652019-09-12 23:35:46 -0400450
451 # {<commit>: <content>}
452 cache = CACHE.get_subcache('get_commit_desc')
453 if commit not in cache:
Mike Frysinger4efdee72019-11-04 10:57:01 -0500454 cache[commit] = _run_command(['git', 'log', '--format=%B',
Mike Frysingerb2496652019-09-12 23:35:46 -0400455 commit + '^!'])
456 return cache[commit]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700457
458
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800459def _check_lines_in_diff(commit, files, check_callable, error_description):
460 """Checks given file for errors via the given check.
461
462 This is a convenience function for common per-line checks. It goes through all
463 files and returns a HookFailure with the error description listing all the
464 failures.
465
466 Args:
467 commit: The commit we're working on.
468 files: The files to check.
469 check_callable: A callable that takes a line and returns True if this line
470 _fails_ the check.
471 error_description: A string describing the error.
472 """
473 errors = []
474 for afile in files:
475 for line_num, line in _get_file_diff(afile, commit):
Bernie Thompson8e26f742020-07-23 14:32:31 -0700476 result = check_callable(line)
477 if result:
478 msg = f'{afile}, line {line_num}'
479 if isinstance(result, str):
480 msg += f': {result}'
481 errors.append(msg)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800482 if errors:
483 return HookFailure(error_description, errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400484 return None
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800485
486
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900487def _parse_common_inclusion_options(options):
488 """Parses common hook options for including/excluding files.
489
490 Args:
491 options: Option string list.
492
493 Returns:
494 (included, excluded) where each one is a list of regex strings.
495 """
496 parser = argparse.ArgumentParser()
497 parser.add_argument('--exclude_regex', action='append')
498 parser.add_argument('--include_regex', action='append')
499 opts = parser.parse_args(options)
500 included = opts.include_regex or []
501 excluded = opts.exclude_regex or []
502 return included, excluded
503
504
Ryan Cuiec4d6332011-05-02 14:15:25 -0700505# Common Hooks
506
Ryan Cui1562fb82011-05-09 11:01:31 -0700507
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900508def _check_no_long_lines(_project, commit, options=()):
Mike Frysinger55f85b52014-12-18 14:45:21 -0500509 """Checks there are no lines longer than MAX_LEN in any of the text files."""
Keigo Oka9732e382019-06-28 17:44:59 +0900510 LONG_LINE_OK_PATHS = [
511 # Go has no line length limit.
512 # https://golang.org/doc/effective_go.html#formatting
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400513 r'.*\.go$',
Mike Frysingerf8961942020-05-15 00:36:31 -0400514 # Python does its own long line checks via pylint.
515 r'.*\.py$',
Keigo Oka9732e382019-06-28 17:44:59 +0900516 ]
Mike Frysinger55f85b52014-12-18 14:45:21 -0500517
Ayato Tokubi5aae3f72020-01-16 17:43:47 +0900518 DEFAULT_MAX_LENGTHS = [
519 # Java's line length limit is 100 chars.
520 # https://chromium.googlesource.com/chromium/src/+/master/styleguide/java/java.md
521 (r'.*\.java$', 100),
Mike Frysingerf8961942020-05-15 00:36:31 -0400522 # Rust's line length limit is 100 chars.
523 (r'.*\.rs$', 100),
Ayato Tokubi5aae3f72020-01-16 17:43:47 +0900524 ]
525
Ryan Cuiec4d6332011-05-02 14:15:25 -0700526 MAX_LEN = 80
527
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900528 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700529 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900530 included + COMMON_INCLUDED_PATHS,
Keigo Oka9732e382019-06-28 17:44:59 +0900531 excluded + COMMON_EXCLUDED_PATHS + LONG_LINE_OK_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700532
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900533 errors = []
Ryan Cuiec4d6332011-05-02 14:15:25 -0700534 for afile in files:
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700535 skip_regexps = (
536 r'https?://',
537 r'^#\s*(define|include|import|pragma|if|ifndef|endif)\b',
538 )
539
Ayato Tokubi5aae3f72020-01-16 17:43:47 +0900540 max_len = MAX_LEN
541
542 for expr, length in DEFAULT_MAX_LENGTHS:
543 if re.search(expr, afile):
544 max_len = length
545 break
546
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700547 if os.path.basename(afile).startswith('OWNERS'):
548 # File paths can get long, and there's no way to break them up into
549 # multiple lines.
550 skip_regexps += (
551 r'^include\b',
552 r'file:',
553 )
554
555 skip_regexps = [re.compile(x) for x in skip_regexps]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700556 for line_num, line in _get_file_diff(afile, commit):
557 # Allow certain lines to exceed the maxlen rule.
Ayato Tokubi5aae3f72020-01-16 17:43:47 +0900558 if len(line) <= max_len or any(x.search(line) for x in skip_regexps):
Jon Salz98255932012-08-18 14:48:02 +0800559 continue
560
Ayato Tokubi5aae3f72020-01-16 17:43:47 +0900561 errors.append('%s, line %s, %s chars, over %s chars' %
562 (afile, line_num, len(line), max_len))
Jon Salz98255932012-08-18 14:48:02 +0800563 if len(errors) == 5: # Just show the first 5 errors.
564 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700565
566 if errors:
Ayato Tokubi5aae3f72020-01-16 17:43:47 +0900567 msg = 'Found lines longer than the limit (first 5 shown):'
Ryan Cui1562fb82011-05-09 11:01:31 -0700568 return HookFailure(msg, errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400569 return None
Ryan Cui1562fb82011-05-09 11:01:31 -0700570
Ryan Cuiec4d6332011-05-02 14:15:25 -0700571
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900572def _check_no_stray_whitespace(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700573 """Checks that there is no stray whitespace at source lines end."""
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900574 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700575 files = _filter_files(_get_affected_files(commit),
Tomasz Figa375b9622020-09-22 14:25:07 +0000576 included + COMMON_INCLUDED_PATHS + [r'^.*\.md$'],
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900577 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800578 return _check_lines_in_diff(commit, files,
579 lambda line: line.rstrip() != line,
580 'Found line ending with white space in:')
Ryan Cui1562fb82011-05-09 11:01:31 -0700581
Ryan Cuiec4d6332011-05-02 14:15:25 -0700582
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900583def _check_no_tabs(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700584 """Checks there are no unexpanded tabs."""
Mike Frysingercd134512017-10-26 04:36:33 -0400585 # Don't add entire repos here. Update the PRESUBMIT.cfg in each repo instead.
Bob Haarman0dc1f942020-10-03 00:06:59 +0000586 # We only allow known specific filetypes here that show up in all repos.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700587 TAB_OK_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400588 r'.*\.ebuild$',
589 r'.*\.eclass$',
590 r'.*\.go$',
591 r'.*/[M|m]akefile$',
592 r'.*\.mk$',
Ryan Cuiec4d6332011-05-02 14:15:25 -0700593 ]
594
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900595 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700596 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900597 included + COMMON_INCLUDED_PATHS,
598 excluded + COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800599 return _check_lines_in_diff(commit, files,
600 lambda line: '\t' in line,
601 'Found a tab character in:')
Ryan Cuiec4d6332011-05-02 14:15:25 -0700602
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800603
Daisuke Nojiri2089e012020-08-20 15:12:36 -0700604def _read_terms_file(terms_file):
605 """Read list of words from file, skipping comments and blank lines."""
606 file_terms = set()
607 for line in osutils.ReadFile(terms_file).splitlines():
608 # Allow comment and blank lines.
609 line = line.split('#', 1)[0]
610 if not line:
611 continue
612 file_terms.add(line)
613 return file_terms
614
615
Bernie Thompson8e26f742020-07-23 14:32:31 -0700616def _check_keywords(_project, commit, options=()):
617 """Checks there are no blocked keywords in commit content."""
Daisuke Nojiri2089e012020-08-20 15:12:36 -0700618 # Read options from override list.
Bernie Thompson8e26f742020-07-23 14:32:31 -0700619 parser = argparse.ArgumentParser()
620 parser.add_argument('--exclude_regex', action='append', default=[])
621 parser.add_argument('--include_regex', action='append', default=[])
622 parser.add_argument('--block', action='append', default=[])
623 parser.add_argument('--unblock', action='append', default=[])
624 opts = parser.parse_args(options)
625
Daisuke Nojiri2089e012020-08-20 15:12:36 -0700626 # Read blocked word list.
627 blocked_terms_file = os.path.join(_get_hooks_dir(), BLOCKED_TERMS_FILE)
628 common_keywords = _read_terms_file(blocked_terms_file)
629
630 # Read unblocked word list. Global list is skipped if local list exists.
631 unblocked_terms_file = os.path.join(_get_hooks_dir(), UNBLOCKED_TERMS_FILE)
632 if os.path.isfile(os.path.join(_project.dir, UNBLOCKED_TERMS_FILE)):
633 unblocked_terms_file = os.path.join(_project.dir, UNBLOCKED_TERMS_FILE)
634 unblocked_words = _read_terms_file(unblocked_terms_file)
635 unblocked_words.update(opts.unblock)
636
Bernie Thompson8e26f742020-07-23 14:32:31 -0700637 keywords = set(common_keywords | set(opts.block))
Daisuke Nojiri2089e012020-08-20 15:12:36 -0700638 keywords = sorted(keywords - unblocked_words)
Mike Frysinger6140f572020-09-10 04:52:46 -0400639 files = _filter_files(
640 _get_affected_files(commit),
Tomasz Figa375b9622020-09-22 14:25:07 +0000641 opts.include_regex + COMMON_INCLUDED_PATHS + [r'^.*\.md$'],
Mike Frysinger6140f572020-09-10 04:52:46 -0400642 opts.exclude_regex + COMMON_EXCLUDED_PATHS)
Bernie Thompson8e26f742020-07-23 14:32:31 -0700643 errors = []
644
645 def _check_line(line):
Laurent Chavey434af9a2020-09-28 22:25:16 +0900646 # Store information about each span matching blocking regex.
647 # to match unblocked regex with blocked reg ex match.
648 # [{'span':re.span, - overlap of matching regex in line
649 # 'group':re.group, - matching term
650 # 'blocked':bool, - True: matching blocked, False: matching unblocked
651 # 'keyword':regex, - block regex
652 # }, ...]
653 blocked_span = []
654 # Store information about each span matching unblocking regex.
655 # [re.span, ...]
656 unblocked_span = []
657
Bernie Thompson8e26f742020-07-23 14:32:31 -0700658 for word in keywords:
Laurent Chavey434af9a2020-09-28 22:25:16 +0900659 for match in re.finditer(word, line, flags=re.I):
660 blocked_span.append({'span' : match.span(),
661 'group' : match.group(0),
662 'blocked' : True,
663 'keyword' : word})
664
665 for unblocked in unblocked_words:
666 for match in re.finditer(unblocked, line, flags=re.I):
667 unblocked_span.append(match.span())
668
669 # Unblock terms that are superset of blocked terms:
670 # blocked := "this.?word"
671 # unblocked := "\.this.?word"
672 # "this line is blocked because of this1word"
673 # "this line is unblocked because of thenew.this1word"
674 #
675 for b in blocked_span:
676 for ub in unblocked_span:
677 if ub[0] <= b['span'][0] and ub[1] >= b['span'][1]:
678 b['blocked'] = False
679 if b['blocked']:
680 return f'Matched "{b["group"]}" with regex of "{b["keyword"]}"'
Bernie Thompson8e26f742020-07-23 14:32:31 -0700681 return False
682
683 diff_errors = _check_lines_in_diff(commit, files, _check_line,
684 'Found a blocked keyword in:')
685 if diff_errors:
686 errors.append(diff_errors)
687
688 line_num = 1
689 commit_desc_errors = []
690 for line in _get_commit_desc(commit).splitlines():
691 result = _check_line(line)
692 if result:
693 commit_desc_errors.append('Commit message, line %s: %s' %
694 (line_num, result))
695 line_num += 1
696 if commit_desc_errors:
697 errors.append(HookFailure('Found a blocked keyword in:',
698 commit_desc_errors))
699 return errors
700
701
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900702def _check_tabbed_indents(_project, commit, options=()):
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800703 """Checks that indents use tabs only."""
704 TABS_REQUIRED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400705 r'.*\.ebuild$',
706 r'.*\.eclass$',
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800707 ]
708 LEADING_SPACE_RE = re.compile('[\t]* ')
709
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900710 included, excluded = _parse_common_inclusion_options(options)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800711 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900712 included + TABS_REQUIRED_PATHS,
713 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800714 return _check_lines_in_diff(
715 commit, files,
716 lambda line: LEADING_SPACE_RE.match(line) is not None,
717 'Found a space in indentation (must be all tabs):')
Ryan Cui1562fb82011-05-09 11:01:31 -0700718
Ryan Cuiec4d6332011-05-02 14:15:25 -0700719
LaMont Jones872fe762020-02-10 15:36:14 -0700720def _check_gofmt(_project, commit, options=()):
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700721 """Checks that Go files are formatted with gofmt."""
LaMont Jones872fe762020-02-10 15:36:14 -0700722 included, excluded = _parse_common_inclusion_options(options)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700723 errors = []
724 files = _filter_files(_get_affected_files(commit, relative=True),
LaMont Jones872fe762020-02-10 15:36:14 -0700725 included + [r'\.go$'],
726 excluded)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700727
728 for gofile in files:
729 contents = _get_file_content(gofile, commit)
Mike Frysingeraa7dc942019-09-25 00:07:24 -0400730 output = _run_command(cmd=['gofmt', '-l'], input=contents.encode('utf-8'),
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -0500731 stderr=subprocess.STDOUT)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700732 if output:
733 errors.append(gofile)
734 if errors:
735 return HookFailure('Files not formatted with gofmt:', errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400736 return None
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700737
738
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600739def _check_rustfmt(_project, commit):
740 """Checks that Rust files are formatted with rustfmt."""
741 errors = []
742 files = _filter_files(_get_affected_files(commit, relative=True),
743 [r'\.rs$'])
744
745 for rustfile in files:
746 contents = _get_file_content(rustfile, commit)
Chirantan Ekbote1da56d52020-07-13 17:35:35 +0900747 output = _run_command(cmd=['rustfmt', '--edition', '2018'],
748 input=contents.encode('utf-8'),
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -0500749 stderr=subprocess.STDOUT)
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600750 if output != contents:
751 errors.append(rustfile)
752 if errors:
753 return HookFailure('Files not formatted with rustfmt: '
754 "(run 'cargo fmt' to fix)", errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400755 return None
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600756
757
Keiichi Watanabe6ab73fd2020-07-14 23:42:02 +0900758class CargoClippyArgumentParserError(Exception):
759 """An exception indicating an invalid check_cargo_clippy option."""
760
761
762class CargoClippyArgumentParser(argparse.ArgumentParser):
763 """A argument parser for check_cargo_clippy."""
764
765 def error(self, message):
766 raise CargoClippyArgumentParserError(message)
767
768
769# A cargo project in which clippy runs.
770ClippyProject = collections.namedtuple('ClippyProject', ('root', 'script'))
771
772
773class _AddClippyProjectAction(argparse.Action):
774 """A callback that adds a cargo clippy setting.
775
776 It accepts a value which is in the form of "ROOT[:SCRIPT]".
777 """
778
779 def __call__(self, parser, namespace, values, option_string=None):
780 if getattr(namespace, self.dest, None) is None:
781 setattr(namespace, self.dest, [])
782 spec = values.split(':', 1)
783 if len(spec) == 1:
784 spec += [None]
785
786 if spec[0].startswith('/'):
787 raise CargoClippyArgumentParserError('root path must not start with "/"'
788 f' but "{spec[0]}"')
789
790 clippy = ClippyProject(root=spec[0], script=spec[1])
791 getattr(namespace, self.dest).append(clippy)
792
793
794def _get_cargo_clippy_parser():
795 """Creates a parser for check_cargo_clippy options."""
796
797 parser = CargoClippyArgumentParser()
798 parser.add_argument('--project', action=_AddClippyProjectAction, default=[])
799
800 return parser
801
802
803def _check_cargo_clippy(project, commit, options=()):
804 """Checks that a change doesn't produce cargo-clippy errors."""
805
806 options = list(options)
807 if not options:
808 return []
809 parser = _get_cargo_clippy_parser()
810
811 try:
812 opts = parser.parse_args(options)
813 except CargoClippyArgumentParserError as e:
814 return [HookFailure('invalid check_cargo_clippy option is given.'
815 f' Please check PRESUBMIT.cfg is correct: {e}')]
816 files = _get_affected_files(commit)
817
818 errors = []
819 for clippy in opts.project:
820 root = os.path.normpath(os.path.join(project.dir, clippy.root))
821
822 # Check if any file under `root` was modified.
823 modified = False
824 for f in files:
825 if f.startswith(root):
826 modified = True
827 break
828 if not modified:
829 continue
830
831 # Clean cargo's cache when we run clippy for this `root` for the first time.
832 # We don't want to clean the cache between commits to save time when
833 # multiple commits are checked.
834 if root not in _check_cargo_clippy.cleaned_root:
835 _run_command(['cargo', 'clean',
836 f'--manifest-path={root}/Cargo.toml'],
837 stderr=subprocess.STDOUT)
838 _check_cargo_clippy.cleaned_root.add(root)
839
840 cmd = ['cargo', 'clippy', '--all-features', '--all-targets',
841 f'--manifest-path={root}/Cargo.toml',
842 '--', '-D', 'warnings']
843 # Overwrite the clippy command if a project-specific script is specified.
844 if clippy.script:
845 cmd = [os.path.join(project.dir, clippy.script)]
846
847 output = _run_command(cmd, stderr=subprocess.STDOUT)
848 error = re.search(r'^error: ', output, flags=re.MULTILINE)
849 if error:
850 msg = output[error.start():]
851 errors.append(HookFailure(msg))
852
853 return errors
854
855
856# Stores cargo projects in which `cargo clean` ran.
857_check_cargo_clippy.cleaned_root = set()
858
859
Mike Frysingerae409522014-02-01 03:16:11 -0500860def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700861 """Check for a non-empty 'TEST=' field in the commit message."""
Mike Frysingered9b2a02019-12-12 18:52:32 -0500862 SEE_ALSO = 'Please review the documentation:\n%s' % (DOC_COMMIT_MSG_URL,)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700863
Cheng Yuehb707c522020-01-02 14:06:59 +0800864 if not re.search(TEST_FIELD_RE, _get_commit_desc(commit)):
Mike Frysingered9b2a02019-12-12 18:52:32 -0500865 msg = ('Changelist description needs TEST field (after first line)\n%s' %
866 (SEE_ALSO,))
Ryan Cui1562fb82011-05-09 11:01:31 -0700867 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400868 return None
Ryan Cui1562fb82011-05-09 11:01:31 -0700869
Ryan Cuiec4d6332011-05-02 14:15:25 -0700870
Mike Frysingerae409522014-02-01 03:16:11 -0500871def _check_change_has_valid_cq_depend(_project, commit):
Jason D. Clinton299e3222019-05-23 09:42:03 -0600872 """Check for a correctly formatted Cq-Depend field in the commit message."""
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700873 desc = _get_commit_desc(commit)
Jason D. Clinton299e3222019-05-23 09:42:03 -0600874 msg = 'Changelist has invalid Cq-Depend target.'
875 example = 'Example: Cq-Depend: chromium:1234, chrome-internal:2345'
David Jamesc3b68b32013-04-03 09:17:03 -0700876 try:
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700877 patch.GetPaladinDeps(desc)
David Jamesc3b68b32013-04-03 09:17:03 -0700878 except ValueError as ex:
879 return HookFailure(msg, [example, str(ex)])
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700880 # Check that Cq-Depend is in the same paragraph as Change-Id.
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500881 msg = 'Cq-Depend is not in the same paragraph as Change-Id.'
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700882 paragraphs = desc.split('\n\n')
883 for paragraph in paragraphs:
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500884 if (re.search(r'^Cq-Depend:', paragraph, re.M) and not
885 re.search('^Change-Id:', paragraph, re.M)):
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700886 return HookFailure(msg)
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500887
888 # We no longer support CQ-DEPEND= lines.
889 if re.search(r'^CQ-DEPEND[=:]', desc, re.M):
890 return HookFailure(
891 'CQ-DEPEND= is no longer supported. Please see:\n'
892 'https://chromium.googlesource.com/chromiumos/docs/+/HEAD/'
893 'contributing.md#CQ-DEPEND')
894
Mike Frysinger8cf80812019-09-16 23:49:29 -0400895 return None
David Jamesc3b68b32013-04-03 09:17:03 -0700896
897
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800898def _check_change_is_contribution(_project, commit):
899 """Check that the change is a contribution."""
900 NO_CONTRIB = 'not a contribution'
901 if NO_CONTRIB in _get_commit_desc(commit).lower():
902 msg = ('Changelist is not a contribution, this cannot be accepted.\n'
903 'Please remove the "%s" text from the commit message.') % NO_CONTRIB
904 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400905 return None
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800906
907
Alex Deymo643ac4c2015-09-03 10:40:50 -0700908def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700909 """Check for a correctly formatted 'BUG=' field in the commit message."""
Mike Frysingered9b2a02019-12-12 18:52:32 -0500910 SEE_ALSO = 'Please review the documentation:\n%s' % (DOC_COMMIT_MSG_URL,)
911
David James5c0073d2013-04-03 08:48:52 -0700912 OLD_BUG_RE = r'\nBUG=.*chromium-os'
913 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
914 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
915 'the chromium tracker in your BUG= line now.')
916 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700917
Alex Deymo643ac4c2015-09-03 10:40:50 -0700918 # Android internal and external projects use "Bug: " to track bugs in
919 # buganizer.
920 BUG_COLON_REMOTES = (
921 'aosp',
922 'goog',
923 )
924 if project.remote in BUG_COLON_REMOTES:
925 BUG_RE = r'\nBug: ?([Nn]one|\d+)'
926 if not re.search(BUG_RE, _get_commit_desc(commit)):
927 msg = ('Changelist description needs BUG field (after first line):\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500928 'Examples:\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700929 'Bug: 9999 (for buganizer)\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500930 'BUG=None\n%s' % (SEE_ALSO,))
Alex Deymo643ac4c2015-09-03 10:40:50 -0700931 return HookFailure(msg)
932 else:
Jorge Lucangeli Obesdce214e2017-10-25 15:04:39 -0400933 BUG_RE = r'\nBUG=([Nn]one|(chromium|b):\d+)'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700934 if not re.search(BUG_RE, _get_commit_desc(commit)):
935 msg = ('Changelist description needs BUG field (after first line):\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500936 'Examples:\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700937 'BUG=chromium:9999 (for public tracker)\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700938 'BUG=b:9999 (for buganizer)\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500939 'BUG=None\n%s' % (SEE_ALSO,))
Alex Deymo643ac4c2015-09-03 10:40:50 -0700940 return HookFailure(msg)
Ryan Cui1562fb82011-05-09 11:01:31 -0700941
Cheng Yuehb707c522020-01-02 14:06:59 +0800942 TEST_BEFORE_BUG_RE = TEST_FIELD_RE + r'.*' + BUG_RE
943
944 if re.search(TEST_BEFORE_BUG_RE, _get_commit_desc(commit), re.DOTALL):
945 msg = ('The BUG field must come before the TEST field.\n%s' %
946 (SEE_ALSO,))
947 return HookFailure(msg)
948
Mike Frysinger8cf80812019-09-16 23:49:29 -0400949 return None
950
Ryan Cuiec4d6332011-05-02 14:15:25 -0700951
Jack Neus8edbf642019-07-10 16:08:31 -0600952def _check_change_no_include_oem(project, commit):
953 """Check that the change does not reference OEMs."""
954 ALLOWLIST = {
955 'chromiumos/platform/ec',
956 # Used by unit tests.
957 'project',
958 }
959 if project.name not in ALLOWLIST:
960 return None
961
Mike Frysingerbb34a222019-07-31 14:40:46 -0400962 TAGS = {
Jack Neus8edbf642019-07-10 16:08:31 -0600963 'Reviewed-on',
964 'Reviewed-by',
965 'Signed-off-by',
966 'Commit-Ready',
967 'Tested-by',
968 'Commit-Queue',
Jack Neus8edbf642019-07-10 16:08:31 -0600969 'Acked-by',
970 'Modified-by',
971 'CC',
972 'Suggested-by',
973 'Reported-by',
974 'Acked-for-chrome-by',
LaMont Jones237f3ef2020-01-22 10:40:52 -0700975 'Cq-Cl-Tag',
LaMont Jonesfb5e8bf2020-03-03 12:50:06 -0700976 'Cq-Include-Trybots',
Mike Frysingerbb34a222019-07-31 14:40:46 -0400977 }
Jack Neus8edbf642019-07-10 16:08:31 -0600978
979 # Ignore tags, which could reasonably contain OEM names
980 # (e.g. Reviewed-by: foo@oem.corp-partner.google.com).
Jack Neus8edbf642019-07-10 16:08:31 -0600981 commit_message = ' '.join(
Mike Frysingerbb34a222019-07-31 14:40:46 -0400982 x for x in _get_commit_desc(commit).splitlines()
983 if ':' not in x or x.split(':', 1)[0] not in TAGS)
984
Jack Neus8edbf642019-07-10 16:08:31 -0600985 commit_message = re.sub(r'[\s_-]+', ' ', commit_message)
986
987 # Exercise caution when expanding these lists. Adding a name
988 # could indicate a new relationship with a company!
989 OEMS = ['hp', 'hewlett packard', 'dell', 'lenovo', 'acer', 'asus', 'samsung']
990 ODMS = [
991 'bitland', 'compal', 'haier', 'huaqin', 'inventec', 'lg', 'pegatron',
992 'pegatron(ems)', 'quanta', 'samsung', 'wistron'
993 ]
994
995 for name_type, name_list in [('OEM', OEMS), ('ODM', ODMS)]:
996 # Construct regex
997 name_re = r'\b(%s)\b' % '|'.join([re.escape(x) for x in name_list])
998 matches = [x[0] for x in re.findall(name_re, commit_message, re.IGNORECASE)]
Mike Frysingere52b1bc2019-09-16 23:45:41 -0400999 if matches:
Jack Neus8edbf642019-07-10 16:08:31 -06001000 # If there's a match, throw an error.
1001 error_msg = ('Changelist description contains the name of an'
1002 ' %s: "%s".' % (name_type, '","'.join(matches)))
1003 return HookFailure(error_msg)
1004
Mike Frysinger8cf80812019-09-16 23:49:29 -04001005 return None
1006
Jack Neus8edbf642019-07-10 16:08:31 -06001007
Mike Frysinger292b45d2014-11-25 01:17:10 -05001008def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -07001009 """Check that we're not missing a revbump of an ebuild in the given commit.
1010
1011 If the given commit touches files in a directory that has ebuilds somewhere
1012 up the directory hierarchy, it's very likely that we need an ebuild revbump
1013 in order for those changes to take effect.
1014
1015 It's not totally trivial to detect a revbump, so at least detect that an
1016 ebuild with a revision number in it was touched. This should handle the
1017 common case where we use a symlink to do the revbump.
1018
1019 TODO: it would be nice to enhance this hook to:
1020 * Handle cases where people revbump with a slightly different syntax. I see
1021 one ebuild (puppy) that revbumps with _pN. This is a false positive.
1022 * Catches cases where people aren't using symlinks for revbumps. If they
1023 edit a revisioned file directly (and are expected to rename it for revbump)
1024 we'll miss that. Perhaps we could detect that the file touched is a
1025 symlink?
1026
1027 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
1028 still better off than without this check.
1029
1030 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001031 project: The Project to look at
Doug Anderson42b8a052013-06-26 10:45:36 -07001032 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -05001033 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -07001034
1035 Returns:
1036 A HookFailure or None.
1037 """
Mike Frysinger011af942014-01-17 16:12:22 -05001038 # If this is the portage-stable overlay, then ignore the check. It's rare
1039 # that we're doing anything other than importing files from upstream, so
1040 # forcing a rev bump makes no sense.
Bob Haarman0dc1f942020-10-03 00:06:59 +00001041 allowlist = (
Mike Frysinger011af942014-01-17 16:12:22 -05001042 'chromiumos/overlays/portage-stable',
1043 )
Bob Haarman0dc1f942020-10-03 00:06:59 +00001044 if project.name in allowlist:
Mike Frysinger011af942014-01-17 16:12:22 -05001045 return None
1046
Mike Frysinger292b45d2014-11-25 01:17:10 -05001047 def FinalName(obj):
1048 # If the file is being deleted, then the dst_file is not set.
1049 if obj.dst_file is None:
1050 return obj.src_file
1051 else:
1052 return obj.dst_file
1053
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001054 def AllowedPath(obj):
Mike Frysinger78dbc242020-11-27 16:46:39 -05001055 allowed_files = {
1056 'ChangeLog', 'Manifest', 'metadata.xml', 'OWNERS', 'README.md',
1057 }
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001058 allowed_directories = {'profiles'}
1059
1060 affected = pathlib.Path(FinalName(obj))
1061 if affected.name in allowed_files:
1062 return True
1063
1064 for directory in allowed_directories:
1065 if directory in affected.parts:
1066 return True
1067
1068 return False
1069
Mike Frysinger292b45d2014-11-25 01:17:10 -05001070 affected_path_objs = _get_affected_files(
1071 commit, include_deletes=True, include_symlinks=True, relative=True,
1072 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -07001073
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001074 # Don't yell about changes to allowed files or directories...
Mike Frysinger292b45d2014-11-25 01:17:10 -05001075 affected_path_objs = [x for x in affected_path_objs
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001076 if not AllowedPath(x)]
Mike Frysinger292b45d2014-11-25 01:17:10 -05001077 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -07001078 return None
1079
1080 # If we've touched any file named with a -rN.ebuild then we'll say we're
1081 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -05001082 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
1083 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -07001084 if touched_revved_ebuild:
1085 return None
1086
Mike Frysinger292b45d2014-11-25 01:17:10 -05001087 # If we're creating new ebuilds from scratch, then we don't need an uprev.
1088 # Find all the dirs that new ebuilds and ignore their files/.
1089 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
1090 if FinalName(x).endswith('.ebuild') and x.status == 'A']
1091 affected_path_objs = [obj for obj in affected_path_objs
1092 if not any(FinalName(obj).startswith(x)
1093 for x in ebuild_dirs)]
1094 if not affected_path_objs:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001095 return None
Mike Frysinger292b45d2014-11-25 01:17:10 -05001096
Doug Anderson42b8a052013-06-26 10:45:36 -07001097 # We want to examine the current contents of all directories that are parents
1098 # of files that were touched (up to the top of the project).
1099 #
1100 # ...note: we use the current directory contents even though it may have
1101 # changed since the commit we're looking at. This is just a heuristic after
1102 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -05001103 if project_top is None:
1104 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -07001105 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -05001106 for obj in affected_path_objs:
1107 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -07001108 while os.path.exists(path) and not os.path.samefile(path, project_top):
1109 dirs_to_check.add(path)
1110 path = os.path.dirname(path)
1111
1112 # Look through each directory. If it's got an ebuild in it then we'll
1113 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -08001114 affected_paths = set(os.path.join(project_top, FinalName(x))
1115 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -07001116 for dir_path in dirs_to_check:
1117 contents = os.listdir(dir_path)
1118 ebuilds = [os.path.join(dir_path, path)
1119 for path in contents if path.endswith('.ebuild')]
1120 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
1121
C Shapiroae157ae2017-09-18 16:24:03 -06001122 affected_paths_under_9999_ebuilds = set()
1123 for affected_path in affected_paths:
1124 for ebuild_9999 in ebuilds_9999:
1125 ebuild_dir = os.path.dirname(ebuild_9999)
1126 if affected_path.startswith(ebuild_dir):
1127 affected_paths_under_9999_ebuilds.add(affected_path)
1128
1129 # If every file changed exists under a 9999 ebuild, then skip
1130 if len(affected_paths_under_9999_ebuilds) == len(affected_paths):
1131 continue
1132
Doug Anderson42b8a052013-06-26 10:45:36 -07001133 # If the -9999.ebuild file was touched the bot will uprev for us.
1134 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -05001135 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -07001136 continue
1137
1138 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -05001139 return HookFailure('Changelist probably needs a revbump of an ebuild, '
1140 'or a -r1.ebuild symlink if this is a new ebuild:\n'
1141 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -07001142
1143 return None
1144
1145
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001146def _check_ebuild_eapi(project, commit):
Mike Frysinger948284a2018-02-01 15:22:56 -05001147 """Make sure we have people use EAPI=5 or newer with custom ebuilds.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001148
1149 We want to get away from older EAPI's as it makes life confusing and they
1150 have less builtin error checking.
1151
1152 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001153 project: The Project to look at
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001154 commit: The commit to look at
1155
1156 Returns:
1157 A HookFailure or None.
1158 """
1159 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -05001160 # that we're doing anything other than importing files from upstream, and
1161 # we shouldn't be rewriting things fundamentally anyways.
Bob Haarman0dc1f942020-10-03 00:06:59 +00001162 allowlist = (
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001163 'chromiumos/overlays/portage-stable',
1164 )
Bob Haarman0dc1f942020-10-03 00:06:59 +00001165 if project.name in allowlist:
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001166 return None
1167
Mike Frysinger948284a2018-02-01 15:22:56 -05001168 BAD_EAPIS = ('0', '1', '2', '3', '4')
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001169
1170 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
1171
1172 ebuilds_re = [r'\.ebuild$']
1173 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1174 ebuilds_re)
1175 bad_ebuilds = []
1176
1177 for ebuild in ebuilds:
1178 # If the ebuild does not specify an EAPI, it defaults to 0.
1179 eapi = '0'
1180
1181 lines = _get_file_content(ebuild, commit).splitlines()
1182 if len(lines) == 1:
1183 # This is most likely a symlink, so skip it entirely.
1184 continue
1185
1186 for line in lines:
1187 m = get_eapi.match(line)
1188 if m:
1189 # Once we hit the first EAPI line in this ebuild, stop processing.
1190 # The spec requires that there only be one and it be first, so
1191 # checking all possible values is pointless. We also assume that
1192 # it's "the" EAPI line and not something in the middle of a heredoc.
1193 eapi = m.group(1)
1194 break
1195
1196 if eapi in BAD_EAPIS:
1197 bad_ebuilds.append((ebuild, eapi))
1198
1199 if bad_ebuilds:
1200 # pylint: disable=C0301
Mike Frysingerc6114f42020-05-15 00:55:05 -04001201 url = 'https://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001202 # pylint: enable=C0301
1203 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -05001204 'These ebuilds are using old EAPIs. If these are imported from\n'
1205 'Gentoo, then you may ignore and upload once with the --no-verify\n'
Mike Frysingerc6114f42020-05-15 00:55:05 -04001206 'flag. Otherwise, please update to 7 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001207 '\t%s\n'
1208 'See this guide for more details:\n%s\n' %
1209 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
1210
Mike Frysinger8cf80812019-09-16 23:49:29 -04001211 return None
1212
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001213
Mike Frysinger89bdb852014-02-01 05:26:26 -05001214def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -05001215 """Make sure we use the new style KEYWORDS when possible in ebuilds.
1216
1217 If an ebuild generally does not care about the arch it is running on, then
1218 ebuilds should flag it with one of:
1219 KEYWORDS="*" # A stable ebuild.
1220 KEYWORDS="~*" # An unstable ebuild.
1221 KEYWORDS="-* ..." # Is known to only work on specific arches.
1222
1223 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001224 project: The Project to look at
Mike Frysingerc51ece72014-01-17 16:23:40 -05001225 commit: The commit to look at
1226
1227 Returns:
1228 A HookFailure or None.
1229 """
Bob Haarman0dc1f942020-10-03 00:06:59 +00001230 ALLOWLIST = set(('*', '-*', '~*'))
Mike Frysingerc51ece72014-01-17 16:23:40 -05001231
1232 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
1233
Mike Frysinger89bdb852014-02-01 05:26:26 -05001234 ebuilds_re = [r'\.ebuild$']
1235 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1236 ebuilds_re)
1237
Mike Frysinger8d42d742014-09-22 15:50:21 -04001238 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -05001239 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -04001240 # We get the full content rather than a diff as the latter does not work
1241 # on new files (like when adding new ebuilds).
1242 lines = _get_file_content(ebuild, commit).splitlines()
1243 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -05001244 m = get_keywords.match(line)
1245 if m:
1246 keywords = set(m.group(1).split())
Bob Haarman0dc1f942020-10-03 00:06:59 +00001247 if not keywords or ALLOWLIST - keywords != ALLOWLIST:
Mike Frysingerc51ece72014-01-17 16:23:40 -05001248 continue
1249
Mike Frysinger8d42d742014-09-22 15:50:21 -04001250 bad_ebuilds.append(ebuild)
1251
1252 if bad_ebuilds:
1253 return HookFailure(
1254 '%s\n'
1255 'Please update KEYWORDS to use a glob:\n'
1256 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
1257 ' KEYWORDS="*"\n'
1258 'If the ebuild should be marked unstable (normal for '
1259 'cros-workon / 9999 ebuilds):\n'
1260 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -04001261 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -04001262 'then use -* like so:\n'
1263 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -05001264
Mike Frysinger8cf80812019-09-16 23:49:29 -04001265 return None
1266
Mike Frysingerc51ece72014-01-17 16:23:40 -05001267
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001268def _check_ebuild_licenses(_project, commit):
1269 """Check if the LICENSE field in the ebuild is correct."""
Brian Norris7a610e82016-02-17 12:24:54 -08001270 affected_paths = _get_affected_files(commit, relative=True)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001271 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
1272
1273 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -08001274 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001275
1276 for ebuild in touched_ebuilds:
Alex Kleinb5953522018-08-03 11:44:21 -06001277 # e.g. path/to/overlay/category/package/package.ebuild -> path/to/overlay
1278 overlay_path = os.sep.join(ebuild.split(os.sep)[:-3])
1279
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001280 try:
Brian Norris7a610e82016-02-17 12:24:54 -08001281 ebuild_content = _get_file_content(ebuild, commit)
Alex Kleinb5953522018-08-03 11:44:21 -06001282 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
1283 overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001284 except ValueError as e:
Mike Frysingerf1ee2bf2019-09-16 23:47:33 -04001285 return HookFailure(str(e), [ebuild])
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001286
Sergey Frolovc1bd8782021-01-20 19:35:44 -07001287 # Virtual packages must use "metapackage" license.
1288 if ebuild.split('/')[-3] == 'virtual':
1289 if license_types != ['metapackage']:
1290 return HookFailure('Virtual package must use LICENSE="metapackage".',
1291 [ebuild])
1292
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001293 # Also ignore licenses ending with '?'
1294 for license_type in [x for x in license_types
1295 if x not in LICENSES_IGNORE and not x.endswith('?')]:
1296 try:
Alex Kleinb5953522018-08-03 11:44:21 -06001297 licenses_lib.Licensing.FindLicenseType(license_type,
1298 overlay_path=overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001299 except AssertionError as e:
Mike Frysingerf1ee2bf2019-09-16 23:47:33 -04001300 return HookFailure(str(e), [ebuild])
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001301
Mike Frysinger8cf80812019-09-16 23:49:29 -04001302 return None
1303
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001304
Mike Frysingerd9ed85f2021-02-03 12:38:24 -05001305def _check_ebuild_owners(project, commit):
Mike Frysingerb04778f2020-11-30 02:41:14 -05001306 """Require all new packages include an OWNERS file."""
1307 # Look for all adds/removes since we're going to ignore changes that only
1308 # update a package. We only want to flag new package imports for now.
1309 affected_files_objs = _get_affected_files(
1310 commit, include_deletes=True, include_symlinks=True, relative=True,
1311 full_details=True)
1312
1313 # If this CL doesn't include any ebuilds, don't bother complaining.
1314 new_ebuilds = [x for x in affected_files_objs
1315 if x.status == 'A' and x.src_file.endswith('.ebuild')]
1316 if not new_ebuilds:
1317 return None
1318
1319 # Check each package dir.
1320 packages_missing_owners = []
1321 package_dirs = sorted(set(os.path.dirname(x.src_file) for x in new_ebuilds))
1322 for package_dir in package_dirs:
1323 package_files = [
1324 x for x in affected_files_objs
1325 if (x.src_file and x.src_file.startswith(f'{package_dir}/')) or
1326 (x.dst_file and x.dst_file.startswith(f'{package_dir}/'))]
1327
1328 # Only complain about new ebuilds, not existing ones. For now.
1329 # We'll assume that "all adds" means it's a new package.
1330 if any(x for x in package_files if x.status != 'A'):
1331 continue
1332
1333 # See if there's an OWNERS file in there already.
Mike Frysingerd9ed85f2021-02-03 12:38:24 -05001334 data = _get_file_content(os.path.join(package_dir, 'OWNERS'), commit)
1335 if not data:
1336 # Allow specific overlays to declare OWNERS for all packages.
1337 if (project.name == 'chromiumos/overlays/board-overlays' or
1338 re.match(r'^chromeos/overlays/(baseboard|chipset|project|overlay)-',
1339 project.name)):
1340 data = _get_file_content(os.path.join(os.path.dirname(os.path.dirname(
1341 package_dir)), 'OWNERS'), commit)
1342
1343 if not data:
1344 packages_missing_owners.append(package_dir)
1345 continue
1346
1347 # Require specific people and not just *.
1348 lines = {x for x in data.splitlines() if x.split('#', 1)[0].strip()}
1349 if not lines - {'*'}:
Mike Frysingerb04778f2020-11-30 02:41:14 -05001350 packages_missing_owners.append(package_dir)
1351
1352 if packages_missing_owners:
1353 return HookFailure(
Mike Frysingerd9ed85f2021-02-03 12:38:24 -05001354 'All new packages must have an OWNERS file filled out.',
Mike Frysingerb04778f2020-11-30 02:41:14 -05001355 packages_missing_owners)
1356
1357 return None
1358
1359
Mike Frysinger6ee76b82020-11-20 01:16:06 -05001360def _check_ebuild_r0(_project, commit):
1361 """Do not allow ebuilds to end with -r0 versions."""
1362 ebuilds = _filter_files(
1363 _get_affected_files(commit, include_symlinks=True, relative=True),
1364 (r'-r0\.ebuild$',))
1365 if ebuilds:
1366 return HookFailure(
1367 'The -r0 in ebuilds is redundant and confusing. Simply remove it.\n'
1368 'For example: git mv foo-1.0-r0.ebuild foo-1.0.ebuild',
1369 ebuilds)
1370
1371 return None
1372
1373
Mike Frysingercd363c82014-02-01 05:20:18 -05001374def _check_ebuild_virtual_pv(project, commit):
1375 """Enforce the virtual PV policies."""
1376 # If this is the portage-stable overlay, then ignore the check.
1377 # We want to import virtuals as-is from upstream Gentoo.
Bob Haarman0dc1f942020-10-03 00:06:59 +00001378 allowlist = (
Mike Frysingercd363c82014-02-01 05:20:18 -05001379 'chromiumos/overlays/portage-stable',
1380 )
Bob Haarman0dc1f942020-10-03 00:06:59 +00001381 if project.name in allowlist:
Mike Frysingercd363c82014-02-01 05:20:18 -05001382 return None
1383
1384 # We assume the repo name is the same as the dir name on disk.
1385 # It would be dumb to not have them match though.
Alex Deymo643ac4c2015-09-03 10:40:50 -07001386 project_base = os.path.basename(project.name)
Mike Frysingercd363c82014-02-01 05:20:18 -05001387
1388 is_variant = lambda x: x.startswith('overlay-variant-')
1389 is_board = lambda x: x.startswith('overlay-')
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001390 is_baseboard = lambda x: x.startswith('baseboard-')
1391 is_chipset = lambda x: x.startswith('chipset-')
1392 is_project = lambda x: x.startswith('project-')
Mike Frysingercd363c82014-02-01 05:20:18 -05001393 is_private = lambda x: x.endswith('-private')
1394
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001395 is_special_overlay = lambda x: (is_board(x) or is_chipset(x) or
1396 is_baseboard(x) or is_project(x))
1397
Mike Frysingercd363c82014-02-01 05:20:18 -05001398 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
1399
1400 ebuilds_re = [r'\.ebuild$']
1401 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1402 ebuilds_re)
1403 bad_ebuilds = []
1404
1405 for ebuild in ebuilds:
1406 m = get_pv.match(ebuild)
1407 if m:
1408 overlay = m.group(1)
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001409 if not overlay or not is_special_overlay(overlay):
Alex Deymo643ac4c2015-09-03 10:40:50 -07001410 overlay = project_base
Mike Frysingercd363c82014-02-01 05:20:18 -05001411
1412 pv = m.group(3).split('-', 1)[0]
1413
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001414 # Virtual versions >= 4 are special cases used above the standard
1415 # versioning structure, e.g. if one has a board inheriting a board.
Julius Werner0299a2b2020-07-31 18:20:43 -07001416 if pv[0] >= '4':
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001417 want_pv = pv
Mike Frysingercd363c82014-02-01 05:20:18 -05001418 elif is_board(overlay):
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001419 if is_private(overlay):
1420 want_pv = '3.5' if is_variant(overlay) else '3'
1421 elif is_board(overlay):
1422 want_pv = '2.5' if is_variant(overlay) else '2'
1423 elif is_baseboard(overlay):
Julius Werner0299a2b2020-07-31 18:20:43 -07001424 want_pv = '1.9.5' if is_private(overlay) else '1.9'
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001425 elif is_chipset(overlay):
Julius Werner0299a2b2020-07-31 18:20:43 -07001426 want_pv = '1.8.5' if is_private(overlay) else '1.8'
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001427 elif is_project(overlay):
1428 want_pv = '1.7' if is_private(overlay) else '1.5'
Mike Frysingercd363c82014-02-01 05:20:18 -05001429 else:
1430 want_pv = '1'
1431
1432 if pv != want_pv:
1433 bad_ebuilds.append((ebuild, pv, want_pv))
1434
1435 if bad_ebuilds:
1436 # pylint: disable=C0301
1437 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
1438 # pylint: enable=C0301
1439 return HookFailure(
1440 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
1441 '\t%s\n'
1442 'If this is an upstream Gentoo virtual, then you may ignore this\n'
1443 'check (and re-run w/--no-verify). Otherwise, please see this\n'
1444 'page for more details:\n%s\n' %
1445 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
1446 for x in bad_ebuilds]), url))
1447
Mike Frysinger8cf80812019-09-16 23:49:29 -04001448 return None
1449
Mike Frysingercd363c82014-02-01 05:20:18 -05001450
Daniel Erat9d203ff2015-02-17 10:12:21 -07001451def _check_portage_make_use_var(_project, commit):
1452 """Verify that $USE is set correctly in make.conf and make.defaults."""
1453 files = _filter_files(_get_affected_files(commit, relative=True),
1454 [r'(^|/)make.(conf|defaults)$'])
1455
1456 errors = []
1457 for path in files:
1458 basename = os.path.basename(path)
1459
1460 # Has a USE= line already been encountered in this file?
1461 saw_use = False
1462
1463 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
1464 if not line.startswith('USE='):
1465 continue
1466
1467 preserves_use = '${USE}' in line or '$USE' in line
1468
1469 if (basename == 'make.conf' or
1470 (basename == 'make.defaults' and saw_use)) and not preserves_use:
1471 errors.append('%s:%d: missing ${USE}' % (path, i))
1472 elif basename == 'make.defaults' and not saw_use and preserves_use:
1473 errors.append('%s:%d: ${USE} referenced in initial declaration' %
1474 (path, i))
1475
1476 saw_use = True
1477
1478 if errors:
1479 return HookFailure(
1480 'One or more Portage make files appear to set USE incorrectly.\n'
1481 '\n'
1482 'All USE assignments in make.conf and all assignments after the\n'
1483 'initial declaration in make.defaults should contain "${USE}" to\n'
1484 'preserve previously-set flags.\n'
1485 '\n'
1486 'The initial USE declaration in make.defaults should not contain\n'
1487 '"${USE}".\n',
1488 errors)
1489
Mike Frysinger8cf80812019-09-16 23:49:29 -04001490 return None
1491
Daniel Erat9d203ff2015-02-17 10:12:21 -07001492
Mike Frysingerae409522014-02-01 03:16:11 -05001493def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001494 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001495 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001496 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001497 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001498 if not m:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001499 return HookFailure('Last paragraph of description must include Change-Id.')
Ryan Cui1562fb82011-05-09 11:01:31 -07001500
LaMont Jonesfb5e8bf2020-03-03 12:50:06 -07001501 # S-o-b and some other tags always allowed to follow Change-ID in the footer.
1502 allowed_tags = ['Signed-off-by', 'Cq-Cl-Tag', 'Cq-Include-Trybots']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001503
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001504 end = desc[m.end():].strip().splitlines()
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001505 cherry_pick_marker = 'cherry picked from commit'
1506
1507 if end and cherry_pick_marker in end[-1]:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001508 # Cherry picked patches allow more tags in the last paragraph.
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001509 allowed_tags += ['Commit-Queue', 'Commit-Ready', 'Reviewed-by',
1510 'Reviewed-on', 'Tested-by']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001511 end = end[:-1]
1512
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001513 # Note that descriptions could have multiple cherry pick markers.
1514 tag_search = r'^(%s:|\(%s) ' % (':|'.join(allowed_tags), cherry_pick_marker)
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001515
1516 if [x for x in end if not re.search(tag_search, x)]:
1517 return HookFailure('Only "%s:" tag(s) may follow the Change-Id.' %
1518 ':", "'.join(allowed_tags))
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001519
Mike Frysinger8cf80812019-09-16 23:49:29 -04001520 return None
1521
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001522
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001523def _check_commit_message_style(_project, commit):
1524 """Verify that the commit message matches our style.
1525
1526 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
1527 commit hooks.
1528 """
Mike Frysingered9b2a02019-12-12 18:52:32 -05001529 SEE_ALSO = 'Please review the documentation:\n%s' % (DOC_COMMIT_MSG_URL,)
Mike Frysinger4efdee72019-11-04 10:57:01 -05001530
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001531 desc = _get_commit_desc(commit)
1532
1533 # The first line should be by itself.
1534 lines = desc.splitlines()
1535 if len(lines) > 1 and lines[1]:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001536 return HookFailure('The second line of the commit message must be blank.'
1537 '\n%s' % (SEE_ALSO,))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001538
1539 # The first line should be one sentence.
1540 if '. ' in lines[0]:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001541 return HookFailure('The first line cannot be more than one sentence.\n%s' %
1542 (SEE_ALSO,))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001543
1544 # The first line cannot be too long.
1545 MAX_FIRST_LINE_LEN = 100
1546 if len(lines[0]) > MAX_FIRST_LINE_LEN:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001547 return HookFailure('The first line must be less than %i chars.\n%s' %
1548 (MAX_FIRST_LINE_LEN, SEE_ALSO))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001549
Mike Frysinger8cf80812019-09-16 23:49:29 -04001550 return None
1551
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001552
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001553def _check_cros_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001554 """Verifies the Chromium OS license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -07001555
Mike Frysinger98638102014-08-28 00:15:08 -04001556 Should be following the spec:
1557 http://dev.chromium.org/developers/coding-style#TOC-File-headers
1558 """
1559 # For older years, be a bit more flexible as our policy says leave them be.
1560 LICENSE_HEADER = (
Keigo Oka7e880ac2019-07-03 15:03:43 +09001561 r'.*Copyright(?: \(c\))? (20[0-9]{2})(?:-20[0-9]{2})? The Chromium OS '
1562 r'Authors\. All rights reserved\.\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001563 r'.*Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001564 r'be\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001565 r'.*found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -05001566 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001567 )
1568 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1569
1570 # For newer years, be stricter.
Keigo Oka7e880ac2019-07-03 15:03:43 +09001571 BAD_COPYRIGHT_LINE = (
Brian Norris68838dd2018-09-26 18:30:24 -07001572 r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001573 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001574 )
Keigo Oka7e880ac2019-07-03 15:03:43 +09001575 bad_copyright_re = re.compile(BAD_COPYRIGHT_LINE)
Mike Frysinger98638102014-08-28 00:15:08 -04001576
Shuhei Takahashiabc20f32017-07-10 19:35:45 +09001577 included, excluded = _parse_common_inclusion_options(options)
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001578
Mike Frysinger98638102014-08-28 00:15:08 -04001579 bad_files = []
1580 bad_copyright_files = []
Keigo Oka7e880ac2019-07-03 15:03:43 +09001581 bad_year_files = []
1582
Ken Turnerd07564b2018-02-08 17:57:59 +11001583 files = _filter_files(
1584 _get_affected_files(commit, relative=True),
1585 included + COMMON_INCLUDED_PATHS,
1586 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001587 existing_files = set(_get_affected_files(commit, relative=True,
1588 include_adds=False))
Mike Frysinger98638102014-08-28 00:15:08 -04001589
Keigo Oka7e880ac2019-07-03 15:03:43 +09001590 current_year = str(datetime.datetime.now().year)
Mike Frysinger98638102014-08-28 00:15:08 -04001591 for f in files:
1592 contents = _get_file_content(f, commit)
1593 if not contents:
1594 # Ignore empty files.
1595 continue
1596
Keigo Oka7e880ac2019-07-03 15:03:43 +09001597 m = license_re.search(contents)
1598 if not m:
Mike Frysinger98638102014-08-28 00:15:08 -04001599 bad_files.append(f)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001600 elif bad_copyright_re.search(contents):
Mike Frysinger98638102014-08-28 00:15:08 -04001601 bad_copyright_files.append(f)
1602
Keigo Oka7e880ac2019-07-03 15:03:43 +09001603 if m and f not in existing_files:
1604 year = m.group(1)
1605 if year != current_year:
1606 bad_year_files.append(f)
1607
1608 errors = []
Mike Frysinger98638102014-08-28 00:15:08 -04001609 if bad_files:
1610 msg = '%s:\n%s\n%s' % (
1611 'License must match', license_re.pattern,
1612 'Found a bad header in these files:')
Keigo Oka7e880ac2019-07-03 15:03:43 +09001613 errors.append(HookFailure(msg, bad_files))
Mike Frysinger98638102014-08-28 00:15:08 -04001614 if bad_copyright_files:
1615 msg = 'Do not use (c) in copyright headers in new files:'
Keigo Oka7e880ac2019-07-03 15:03:43 +09001616 errors.append(HookFailure(msg, bad_copyright_files))
1617 if bad_year_files:
1618 msg = 'Use current year (%s) in copyright headers in new files:' % (
1619 current_year)
1620 errors.append(HookFailure(msg, bad_year_files))
Ryan Cuiec4d6332011-05-02 14:15:25 -07001621
Keigo Oka7e880ac2019-07-03 15:03:43 +09001622 return errors
Ryan Cuiec4d6332011-05-02 14:15:25 -07001623
Mike Frysinger8cf80812019-09-16 23:49:29 -04001624
Amin Hassani391efa92018-01-26 17:58:05 -08001625def _check_aosp_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001626 """Verifies the AOSP license/copyright header.
1627
1628 AOSP uses the Apache2 License:
1629 https://source.android.com/source/licenses.html
1630 """
1631 LICENSE_HEADER = (
1632 r"""^[#/\*]*
1633[#/\*]* ?Copyright( \([cC]\))? 20[-0-9]{2,7} The Android Open Source Project
1634[#/\*]* ?
1635[#/\*]* ?Licensed under the Apache License, Version 2.0 \(the "License"\);
1636[#/\*]* ?you may not use this file except in compliance with the License\.
1637[#/\*]* ?You may obtain a copy of the License at
1638[#/\*]* ?
1639[#/\*]* ? http://www\.apache\.org/licenses/LICENSE-2\.0
1640[#/\*]* ?
1641[#/\*]* ?Unless required by applicable law or agreed to in writing, software
1642[#/\*]* ?distributed under the License is distributed on an "AS IS" BASIS,
1643[#/\*]* ?WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or """
1644 r"""implied\.
1645[#/\*]* ?See the License for the specific language governing permissions and
1646[#/\*]* ?limitations under the License\.
1647[#/\*]*$
1648"""
1649 )
1650 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1651
Amin Hassani391efa92018-01-26 17:58:05 -08001652 included, excluded = _parse_common_inclusion_options(options)
1653
Ken Turnerd07564b2018-02-08 17:57:59 +11001654 files = _filter_files(
1655 _get_affected_files(commit, relative=True),
1656 included + COMMON_INCLUDED_PATHS,
1657 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Alex Deymof5792ce2015-08-24 22:50:08 -07001658
1659 bad_files = []
1660 for f in files:
1661 contents = _get_file_content(f, commit)
1662 if not contents:
1663 # Ignore empty files.
1664 continue
1665
1666 if not license_re.search(contents):
1667 bad_files.append(f)
1668
1669 if bad_files:
1670 msg = ('License must match:\n%s\nFound a bad header in these files:' %
1671 license_re.pattern)
1672 return HookFailure(msg, bad_files)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001673 return None
Alex Deymof5792ce2015-08-24 22:50:08 -07001674
1675
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001676def _check_layout_conf(_project, commit):
1677 """Verifies the metadata/layout.conf file."""
1678 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001679 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001680 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001681 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001682
Mike Frysinger94a670c2014-09-19 12:46:26 -04001683 # Handle multiple overlays in a single commit (like the public tree).
1684 for f in _get_affected_files(commit, relative=True):
1685 if f.endswith(repo_name):
1686 repo_names.append(f)
1687 elif f.endswith(layout_path):
1688 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001689
1690 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -04001691 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001692 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -04001693 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001694
Mike Frysinger94a670c2014-09-19 12:46:26 -04001695 # Gather all the errors in one pass so we show one full message.
1696 all_errors = {}
1697 for layout_path in layout_paths:
1698 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001699
Mike Frysinger94a670c2014-09-19 12:46:26 -04001700 # Make sure the config file is sorted.
1701 data = [x for x in _get_file_content(layout_path, commit).splitlines()
1702 if x and x[0] != '#']
1703 if sorted(data) != data:
1704 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001705
Mike Frysinger94a670c2014-09-19 12:46:26 -04001706 # Require people to set specific values all the time.
1707 settings = (
1708 # TODO: Enable this for everyone. http://crbug.com/408038
Shuhei Takahashi3cbb8dd2019-10-29 12:37:11 +09001709 # ('fast caching', 'cache-format = md5-dict'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001710 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -05001711 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
1712 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001713 )
1714 for reason, line in settings:
1715 if line not in data:
1716 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001717
Mike Frysinger94a670c2014-09-19 12:46:26 -04001718 # Require one of these settings.
Mike Frysinger5fae62d2015-11-11 20:12:15 -05001719 if 'use-manifests = strict' not in data:
1720 errors += ['enable file checking with: use-manifests = strict']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001721
Mike Frysinger94a670c2014-09-19 12:46:26 -04001722 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -04001723 for line in data:
1724 if line.startswith('repo-name = '):
1725 break
1726 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001727 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001728
Mike Frysinger94a670c2014-09-19 12:46:26 -04001729 # Summarize all the errors we saw (if any).
1730 lines = ''
1731 for layout_path, errors in all_errors.items():
1732 if errors:
1733 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1734 if lines:
1735 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1736 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001737
Mike Frysinger8cf80812019-09-16 23:49:29 -04001738 return None
1739
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001740
Keigo Oka4a09bd92019-05-07 14:01:00 +09001741def _check_no_new_gyp(_project, commit):
1742 """Verifies no project starts to use GYP."""
Keigo Oka4a09bd92019-05-07 14:01:00 +09001743 gypfiles = _filter_files(
1744 _get_affected_files(commit, include_symlinks=True, relative=True),
1745 [r'\.gyp$'])
1746
1747 if gypfiles:
1748 return HookFailure('GYP is deprecated and not allowed in a new project:',
1749 gypfiles)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001750 return None
Keigo Oka4a09bd92019-05-07 14:01:00 +09001751
1752
Ryan Cuiec4d6332011-05-02 14:15:25 -07001753# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001754
Ryan Cui1562fb82011-05-09 11:01:31 -07001755
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001756def _check_clang_format(_project, commit, options=()):
1757 """Runs clang-format on the given project"""
1758 hooks_dir = _get_hooks_dir()
1759 options = list(options)
1760 if commit == PRE_SUBMIT:
1761 options.append('--commit=HEAD')
1762 else:
1763 options.extend(['--commit', commit])
Brian Norris0c62a142018-12-11 13:24:29 -08001764 cmd = [os.path.join(hooks_dir, 'clang-format.py')] + options
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001765 cmd_result = cros_build_lib.run(cmd,
1766 print_cmd=False,
1767 stdout=True,
1768 encoding='utf-8',
1769 errors='replace',
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05001770 stderr=subprocess.STDOUT,
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001771 check=False)
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001772 if cmd_result.returncode:
1773 return HookFailure('clang-format.py errors/warnings\n\n' +
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001774 cmd_result.stdout)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001775 return None
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001776
1777
Mike Frysingerae409522014-02-01 03:16:11 -05001778def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001779 """Runs checkpatch.pl on the given project"""
Tomasz Figabd8faf62020-09-22 14:06:05 +00001780 # Bypass checkpatch for upstream or almost upstream commits, since we do not
1781 # intend to modify the upstream commits when landing them to our branches.
1782 # Any fixes should sent as independent patches.
1783 # The check is retained for FROMLIST and BACKPORT commits, as by definition
1784 # those can be still fixed up.
1785 desc = _get_commit_desc(commit)
1786 if desc.startswith('UPSTREAM:') or desc.startswith('FROMGIT:'):
1787 return None
1788
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001789 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001790 options = list(options)
Brian Norris4cee0502020-06-12 17:02:56 -07001791 if options and options[0].startswith('./') and os.path.exists(options[0]):
1792 cmdpath = options.pop(0)
1793 else:
1794 cmdpath = os.path.join(hooks_dir, 'checkpatch.pl')
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001795 if commit == PRE_SUBMIT:
1796 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1797 # this case.
1798 options.append('--ignore=MISSING_SIGN_OFF')
Filipe Brandenburger28d48d62015-10-07 09:48:54 -07001799 # Always ignore the check for the MAINTAINERS file. We do not track that
1800 # information on that file in our source trees, so let's suppress the
1801 # warning.
1802 options.append('--ignore=FILE_PATH_CHANGES')
Filipe Brandenburgerce978322015-10-09 10:04:15 -07001803 # Do not complain about the Change-Id: fields, since we use Gerrit.
1804 # Upstream does not want those lines (since they do not use Gerrit), but
1805 # we always do, so disable the check globally.
Daisuke Nojiri4844f892015-10-08 09:54:33 -07001806 options.append('--ignore=GERRIT_CHANGE_ID')
Brian Norris4cee0502020-06-12 17:02:56 -07001807 cmd = [cmdpath] + options + ['-']
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001808 cmd_result = cros_build_lib.run(
1809 cmd, print_cmd=False, input=_get_patch(commit).encode('utf-8'),
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05001810 stdout=True, stderr=subprocess.STDOUT, check=False, encoding='utf-8',
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001811 errors='replace')
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001812 if cmd_result.returncode:
Brian Norris4cee0502020-06-12 17:02:56 -07001813 return HookFailure('%s errors/warnings\n\n%s' %
1814 (cmdpath, cmd_result.stdout))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001815 return None
Ryan Cuiec4d6332011-05-02 14:15:25 -07001816
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001817
Brian Norris23c62e92018-11-14 12:25:51 -08001818def _run_kerneldoc(_project, commit, options=()):
1819 """Runs kernel-doc validator on the given project"""
1820 included, excluded = _parse_common_inclusion_options(options)
1821 files = _filter_files(_get_affected_files(commit, relative=True),
1822 included, excluded)
1823 if files:
1824 hooks_dir = _get_hooks_dir()
Brian Norris0c62a142018-12-11 13:24:29 -08001825 cmd = [os.path.join(hooks_dir, 'kernel-doc'), '-none'] + files
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05001826 output = _run_command(cmd, stderr=subprocess.STDOUT)
Brian Norris23c62e92018-11-14 12:25:51 -08001827 if output:
Brian Norris0c62a142018-12-11 13:24:29 -08001828 return HookFailure('kernel-doc errors/warnings:',
1829 items=output.splitlines())
Mike Frysinger8cf80812019-09-16 23:49:29 -04001830 return None
Brian Norris23c62e92018-11-14 12:25:51 -08001831
1832
Mike Frysingerae409522014-02-01 03:16:11 -05001833def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001834 """Makes sure kernel config changes are not mixed with code changes"""
1835 files = _get_affected_files(commit)
Mike Frysingerf252cfc2019-12-09 17:34:28 -05001836 if len(_filter_files(files, [r'chromeos/config'])) not in [0, len(files)]:
Olof Johanssona96810f2012-09-04 16:20:03 -07001837 return HookFailure('Changes to chromeos/config/ and regular files must '
1838 'be in separate commits:\n%s' % '\n'.join(files))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001839 return None
Anton Staaf815d6852011-08-22 10:08:45 -07001840
Mike Frysingerae409522014-02-01 03:16:11 -05001841
1842def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001843 """Checks that all JSON files are syntactically valid."""
Mike Frysinger908be682018-01-04 02:21:50 -05001844 ret = []
1845
1846 files = _filter_files(_get_affected_files(commit, relative=True),
1847 [r'.*\.json$'])
1848 for f in files:
1849 data = _get_file_content(f, commit)
Dale Curtis2975c432011-05-03 17:25:20 -07001850 try:
Mike Frysinger908be682018-01-04 02:21:50 -05001851 json.loads(data)
1852 except Exception as e:
1853 ret.append('%s: Invalid JSON: %s' % (f, e))
1854
1855 if ret:
1856 return HookFailure('\n'.join(ret))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001857 return None
Dale Curtis2975c432011-05-03 17:25:20 -07001858
1859
Mike Frysingerae409522014-02-01 03:16:11 -05001860def _check_manifests(_project, commit):
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001861 """Make sure Manifest files only have comments & DIST lines."""
1862 ret = []
Mike Frysinger52b537e2013-08-22 22:59:53 -04001863
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001864 manifests = _filter_files(_get_affected_files(commit, relative=True),
1865 [r'.*/Manifest$'])
1866 for path in manifests:
1867 data = _get_file_content(path, commit)
1868
1869 # Disallow blank files.
1870 if not data.strip():
1871 ret.append('%s: delete empty file' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001872 continue
1873
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001874 # Make sure the last newline isn't omitted.
1875 if data[-1] != '\n':
1876 ret.append('%s: missing trailing newline' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001877
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001878 # Do not allow leading or trailing blank lines.
1879 lines = data.splitlines()
1880 if not lines[0]:
1881 ret.append('%s: delete leading blank lines' % (path,))
1882 if not lines[-1]:
1883 ret.append('%s: delete trailing blank lines' % (path,))
1884
1885 for line in lines:
1886 # Disallow leading/trailing whitespace.
1887 if line != line.strip():
1888 ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
1889
1890 # Allow blank lines & comments.
1891 line = line.split('#', 1)[0]
1892 if not line:
1893 continue
1894
1895 # All other linse should start with DIST.
1896 if not line.startswith('DIST '):
1897 ret.append('%s: remove non-DIST lines: %s' % (path, line))
1898 break
1899
1900 if ret:
1901 return HookFailure('\n'.join(ret))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001902 return None
Mike Frysinger52b537e2013-08-22 22:59:53 -04001903
1904
Mike Frysingerae409522014-02-01 03:16:11 -05001905def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001906 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001907 if commit == PRE_SUBMIT:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001908 return None
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001909 BRANCH_RE = r'\nBRANCH=\S+'
1910
1911 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1912 msg = ('Changelist description needs BRANCH field (after first line)\n'
1913 'E.g. BRANCH=none or BRANCH=link,snow')
1914 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001915 return None
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001916
1917
Mike Frysinger45334bd2019-11-04 10:42:33 -05001918def _check_change_has_no_branch_field(_project, commit):
1919 """Verify 'BRANCH=' field does not exist in the commit message."""
1920 if commit == PRE_SUBMIT:
1921 return None
1922 BRANCH_RE = r'\nBRANCH=\S+'
1923
1924 if re.search(BRANCH_RE, _get_commit_desc(commit)):
1925 msg = 'This checkout does not use BRANCH= fields. Delete them.'
1926 return HookFailure(msg)
1927 return None
1928
1929
Mike Frysingerae409522014-02-01 03:16:11 -05001930def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001931 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001932 if commit == PRE_SUBMIT:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001933 return None
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001934 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1935
1936 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1937 msg = ('Changelist description needs Signed-off-by: field\n'
1938 'E.g. Signed-off-by: My Name <me@chromium.org>')
1939 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001940 return None
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001941
1942
Mike Frysinger9ab64b12019-11-04 10:53:08 -05001943def _check_change_has_no_signoff_field(_project, commit):
1944 """Verify 'Signed-off-by:' field does not exist in the commit message."""
1945 if commit == PRE_SUBMIT:
1946 return None
1947 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1948
1949 if re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1950 msg = 'This checkout does not use Signed-off-by: tags. Delete them.'
1951 return HookFailure(msg)
1952 return None
1953
1954
Jon Salz3ee59de2012-08-18 13:54:22 +08001955def _run_project_hook_script(script, project, commit):
1956 """Runs a project hook script.
1957
1958 The script is run with the following environment variables set:
1959 PRESUBMIT_PROJECT: The affected project
1960 PRESUBMIT_COMMIT: The affected commit
1961 PRESUBMIT_FILES: A newline-separated list of affected files
1962
1963 The script is considered to fail if the exit code is non-zero. It should
1964 write an error message to stdout.
1965 """
1966 env = dict(os.environ)
Alex Deymo643ac4c2015-09-03 10:40:50 -07001967 env['PRESUBMIT_PROJECT'] = project.name
Jon Salz3ee59de2012-08-18 13:54:22 +08001968 env['PRESUBMIT_COMMIT'] = commit
1969
1970 # Put affected files in an environment variable
1971 files = _get_affected_files(commit)
1972 env['PRESUBMIT_FILES'] = '\n'.join(files)
1973
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001974 cmd_result = cros_build_lib.run(cmd=script,
1975 env=env,
1976 shell=True,
1977 print_cmd=False,
1978 input=os.devnull,
1979 stdout=True,
1980 encoding='utf-8',
1981 errors='replace',
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05001982 stderr=subprocess.STDOUT,
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001983 check=False)
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001984 if cmd_result.returncode:
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001985 stdout = cmd_result.stdout
Jon Salz7b618af2012-08-31 06:03:16 +08001986 if stdout:
1987 stdout = re.sub('(?m)^', ' ', stdout)
1988 return HookFailure('Hook script "%s" failed with code %d%s' %
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001989 (script, cmd_result.returncode,
Jon Salz3ee59de2012-08-18 13:54:22 +08001990 ':\n' + stdout if stdout else ''))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001991 return None
Jon Salz3ee59de2012-08-18 13:54:22 +08001992
1993
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001994def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05001995 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001996
Brian Norris77608e12018-04-06 10:38:43 -07001997 files = _get_affected_files(commit, include_deletes=True, relative=True)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001998 prefix = os.path.commonprefix(files)
1999 prefix = os.path.dirname(prefix)
2000
2001 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07002002 if not prefix:
Mike Frysinger8cf80812019-09-16 23:49:29 -04002003 return None
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002004
2005 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07002006
2007 # The common files may all be within a subdirectory of the main project
2008 # directory, so walk up the tree until we find an alias file.
2009 # _get_affected_files() should return relative paths, but check against '/' to
2010 # ensure that this loop terminates even if it receives an absolute path.
2011 while prefix and prefix != '/':
2012 alias_file = os.path.join(prefix, '.project_alias')
2013
2014 # If an alias exists, use it.
2015 if os.path.isfile(alias_file):
2016 project_name = osutils.ReadFile(alias_file).strip()
2017
2018 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002019
2020 if not _get_commit_desc(commit).startswith(project_name + ': '):
2021 return HookFailure('The commit title for changes affecting only %s'
2022 ' should start with \"%s: \"'
2023 % (project_name, project_name))
Mike Frysinger8cf80812019-09-16 23:49:29 -04002024 return None
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002025
2026
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002027def _check_filepath_chartype(_project, commit):
2028 """Checks that FilePath::CharType stuff is not used."""
2029
2030 FILEPATH_REGEXP = re.compile('|'.join(
2031 [r'(?:base::)?FilePath::(?:Char|String|StringPiece)Type',
Satoru Takabayashi4ca37922018-08-08 10:16:38 +09002032 r'(?:base::)?FilePath::FromUTF8Unsafe',
2033 r'AsUTF8Unsafe',
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002034 r'FILE_PATH_LITERAL']))
2035 files = _filter_files(_get_affected_files(commit, relative=True),
2036 [r'.*\.(cc|h)$'])
2037
2038 errors = []
2039 for afile in files:
2040 for line_num, line in _get_file_diff(afile, commit):
2041 m = re.search(FILEPATH_REGEXP, line)
2042 if m:
2043 errors.append('%s, line %s has %s' % (afile, line_num, m.group(0)))
2044
2045 if errors:
2046 msg = 'Please assume FilePath::CharType is char (crbug.com/870621):'
2047 return HookFailure(msg, errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -04002048 return None
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002049
2050
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002051def _check_exec_files(_project, commit):
2052 """Make +x bits on files."""
2053 # List of files that should never be +x.
2054 NO_EXEC = (
2055 'ChangeLog*',
2056 'COPYING',
2057 'make.conf',
2058 'make.defaults',
2059 'Manifest',
2060 'OWNERS',
2061 'package.use',
2062 'package.keywords',
2063 'package.mask',
2064 'parent',
2065 'README',
2066 'TODO',
2067 '.gitignore',
2068 '*.[achly]',
2069 '*.[ch]xx',
2070 '*.boto',
2071 '*.cc',
2072 '*.cfg',
2073 '*.conf',
2074 '*.config',
2075 '*.cpp',
2076 '*.css',
2077 '*.ebuild',
2078 '*.eclass',
Tatsuhisa Yamaguchi3b053632019-01-10 14:45:14 +09002079 '*.gn',
2080 '*.gni',
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002081 '*.gyp',
2082 '*.gypi',
2083 '*.htm',
2084 '*.html',
2085 '*.ini',
2086 '*.js',
2087 '*.json',
2088 '*.md',
2089 '*.mk',
2090 '*.patch',
2091 '*.policy',
2092 '*.proto',
2093 '*.raw',
2094 '*.rules',
2095 '*.service',
2096 '*.target',
2097 '*.txt',
2098 '*.xml',
2099 '*.yaml',
2100 )
2101
2102 def FinalName(obj):
2103 # If the file is being deleted, then the dst_file is not set.
2104 if obj.dst_file is None:
2105 return obj.src_file
2106 else:
2107 return obj.dst_file
2108
2109 bad_files = []
2110 files = _get_affected_files(commit, relative=True, full_details=True)
2111 for f in files:
2112 mode = int(f.dst_mode, 8)
2113 if not mode & 0o111:
2114 continue
2115 name = FinalName(f)
2116 for no_exec in NO_EXEC:
2117 if fnmatch.fnmatch(name, no_exec):
2118 bad_files.append(name)
2119 break
2120
2121 if bad_files:
2122 return HookFailure('These files should not be executable. '
2123 'Please `chmod -x` them.', bad_files)
Mike Frysinger8cf80812019-09-16 23:49:29 -04002124 return None
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002125
2126
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002127# Base
2128
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002129# A list of hooks which are not project specific and check patch description
2130# (as opposed to patch body).
2131_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07002132 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07002133 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07002134 _check_change_has_test_field,
2135 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04002136 _check_commit_message_style,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08002137 _check_change_is_contribution,
Jack Neus8edbf642019-07-10 16:08:31 -06002138 _check_change_no_include_oem,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002139]
2140
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002141# A list of hooks that are not project-specific
2142_COMMON_HOOKS = [
Keiichi Watanabe6ab73fd2020-07-14 23:42:02 +09002143 _check_cargo_clippy,
Aviv Keshet5ac59522017-01-31 14:28:27 -08002144 _check_cros_license,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05002145 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05002146 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08002147 _check_ebuild_licenses,
Mike Frysingerb04778f2020-11-30 02:41:14 -05002148 _check_ebuild_owners,
Mike Frysinger6ee76b82020-11-20 01:16:06 -05002149 _check_ebuild_r0,
Mike Frysingercd363c82014-02-01 05:20:18 -05002150 _check_ebuild_virtual_pv,
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002151 _check_exec_files,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002152 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07002153 _check_gofmt,
Bernie Thompson8e26f742020-07-23 14:32:31 -07002154 _check_keywords,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04002155 _check_layout_conf,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002156 _check_no_long_lines,
Keigo Oka4a09bd92019-05-07 14:01:00 +09002157 _check_no_new_gyp,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002158 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07002159 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002160 _check_portage_make_use_var,
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -06002161 _check_rustfmt,
Aviv Keshet5ac59522017-01-31 14:28:27 -08002162 _check_tabbed_indents,
Ryan Cui9b651632011-05-11 11:38:58 -07002163]
Ryan Cuiec4d6332011-05-02 14:15:25 -07002164
Ryan Cui1562fb82011-05-09 11:01:31 -07002165
Ryan Cui9b651632011-05-11 11:38:58 -07002166# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04002167# that the flag controls (value).
2168_HOOK_FLAGS = {
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07002169 'clang_format_check': _check_clang_format,
Mike Frysingera7642f52015-03-25 18:31:42 -04002170 'checkpatch_check': _run_checkpatch,
Brian Norris23c62e92018-11-14 12:25:51 -08002171 'kerneldoc_check': _run_kerneldoc,
Ryan Cui9b651632011-05-11 11:38:58 -07002172 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04002173 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07002174 'long_line_check': _check_no_long_lines,
Alex Deymof5792ce2015-08-24 22:50:08 -07002175 'cros_license_check': _check_cros_license,
2176 'aosp_license_check': _check_aosp_license,
LaMont Jones872fe762020-02-10 15:36:14 -07002177 'gofmt_check': _check_gofmt,
Bernie Thompson8e26f742020-07-23 14:32:31 -07002178 'keyword_check': _check_keywords,
Ryan Cui9b651632011-05-11 11:38:58 -07002179 'tab_check': _check_no_tabs,
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08002180 'tabbed_indent_required_check': _check_tabbed_indents,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07002181 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08002182 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07002183 'bug_field_check': _check_change_has_bug_field,
2184 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07002185 'manifest_check': _check_manifests,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08002186 'contribution_check': _check_change_is_contribution,
Mike Frysinger47bd1252018-06-11 12:12:20 -04002187 'project_prefix_check': _check_project_prefix,
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002188 'filepath_chartype_check': _check_filepath_chartype,
Keiichi Watanabe6ab73fd2020-07-14 23:42:02 +09002189 'cargo_clippy_check': _check_cargo_clippy,
Julius Werner291d8602020-07-31 17:34:02 -07002190 'exec_files_check': _check_exec_files,
Mike Frysingera5c2a5b2020-12-18 01:57:13 -05002191 'kernel_splitconfig_check': _kernel_configcheck,
Ryan Cui9b651632011-05-11 11:38:58 -07002192}
2193
2194
Mike Frysinger3554bc92015-03-11 04:59:21 -04002195def _get_override_hooks(config):
2196 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07002197
2198 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08002199
2200 Args:
2201 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07002202 """
2203 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002204 SECTION_OPTIONS = 'Hook Overrides Options'
Ryan Cui9b651632011-05-11 11:38:58 -07002205
Mike Frysinger56e8de02019-07-31 14:40:14 -04002206 valid_keys = set(_HOOK_FLAGS.keys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002207 hooks = _HOOK_FLAGS.copy()
Mike Frysinger180ecd62020-08-19 00:41:51 -04002208 hook_overrides = set(
2209 config.options(SECTION) if config.has_section(SECTION) else [])
2210
2211 unknown_keys = hook_overrides - valid_keys
2212 if unknown_keys:
2213 raise ValueError(f'{_CONFIG_FILE}: [{SECTION}]: unknown keys: '
2214 f'{unknown_keys}')
Mike Frysinger3554bc92015-03-11 04:59:21 -04002215
2216 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07002217 disable_flags = []
Mike Frysinger180ecd62020-08-19 00:41:51 -04002218 for flag in valid_keys:
2219 if flag in hook_overrides:
2220 try:
2221 enabled = config.getboolean(SECTION, flag)
2222 except ValueError as e:
2223 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
2224 (flag, _CONFIG_FILE, e))
2225 elif hooks[flag] in _COMMON_HOOKS:
2226 # Enable common hooks by default so we process custom options below.
2227 enabled = True
2228 else:
2229 # All other hooks we left as a tristate. We use this below for a few
2230 # hooks to control default behavior.
2231 enabled = None
Mike Frysinger3554bc92015-03-11 04:59:21 -04002232
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002233 if enabled:
2234 enable_flags.append(flag)
Mike Frysinger180ecd62020-08-19 00:41:51 -04002235 elif enabled is not None:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002236 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07002237
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002238 # See if this hook has custom options.
2239 if enabled:
2240 try:
2241 options = config.get(SECTION_OPTIONS, flag)
2242 hooks[flag] = functools.partial(hooks[flag], options=options.split())
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002243 hooks[flag].__name__ = flag
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04002244 except (configparser.NoOptionError, configparser.NoSectionError):
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002245 pass
2246
2247 enabled_hooks = set(hooks[x] for x in enable_flags)
2248 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger45334bd2019-11-04 10:42:33 -05002249
Mike Frysinger9ab64b12019-11-04 10:53:08 -05002250 if _check_change_has_signoff_field not in enabled_hooks:
Drew Davenport85848e52020-01-15 14:40:29 -07002251 if _check_change_has_signoff_field not in disabled_hooks:
2252 enabled_hooks.add(_check_change_has_no_signoff_field)
Mike Frysinger45334bd2019-11-04 10:42:33 -05002253 if _check_change_has_branch_field not in enabled_hooks:
2254 enabled_hooks.add(_check_change_has_no_branch_field)
2255
Mike Frysinger3554bc92015-03-11 04:59:21 -04002256 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07002257
2258
Jon Salz3ee59de2012-08-18 13:54:22 +08002259def _get_project_hook_scripts(config):
2260 """Returns a list of project-specific hook scripts.
2261
2262 Args:
2263 config: A ConfigParser for the project's config file.
2264 """
2265 SECTION = 'Hook Scripts'
2266 if not config.has_section(SECTION):
2267 return []
2268
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002269 return config.items(SECTION)
Jon Salz3ee59de2012-08-18 13:54:22 +08002270
2271
Mike Frysingerff916c62020-12-18 01:58:08 -05002272def _get_project_hooks(presubmit, config_file=None):
Ryan Cui9b651632011-05-11 11:38:58 -07002273 """Returns a list of hooks that need to be run for a project.
2274
2275 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002276
2277 Args:
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002278 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002279 config_file: A string, the config file. Defaults to _CONFIG_FILE.
Ryan Cui9b651632011-05-11 11:38:58 -07002280 """
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04002281 config = configparser.RawConfigParser()
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002282 if config_file is None:
2283 config_file = _CONFIG_FILE
2284 if not os.path.exists(config_file):
Jon Salz3ee59de2012-08-18 13:54:22 +08002285 # Just use an empty config file
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04002286 config = configparser.RawConfigParser()
Shuhei Takahashi3cbb8dd2019-10-29 12:37:11 +09002287 else:
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002288 config.read(config_file)
Jon Salz3ee59de2012-08-18 13:54:22 +08002289
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002290 if presubmit:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07002291 hooks = _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002292 else:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07002293 hooks = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002294
Mike Frysinger3554bc92015-03-11 04:59:21 -04002295 enabled_hooks, disabled_hooks = _get_override_hooks(config)
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07002296 hooks = [hook for hook in hooks if hook not in disabled_hooks]
2297
2298 # If a list is both in _COMMON_HOOKS and also enabled explicitly through an
2299 # override, keep the override only. Note that the override may end up being
2300 # a functools.partial, in which case we need to extract the .func to compare
2301 # it to the common hooks.
2302 unwrapped_hooks = [getattr(hook, 'func', hook) for hook in enabled_hooks]
2303 hooks = [hook for hook in hooks if hook not in unwrapped_hooks]
2304
2305 hooks = list(enabled_hooks) + hooks
Ryan Cui9b651632011-05-11 11:38:58 -07002306
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002307 for name, script in _get_project_hook_scripts(config):
2308 func = functools.partial(_run_project_hook_script, script)
2309 func.__name__ = name
2310 hooks.append(func)
Jon Salz3ee59de2012-08-18 13:54:22 +08002311
Ryan Cui9b651632011-05-11 11:38:58 -07002312 return hooks
2313
2314
Alex Deymo643ac4c2015-09-03 10:40:50 -07002315def _run_project_hooks(project_name, proj_dir=None,
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002316 commit_list=None, presubmit=False,
2317 config_file=None):
Ryan Cui1562fb82011-05-09 11:01:31 -07002318 """For each project run its project specific hook from the hooks dictionary.
2319
2320 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002321 project_name: The name of project to run hooks for.
Doug Anderson44a644f2011-11-02 10:37:37 -07002322 proj_dir: If non-None, this is the directory the project is in. If None,
2323 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07002324 commit_list: A list of commits to run hooks against. If None or empty list
2325 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002326 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002327 config_file: A string, the presubmit config file. If not specified,
2328 defaults to PRESUBMIT.cfg in the project directory.
Ryan Cui1562fb82011-05-09 11:01:31 -07002329
2330 Returns:
2331 Boolean value of whether any errors were ecountered while running the hooks.
2332 """
Doug Anderson44a644f2011-11-02 10:37:37 -07002333 if proj_dir is None:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002334 proj_dirs = _run_command(
2335 ['repo', 'forall', project_name, '-c', 'pwd']).split()
Mike Frysingere52b1bc2019-09-16 23:45:41 -04002336 if not proj_dirs:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002337 print('%s cannot be found.' % project_name, file=sys.stderr)
David James2edd9002013-10-11 14:09:19 -07002338 print('Please specify a valid project.', file=sys.stderr)
2339 return True
2340 if len(proj_dirs) > 1:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002341 print('%s is associated with multiple directories.' % project_name,
David James2edd9002013-10-11 14:09:19 -07002342 file=sys.stderr)
2343 print('Please specify a directory to help disambiguate.', file=sys.stderr)
2344 return True
2345 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07002346
Ryan Cuiec4d6332011-05-02 14:15:25 -07002347 pwd = os.getcwd()
2348 # hooks assume they are run from the root of the project
2349 os.chdir(proj_dir)
2350
Mike Frysingered1b95a2019-12-12 19:04:51 -05002351 color = terminal.Color()
2352
Alex Deymo643ac4c2015-09-03 10:40:50 -07002353 remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
2354 '--symbolic-full-name', '@{u}']).strip()
2355 if not remote_branch:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002356 print("Your project %s doesn't track any remote repo." % project_name,
Alex Deymo643ac4c2015-09-03 10:40:50 -07002357 file=sys.stderr)
2358 remote = None
2359 else:
Josh Pratt15d13ab2018-08-13 11:52:48 +10002360 branch_items = remote_branch.split('/', 1)
2361 if len(branch_items) != 2:
2362 PrintErrorForProject(
2363 project_name,
2364 HookFailure(
2365 'Cannot get remote and branch name (%s)' % remote_branch))
2366 os.chdir(pwd)
2367 return True
2368 remote, _branch = branch_items
Alex Deymo643ac4c2015-09-03 10:40:50 -07002369
2370 project = Project(name=project_name, dir=proj_dir, remote=remote)
2371
Doug Anderson14749562013-06-26 13:38:29 -07002372 if not commit_list:
2373 try:
2374 commit_list = _get_commits()
2375 except VerifyException as e:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002376 PrintErrorForProject(project.name, HookFailure(str(e)))
Doug Anderson14749562013-06-26 13:38:29 -07002377 os.chdir(pwd)
2378 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07002379
Mike Frysingerff916c62020-12-18 01:58:08 -05002380 hooks = _get_project_hooks(presubmit, config_file)
Ryan Cui1562fb82011-05-09 11:01:31 -07002381 error_found = False
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002382 commit_count = len(commit_list)
Mike Frysingerb99b3772019-08-17 14:19:44 -04002383 hook_count = len(hooks)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002384 for i, commit in enumerate(commit_list):
Mike Frysingerb2496652019-09-12 23:35:46 -04002385 CACHE.clear()
2386
LaMont Jones69d3e182020-03-11 15:00:53 -06002387 # If run with --pre-submit, then commit is PRE_SUBMIT, and not a commit.
2388 # Use that as the description.
2389 desc = commit if commit == PRE_SUBMIT else _get_commit_desc(commit)
Mike Frysingered1b95a2019-12-12 19:04:51 -05002390 print('[%s %i/%i %s] %s' %
2391 (color.Color(color.CYAN, 'COMMIT'), i + 1, commit_count, commit[0:12],
2392 desc.splitlines()[0]))
2393
Mike Frysingerb99b3772019-08-17 14:19:44 -04002394 for h, hook in enumerate(hooks):
Mike Frysingered1b95a2019-12-12 19:04:51 -05002395 output = ('[%s %i/%i PRESUBMIT.cfg] %s' %
2396 (color.Color(color.YELLOW, 'RUNNING'), h + 1, hook_count,
2397 hook.__name__))
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002398 print(output, end='\r')
2399 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07002400 hook_error = hook(project, commit)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002401 print(' ' * len(output), end='\r')
2402 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07002403 if hook_error:
Mike Frysingered1b95a2019-12-12 19:04:51 -05002404 if not isinstance(hook_error, list):
2405 hook_error = [hook_error]
2406 PrintErrorsForCommit(color, hook, project.name, hook_error)
Ryan Cui1562fb82011-05-09 11:01:31 -07002407 error_found = True
Don Garrettdba548a2011-05-05 15:17:14 -07002408
Ryan Cuiec4d6332011-05-02 14:15:25 -07002409 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07002410 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002411
Mike Frysingerae409522014-02-01 03:16:11 -05002412
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002413# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002414
Ryan Cui1562fb82011-05-09 11:01:31 -07002415
Mike Frysingerae409522014-02-01 03:16:11 -05002416def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08002417 """Main function invoked directly by repo.
2418
2419 This function will exit directly upon error so that repo doesn't print some
2420 obscure error message.
2421
2422 Args:
2423 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07002424 worktree_list: A list of directories. It should be the same length as
2425 project_list, so that each entry in project_list matches with a directory
2426 in worktree_list. If None, we will attempt to calculate the directories
2427 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08002428 kwargs: Leave this here for forward-compatibility.
2429 """
Mike Frysingered1b95a2019-12-12 19:04:51 -05002430 start_time = datetime.datetime.now()
Ryan Cui1562fb82011-05-09 11:01:31 -07002431 found_error = False
David James2edd9002013-10-11 14:09:19 -07002432 if not worktree_list:
2433 worktree_list = [None] * len(project_list)
2434 for project, worktree in zip(project_list, worktree_list):
2435 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07002436 found_error = True
2437
Mike Frysingered1b95a2019-12-12 19:04:51 -05002438 end_time = datetime.datetime.now()
2439 color = terminal.Color()
Mike Frysingerae409522014-02-01 03:16:11 -05002440 if found_error:
Mike Frysingered1b95a2019-12-12 19:04:51 -05002441 msg = ('%s: Preupload failed due to above error(s).\n'
Ryan Cui9b651632011-05-11 11:38:58 -07002442 '- To disable some source style checks, and for other hints, see '
Mike Frysingered1b95a2019-12-12 19:04:51 -05002443 '<checkout_dir>/src/repohooks/README.md\n'
2444 "- To upload only current project, run 'repo upload .'" %
2445 (color.Color(color.RED, 'FATAL'),))
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04002446 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07002447 sys.exit(1)
Mike Frysingered1b95a2019-12-12 19:04:51 -05002448 else:
2449 msg = ('[%s] repohooks passed in %s' %
2450 (color.Color(color.GREEN, 'PASSED'), end_time - start_time))
2451 print(msg)
Anush Elangovan63afad72011-03-23 00:41:27 -07002452
Ryan Cui1562fb82011-05-09 11:01:31 -07002453
Doug Anderson44a644f2011-11-02 10:37:37 -07002454def _identify_project(path):
2455 """Identify the repo project associated with the given path.
2456
2457 Returns:
2458 A string indicating what project is associated with the path passed in or
2459 a blank string upon failure.
2460 """
2461 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002462 stderr=True, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002463
2464
Mike Frysinger55f85b52014-12-18 14:45:21 -05002465def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07002466 """Run hooks directly (outside of the context of repo).
2467
Doug Anderson44a644f2011-11-02 10:37:37 -07002468 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05002469 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07002470
2471 Returns:
2472 0 if no pre-upload failures, 1 if failures.
2473
2474 Raises:
2475 BadInvocation: On some types of invocation errors.
2476 """
Mike Frysinger66142932014-12-18 14:55:57 -05002477 parser = commandline.ArgumentParser(description=__doc__)
2478 parser.add_argument('--dir', default=None,
2479 help='The directory that the project lives in. If not '
2480 'specified, use the git project root based on the cwd.')
2481 parser.add_argument('--project', default=None,
2482 help='The project repo path; this can affect how the '
2483 'hooks get run, since some hooks are project-specific. '
2484 'For chromite this is chromiumos/chromite. If not '
2485 'specified, the repo tool will be used to figure this '
2486 'out based on the dir.')
2487 parser.add_argument('--rerun-since', default=None,
Vadim Bendebury75447b92018-01-10 12:06:01 -08002488 help='Rerun hooks on old commits since some point '
2489 'in the past. The argument could be a date (should '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002490 "match git log's concept of a date, e.g. 2012-06-20), "
Vadim Bendebury75447b92018-01-10 12:06:01 -08002491 'or a SHA1, or just a number of commits to check (from 1 '
2492 'to 99). This option is mutually exclusive with '
2493 '--pre-submit.')
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002494 parser.add_argument('--pre-submit', action='store_true',
Mike Frysinger66142932014-12-18 14:55:57 -05002495 help='Run the check against the pending commit. '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002496 "This option should be used at the 'git commit' "
2497 "phase as opposed to 'repo upload'. This option "
Mike Frysinger66142932014-12-18 14:55:57 -05002498 'is mutually exclusive with --rerun-since.')
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002499 parser.add_argument('--presubmit-config',
2500 help='Specify presubmit config file to be used.')
Mike Frysinger66142932014-12-18 14:55:57 -05002501 parser.add_argument('commits', nargs='*',
2502 help='Check specific commits')
2503 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07002504
Doug Anderson14749562013-06-26 13:38:29 -07002505 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05002506 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002507 raise BadInvocation("Can't pass commits and use rerun-since: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002508 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07002509
Vadim Bendebury75447b92018-01-10 12:06:01 -08002510 if len(opts.rerun_since) < 3 and opts.rerun_since.isdigit():
2511 # This must be the number of commits to check. We don't expect the user
2512 # to want to check more than 99 commits.
2513 limit = '-n%s' % opts.rerun_since
2514 elif git.IsSHA1(opts.rerun_since, False):
2515 limit = '%s..' % opts.rerun_since
2516 else:
2517 # This better be a date.
2518 limit = '--since=%s' % opts.rerun_since
2519 cmd = ['git', 'log', limit, '--pretty=%H']
Doug Anderson14749562013-06-26 13:38:29 -07002520 all_commits = _run_command(cmd).splitlines()
2521 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
2522
2523 # Eliminate chrome-bot commits but keep ordering the same...
2524 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05002525 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07002526
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002527 if opts.pre_submit:
2528 raise BadInvocation('rerun-since and pre-submit can not be '
2529 'used together')
2530 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05002531 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002532 raise BadInvocation("Can't pass commits and use pre-submit: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002533 ' '.join(opts.commits))
2534 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07002535
2536 # Check/normlaize git dir; if unspecified, we'll use the root of the git
2537 # project from CWD
2538 if opts.dir is None:
Mike Frysinger9ba001a2020-11-20 01:02:11 -05002539 git_dir = _run_command(['git', 'rev-parse', '--show-toplevel'],
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002540 stderr=True).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002541 if not git_dir:
2542 raise BadInvocation('The current directory is not part of a git project.')
Mike Frysinger9ba001a2020-11-20 01:02:11 -05002543 opts.dir = git_dir
Doug Anderson44a644f2011-11-02 10:37:37 -07002544 elif not os.path.isdir(opts.dir):
2545 raise BadInvocation('Invalid dir: %s' % opts.dir)
2546 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
2547 raise BadInvocation('Not a git directory: %s' % opts.dir)
2548
2549 # Identify the project if it wasn't specified; this _requires_ the repo
2550 # tool to be installed and for the project to be part of a repo checkout.
2551 if not opts.project:
2552 opts.project = _identify_project(opts.dir)
2553 if not opts.project:
2554 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
2555
Doug Anderson14749562013-06-26 13:38:29 -07002556 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05002557 commit_list=opts.commits,
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002558 presubmit=opts.pre_submit,
2559 config_file=opts.presubmit_config)
Doug Anderson44a644f2011-11-02 10:37:37 -07002560 if found_error:
2561 return 1
2562 return 0
2563
2564
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002565if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05002566 sys.exit(direct_main(sys.argv[1:]))