blob: 4977e36e746e24b49ff455873616e523226e21b5 [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
Daisuke Nojiri062a0a92021-02-14 15:38:16 -0800616def _check_keywords_in_file(project, commit, file, keywords,
617 unblocked_terms_file, opts):
618 """Checks there are no blocked keywords in a file being changed."""
619 if file:
620 # Search for UNBLOCKED_TERMS_FILE in the parent directories of the file
621 # being changed.
622 d = os.path.dirname(file)
623 while d != project.dir:
624 terms_file = os.path.join(d, UNBLOCKED_TERMS_FILE)
625 if os.path.isfile(terms_file):
626 unblocked_terms_file = terms_file
627 break
628 d = os.path.dirname(d)
Bernie Thompson8e26f742020-07-23 14:32:31 -0700629
Daisuke Nojiri062a0a92021-02-14 15:38:16 -0800630 # Read unblocked word list.
Daisuke Nojiri2089e012020-08-20 15:12:36 -0700631 unblocked_words = _read_terms_file(unblocked_terms_file)
632 unblocked_words.update(opts.unblock)
Daisuke Nojiri2089e012020-08-20 15:12:36 -0700633 keywords = sorted(keywords - unblocked_words)
Bernie Thompson8e26f742020-07-23 14:32:31 -0700634
635 def _check_line(line):
Laurent Chavey434af9a2020-09-28 22:25:16 +0900636 # Store information about each span matching blocking regex.
637 # to match unblocked regex with blocked reg ex match.
638 # [{'span':re.span, - overlap of matching regex in line
639 # 'group':re.group, - matching term
640 # 'blocked':bool, - True: matching blocked, False: matching unblocked
641 # 'keyword':regex, - block regex
642 # }, ...]
643 blocked_span = []
644 # Store information about each span matching unblocking regex.
645 # [re.span, ...]
646 unblocked_span = []
647
Bernie Thompson8e26f742020-07-23 14:32:31 -0700648 for word in keywords:
Laurent Chavey434af9a2020-09-28 22:25:16 +0900649 for match in re.finditer(word, line, flags=re.I):
650 blocked_span.append({'span' : match.span(),
651 'group' : match.group(0),
652 'blocked' : True,
653 'keyword' : word})
654
655 for unblocked in unblocked_words:
656 for match in re.finditer(unblocked, line, flags=re.I):
657 unblocked_span.append(match.span())
658
659 # Unblock terms that are superset of blocked terms:
660 # blocked := "this.?word"
661 # unblocked := "\.this.?word"
662 # "this line is blocked because of this1word"
663 # "this line is unblocked because of thenew.this1word"
664 #
665 for b in blocked_span:
666 for ub in unblocked_span:
667 if ub[0] <= b['span'][0] and ub[1] >= b['span'][1]:
668 b['blocked'] = False
669 if b['blocked']:
670 return f'Matched "{b["group"]}" with regex of "{b["keyword"]}"'
Bernie Thompson8e26f742020-07-23 14:32:31 -0700671 return False
672
Daisuke Nojiri062a0a92021-02-14 15:38:16 -0800673 if file:
674 return _check_lines_in_diff(commit, [file], _check_line,
675 'Found a blocked keyword in:')
Bernie Thompson8e26f742020-07-23 14:32:31 -0700676
677 line_num = 1
678 commit_desc_errors = []
679 for line in _get_commit_desc(commit).splitlines():
680 result = _check_line(line)
681 if result:
682 commit_desc_errors.append('Commit message, line %s: %s' %
683 (line_num, result))
684 line_num += 1
685 if commit_desc_errors:
Daisuke Nojiri062a0a92021-02-14 15:38:16 -0800686 return HookFailure('Found a blocked keyword in:', commit_desc_errors)
687
688 return None
689
690
691def _check_keywords(project, commit, options=()):
692 """Checks there are no blocked keywords in commit content."""
693 # Read options from override list.
694 parser = argparse.ArgumentParser()
695 parser.add_argument('--exclude_regex', action='append', default=[])
696 parser.add_argument('--include_regex', action='append', default=[])
697 parser.add_argument('--block', action='append', default=[])
698 parser.add_argument('--unblock', action='append', default=[])
699 opts = parser.parse_args(options)
700
701 # Read blocked word list.
702 blocked_terms_file = os.path.join(_get_hooks_dir(), BLOCKED_TERMS_FILE)
703 common_keywords = _read_terms_file(blocked_terms_file)
704
705 # Find unblocked word list in project root directory. If not found, global
706 # list is used.
707 unblocked_terms_file = os.path.join(_get_hooks_dir(), UNBLOCKED_TERMS_FILE)
708 if os.path.isfile(os.path.join(project.dir, UNBLOCKED_TERMS_FILE)):
709 unblocked_terms_file = os.path.join(project.dir, UNBLOCKED_TERMS_FILE)
710
711 keywords = set(common_keywords | set(opts.block))
712
713 files = _filter_files(
714 _get_affected_files(commit),
715 opts.include_regex + COMMON_INCLUDED_PATHS + [r'^.*\.md$'],
716 opts.exclude_regex + COMMON_EXCLUDED_PATHS)
717
718 errors = []
719 for file in files:
720 errs = _check_keywords_in_file(project, commit, file,
721 keywords, unblocked_terms_file, opts)
722 if errs:
723 errors.append(errs)
724
725 errs = _check_keywords_in_file(project, commit, None,
726 keywords, unblocked_terms_file, opts)
727 if errs:
728 errors.append(errs)
729
Bernie Thompson8e26f742020-07-23 14:32:31 -0700730 return errors
731
732
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900733def _check_tabbed_indents(_project, commit, options=()):
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800734 """Checks that indents use tabs only."""
735 TABS_REQUIRED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400736 r'.*\.ebuild$',
737 r'.*\.eclass$',
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800738 ]
739 LEADING_SPACE_RE = re.compile('[\t]* ')
740
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900741 included, excluded = _parse_common_inclusion_options(options)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800742 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900743 included + TABS_REQUIRED_PATHS,
744 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800745 return _check_lines_in_diff(
746 commit, files,
747 lambda line: LEADING_SPACE_RE.match(line) is not None,
748 'Found a space in indentation (must be all tabs):')
Ryan Cui1562fb82011-05-09 11:01:31 -0700749
Ryan Cuiec4d6332011-05-02 14:15:25 -0700750
LaMont Jones872fe762020-02-10 15:36:14 -0700751def _check_gofmt(_project, commit, options=()):
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700752 """Checks that Go files are formatted with gofmt."""
LaMont Jones872fe762020-02-10 15:36:14 -0700753 included, excluded = _parse_common_inclusion_options(options)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700754 errors = []
755 files = _filter_files(_get_affected_files(commit, relative=True),
LaMont Jones872fe762020-02-10 15:36:14 -0700756 included + [r'\.go$'],
757 excluded)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700758
759 for gofile in files:
760 contents = _get_file_content(gofile, commit)
Mike Frysingeraa7dc942019-09-25 00:07:24 -0400761 output = _run_command(cmd=['gofmt', '-l'], input=contents.encode('utf-8'),
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -0500762 stderr=subprocess.STDOUT)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700763 if output:
764 errors.append(gofile)
765 if errors:
766 return HookFailure('Files not formatted with gofmt:', errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400767 return None
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700768
769
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600770def _check_rustfmt(_project, commit):
771 """Checks that Rust files are formatted with rustfmt."""
772 errors = []
773 files = _filter_files(_get_affected_files(commit, relative=True),
774 [r'\.rs$'])
775
776 for rustfile in files:
777 contents = _get_file_content(rustfile, commit)
Chirantan Ekbote1da56d52020-07-13 17:35:35 +0900778 output = _run_command(cmd=['rustfmt', '--edition', '2018'],
779 input=contents.encode('utf-8'),
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -0500780 stderr=subprocess.STDOUT)
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600781 if output != contents:
782 errors.append(rustfile)
783 if errors:
784 return HookFailure('Files not formatted with rustfmt: '
785 "(run 'cargo fmt' to fix)", errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400786 return None
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600787
788
Keiichi Watanabe6ab73fd2020-07-14 23:42:02 +0900789class CargoClippyArgumentParserError(Exception):
790 """An exception indicating an invalid check_cargo_clippy option."""
791
792
793class CargoClippyArgumentParser(argparse.ArgumentParser):
794 """A argument parser for check_cargo_clippy."""
795
796 def error(self, message):
797 raise CargoClippyArgumentParserError(message)
798
799
800# A cargo project in which clippy runs.
801ClippyProject = collections.namedtuple('ClippyProject', ('root', 'script'))
802
803
804class _AddClippyProjectAction(argparse.Action):
805 """A callback that adds a cargo clippy setting.
806
807 It accepts a value which is in the form of "ROOT[:SCRIPT]".
808 """
809
810 def __call__(self, parser, namespace, values, option_string=None):
811 if getattr(namespace, self.dest, None) is None:
812 setattr(namespace, self.dest, [])
813 spec = values.split(':', 1)
814 if len(spec) == 1:
815 spec += [None]
816
817 if spec[0].startswith('/'):
818 raise CargoClippyArgumentParserError('root path must not start with "/"'
819 f' but "{spec[0]}"')
820
821 clippy = ClippyProject(root=spec[0], script=spec[1])
822 getattr(namespace, self.dest).append(clippy)
823
824
825def _get_cargo_clippy_parser():
826 """Creates a parser for check_cargo_clippy options."""
827
828 parser = CargoClippyArgumentParser()
829 parser.add_argument('--project', action=_AddClippyProjectAction, default=[])
830
831 return parser
832
833
834def _check_cargo_clippy(project, commit, options=()):
835 """Checks that a change doesn't produce cargo-clippy errors."""
836
837 options = list(options)
838 if not options:
839 return []
840 parser = _get_cargo_clippy_parser()
841
842 try:
843 opts = parser.parse_args(options)
844 except CargoClippyArgumentParserError as e:
845 return [HookFailure('invalid check_cargo_clippy option is given.'
846 f' Please check PRESUBMIT.cfg is correct: {e}')]
847 files = _get_affected_files(commit)
848
849 errors = []
850 for clippy in opts.project:
851 root = os.path.normpath(os.path.join(project.dir, clippy.root))
852
853 # Check if any file under `root` was modified.
854 modified = False
855 for f in files:
856 if f.startswith(root):
857 modified = True
858 break
859 if not modified:
860 continue
861
862 # Clean cargo's cache when we run clippy for this `root` for the first time.
863 # We don't want to clean the cache between commits to save time when
864 # multiple commits are checked.
865 if root not in _check_cargo_clippy.cleaned_root:
866 _run_command(['cargo', 'clean',
867 f'--manifest-path={root}/Cargo.toml'],
868 stderr=subprocess.STDOUT)
869 _check_cargo_clippy.cleaned_root.add(root)
870
871 cmd = ['cargo', 'clippy', '--all-features', '--all-targets',
872 f'--manifest-path={root}/Cargo.toml',
873 '--', '-D', 'warnings']
874 # Overwrite the clippy command if a project-specific script is specified.
875 if clippy.script:
876 cmd = [os.path.join(project.dir, clippy.script)]
877
878 output = _run_command(cmd, stderr=subprocess.STDOUT)
879 error = re.search(r'^error: ', output, flags=re.MULTILINE)
880 if error:
881 msg = output[error.start():]
882 errors.append(HookFailure(msg))
883
884 return errors
885
886
887# Stores cargo projects in which `cargo clean` ran.
888_check_cargo_clippy.cleaned_root = set()
889
890
Mike Frysingerae409522014-02-01 03:16:11 -0500891def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700892 """Check for a non-empty 'TEST=' field in the commit message."""
Mike Frysingered9b2a02019-12-12 18:52:32 -0500893 SEE_ALSO = 'Please review the documentation:\n%s' % (DOC_COMMIT_MSG_URL,)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700894
Cheng Yuehb707c522020-01-02 14:06:59 +0800895 if not re.search(TEST_FIELD_RE, _get_commit_desc(commit)):
Mike Frysingered9b2a02019-12-12 18:52:32 -0500896 msg = ('Changelist description needs TEST field (after first line)\n%s' %
897 (SEE_ALSO,))
Ryan Cui1562fb82011-05-09 11:01:31 -0700898 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400899 return None
Ryan Cui1562fb82011-05-09 11:01:31 -0700900
Ryan Cuiec4d6332011-05-02 14:15:25 -0700901
Mike Frysingerae409522014-02-01 03:16:11 -0500902def _check_change_has_valid_cq_depend(_project, commit):
Jason D. Clinton299e3222019-05-23 09:42:03 -0600903 """Check for a correctly formatted Cq-Depend field in the commit message."""
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700904 desc = _get_commit_desc(commit)
Jason D. Clinton299e3222019-05-23 09:42:03 -0600905 msg = 'Changelist has invalid Cq-Depend target.'
906 example = 'Example: Cq-Depend: chromium:1234, chrome-internal:2345'
David Jamesc3b68b32013-04-03 09:17:03 -0700907 try:
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700908 patch.GetPaladinDeps(desc)
David Jamesc3b68b32013-04-03 09:17:03 -0700909 except ValueError as ex:
910 return HookFailure(msg, [example, str(ex)])
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700911 # Check that Cq-Depend is in the same paragraph as Change-Id.
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500912 msg = 'Cq-Depend is not in the same paragraph as Change-Id.'
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700913 paragraphs = desc.split('\n\n')
914 for paragraph in paragraphs:
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500915 if (re.search(r'^Cq-Depend:', paragraph, re.M) and not
916 re.search('^Change-Id:', paragraph, re.M)):
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700917 return HookFailure(msg)
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500918
919 # We no longer support CQ-DEPEND= lines.
920 if re.search(r'^CQ-DEPEND[=:]', desc, re.M):
921 return HookFailure(
922 'CQ-DEPEND= is no longer supported. Please see:\n'
923 'https://chromium.googlesource.com/chromiumos/docs/+/HEAD/'
924 'contributing.md#CQ-DEPEND')
925
Mike Frysinger8cf80812019-09-16 23:49:29 -0400926 return None
David Jamesc3b68b32013-04-03 09:17:03 -0700927
928
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800929def _check_change_is_contribution(_project, commit):
930 """Check that the change is a contribution."""
931 NO_CONTRIB = 'not a contribution'
932 if NO_CONTRIB in _get_commit_desc(commit).lower():
933 msg = ('Changelist is not a contribution, this cannot be accepted.\n'
934 'Please remove the "%s" text from the commit message.') % NO_CONTRIB
935 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400936 return None
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800937
938
Alex Deymo643ac4c2015-09-03 10:40:50 -0700939def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700940 """Check for a correctly formatted 'BUG=' field in the commit message."""
Mike Frysingered9b2a02019-12-12 18:52:32 -0500941 SEE_ALSO = 'Please review the documentation:\n%s' % (DOC_COMMIT_MSG_URL,)
942
David James5c0073d2013-04-03 08:48:52 -0700943 OLD_BUG_RE = r'\nBUG=.*chromium-os'
944 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
945 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
946 'the chromium tracker in your BUG= line now.')
947 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700948
Alex Deymo643ac4c2015-09-03 10:40:50 -0700949 # Android internal and external projects use "Bug: " to track bugs in
950 # buganizer.
951 BUG_COLON_REMOTES = (
952 'aosp',
953 'goog',
954 )
955 if project.remote in BUG_COLON_REMOTES:
956 BUG_RE = r'\nBug: ?([Nn]one|\d+)'
957 if not re.search(BUG_RE, _get_commit_desc(commit)):
958 msg = ('Changelist description needs BUG field (after first line):\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500959 'Examples:\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700960 'Bug: 9999 (for buganizer)\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500961 'BUG=None\n%s' % (SEE_ALSO,))
Alex Deymo643ac4c2015-09-03 10:40:50 -0700962 return HookFailure(msg)
963 else:
Jorge Lucangeli Obesdce214e2017-10-25 15:04:39 -0400964 BUG_RE = r'\nBUG=([Nn]one|(chromium|b):\d+)'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700965 if not re.search(BUG_RE, _get_commit_desc(commit)):
966 msg = ('Changelist description needs BUG field (after first line):\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500967 'Examples:\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700968 'BUG=chromium:9999 (for public tracker)\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700969 'BUG=b:9999 (for buganizer)\n'
Mike Frysingered9b2a02019-12-12 18:52:32 -0500970 'BUG=None\n%s' % (SEE_ALSO,))
Alex Deymo643ac4c2015-09-03 10:40:50 -0700971 return HookFailure(msg)
Ryan Cui1562fb82011-05-09 11:01:31 -0700972
Cheng Yuehb707c522020-01-02 14:06:59 +0800973 TEST_BEFORE_BUG_RE = TEST_FIELD_RE + r'.*' + BUG_RE
974
975 if re.search(TEST_BEFORE_BUG_RE, _get_commit_desc(commit), re.DOTALL):
976 msg = ('The BUG field must come before the TEST field.\n%s' %
977 (SEE_ALSO,))
978 return HookFailure(msg)
979
Mike Frysinger8cf80812019-09-16 23:49:29 -0400980 return None
981
Ryan Cuiec4d6332011-05-02 14:15:25 -0700982
Jack Neus8edbf642019-07-10 16:08:31 -0600983def _check_change_no_include_oem(project, commit):
984 """Check that the change does not reference OEMs."""
985 ALLOWLIST = {
986 'chromiumos/platform/ec',
987 # Used by unit tests.
988 'project',
989 }
990 if project.name not in ALLOWLIST:
991 return None
992
Mike Frysingerbb34a222019-07-31 14:40:46 -0400993 TAGS = {
Jack Neus8edbf642019-07-10 16:08:31 -0600994 'Reviewed-on',
995 'Reviewed-by',
996 'Signed-off-by',
997 'Commit-Ready',
998 'Tested-by',
999 'Commit-Queue',
Jack Neus8edbf642019-07-10 16:08:31 -06001000 'Acked-by',
1001 'Modified-by',
1002 'CC',
1003 'Suggested-by',
1004 'Reported-by',
1005 'Acked-for-chrome-by',
LaMont Jones237f3ef2020-01-22 10:40:52 -07001006 'Cq-Cl-Tag',
LaMont Jonesfb5e8bf2020-03-03 12:50:06 -07001007 'Cq-Include-Trybots',
Mike Frysingerbb34a222019-07-31 14:40:46 -04001008 }
Jack Neus8edbf642019-07-10 16:08:31 -06001009
1010 # Ignore tags, which could reasonably contain OEM names
1011 # (e.g. Reviewed-by: foo@oem.corp-partner.google.com).
Jack Neus8edbf642019-07-10 16:08:31 -06001012 commit_message = ' '.join(
Mike Frysingerbb34a222019-07-31 14:40:46 -04001013 x for x in _get_commit_desc(commit).splitlines()
1014 if ':' not in x or x.split(':', 1)[0] not in TAGS)
1015
Jack Neus8edbf642019-07-10 16:08:31 -06001016 commit_message = re.sub(r'[\s_-]+', ' ', commit_message)
1017
1018 # Exercise caution when expanding these lists. Adding a name
1019 # could indicate a new relationship with a company!
1020 OEMS = ['hp', 'hewlett packard', 'dell', 'lenovo', 'acer', 'asus', 'samsung']
1021 ODMS = [
1022 'bitland', 'compal', 'haier', 'huaqin', 'inventec', 'lg', 'pegatron',
1023 'pegatron(ems)', 'quanta', 'samsung', 'wistron'
1024 ]
1025
1026 for name_type, name_list in [('OEM', OEMS), ('ODM', ODMS)]:
1027 # Construct regex
1028 name_re = r'\b(%s)\b' % '|'.join([re.escape(x) for x in name_list])
1029 matches = [x[0] for x in re.findall(name_re, commit_message, re.IGNORECASE)]
Mike Frysingere52b1bc2019-09-16 23:45:41 -04001030 if matches:
Jack Neus8edbf642019-07-10 16:08:31 -06001031 # If there's a match, throw an error.
1032 error_msg = ('Changelist description contains the name of an'
1033 ' %s: "%s".' % (name_type, '","'.join(matches)))
1034 return HookFailure(error_msg)
1035
Mike Frysinger8cf80812019-09-16 23:49:29 -04001036 return None
1037
Jack Neus8edbf642019-07-10 16:08:31 -06001038
Mike Frysinger292b45d2014-11-25 01:17:10 -05001039def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -07001040 """Check that we're not missing a revbump of an ebuild in the given commit.
1041
1042 If the given commit touches files in a directory that has ebuilds somewhere
1043 up the directory hierarchy, it's very likely that we need an ebuild revbump
1044 in order for those changes to take effect.
1045
1046 It's not totally trivial to detect a revbump, so at least detect that an
1047 ebuild with a revision number in it was touched. This should handle the
1048 common case where we use a symlink to do the revbump.
1049
1050 TODO: it would be nice to enhance this hook to:
1051 * Handle cases where people revbump with a slightly different syntax. I see
1052 one ebuild (puppy) that revbumps with _pN. This is a false positive.
1053 * Catches cases where people aren't using symlinks for revbumps. If they
1054 edit a revisioned file directly (and are expected to rename it for revbump)
1055 we'll miss that. Perhaps we could detect that the file touched is a
1056 symlink?
1057
1058 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
1059 still better off than without this check.
1060
1061 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001062 project: The Project to look at
Doug Anderson42b8a052013-06-26 10:45:36 -07001063 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -05001064 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -07001065
1066 Returns:
1067 A HookFailure or None.
1068 """
Mike Frysinger011af942014-01-17 16:12:22 -05001069 # If this is the portage-stable overlay, then ignore the check. It's rare
1070 # that we're doing anything other than importing files from upstream, so
1071 # forcing a rev bump makes no sense.
Bob Haarman0dc1f942020-10-03 00:06:59 +00001072 allowlist = (
Mike Frysinger011af942014-01-17 16:12:22 -05001073 'chromiumos/overlays/portage-stable',
1074 )
Bob Haarman0dc1f942020-10-03 00:06:59 +00001075 if project.name in allowlist:
Mike Frysinger011af942014-01-17 16:12:22 -05001076 return None
1077
Mike Frysinger292b45d2014-11-25 01:17:10 -05001078 def FinalName(obj):
1079 # If the file is being deleted, then the dst_file is not set.
1080 if obj.dst_file is None:
1081 return obj.src_file
1082 else:
1083 return obj.dst_file
1084
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001085 def AllowedPath(obj):
Mike Frysinger78dbc242020-11-27 16:46:39 -05001086 allowed_files = {
1087 'ChangeLog', 'Manifest', 'metadata.xml', 'OWNERS', 'README.md',
1088 }
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001089 allowed_directories = {'profiles'}
1090
1091 affected = pathlib.Path(FinalName(obj))
1092 if affected.name in allowed_files:
1093 return True
1094
1095 for directory in allowed_directories:
1096 if directory in affected.parts:
1097 return True
1098
1099 return False
1100
Mike Frysinger292b45d2014-11-25 01:17:10 -05001101 affected_path_objs = _get_affected_files(
1102 commit, include_deletes=True, include_symlinks=True, relative=True,
1103 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -07001104
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001105 # Don't yell about changes to allowed files or directories...
Mike Frysinger292b45d2014-11-25 01:17:10 -05001106 affected_path_objs = [x for x in affected_path_objs
Stephen Boyd6bf5ea82020-10-15 00:02:07 -07001107 if not AllowedPath(x)]
Mike Frysinger292b45d2014-11-25 01:17:10 -05001108 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -07001109 return None
1110
1111 # If we've touched any file named with a -rN.ebuild then we'll say we're
1112 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -05001113 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
1114 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -07001115 if touched_revved_ebuild:
1116 return None
1117
Mike Frysinger292b45d2014-11-25 01:17:10 -05001118 # If we're creating new ebuilds from scratch, then we don't need an uprev.
1119 # Find all the dirs that new ebuilds and ignore their files/.
1120 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
1121 if FinalName(x).endswith('.ebuild') and x.status == 'A']
1122 affected_path_objs = [obj for obj in affected_path_objs
1123 if not any(FinalName(obj).startswith(x)
1124 for x in ebuild_dirs)]
1125 if not affected_path_objs:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001126 return None
Mike Frysinger292b45d2014-11-25 01:17:10 -05001127
Doug Anderson42b8a052013-06-26 10:45:36 -07001128 # We want to examine the current contents of all directories that are parents
1129 # of files that were touched (up to the top of the project).
1130 #
1131 # ...note: we use the current directory contents even though it may have
1132 # changed since the commit we're looking at. This is just a heuristic after
1133 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -05001134 if project_top is None:
1135 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -07001136 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -05001137 for obj in affected_path_objs:
1138 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -07001139 while os.path.exists(path) and not os.path.samefile(path, project_top):
1140 dirs_to_check.add(path)
1141 path = os.path.dirname(path)
1142
1143 # Look through each directory. If it's got an ebuild in it then we'll
1144 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -08001145 affected_paths = set(os.path.join(project_top, FinalName(x))
1146 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -07001147 for dir_path in dirs_to_check:
1148 contents = os.listdir(dir_path)
1149 ebuilds = [os.path.join(dir_path, path)
1150 for path in contents if path.endswith('.ebuild')]
1151 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
1152
C Shapiroae157ae2017-09-18 16:24:03 -06001153 affected_paths_under_9999_ebuilds = set()
1154 for affected_path in affected_paths:
1155 for ebuild_9999 in ebuilds_9999:
1156 ebuild_dir = os.path.dirname(ebuild_9999)
1157 if affected_path.startswith(ebuild_dir):
1158 affected_paths_under_9999_ebuilds.add(affected_path)
1159
1160 # If every file changed exists under a 9999 ebuild, then skip
1161 if len(affected_paths_under_9999_ebuilds) == len(affected_paths):
1162 continue
1163
Doug Anderson42b8a052013-06-26 10:45:36 -07001164 # If the -9999.ebuild file was touched the bot will uprev for us.
1165 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -05001166 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -07001167 continue
1168
1169 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -05001170 return HookFailure('Changelist probably needs a revbump of an ebuild, '
1171 'or a -r1.ebuild symlink if this is a new ebuild:\n'
1172 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -07001173
1174 return None
1175
1176
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001177def _check_ebuild_eapi(project, commit):
Mike Frysinger948284a2018-02-01 15:22:56 -05001178 """Make sure we have people use EAPI=5 or newer with custom ebuilds.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001179
1180 We want to get away from older EAPI's as it makes life confusing and they
1181 have less builtin error checking.
1182
1183 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001184 project: The Project to look at
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001185 commit: The commit to look at
1186
1187 Returns:
1188 A HookFailure or None.
1189 """
1190 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -05001191 # that we're doing anything other than importing files from upstream, and
1192 # we shouldn't be rewriting things fundamentally anyways.
Bob Haarman0dc1f942020-10-03 00:06:59 +00001193 allowlist = (
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001194 'chromiumos/overlays/portage-stable',
1195 )
Bob Haarman0dc1f942020-10-03 00:06:59 +00001196 if project.name in allowlist:
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001197 return None
1198
Mike Frysinger948284a2018-02-01 15:22:56 -05001199 BAD_EAPIS = ('0', '1', '2', '3', '4')
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001200
1201 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
1202
1203 ebuilds_re = [r'\.ebuild$']
1204 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1205 ebuilds_re)
1206 bad_ebuilds = []
1207
1208 for ebuild in ebuilds:
1209 # If the ebuild does not specify an EAPI, it defaults to 0.
1210 eapi = '0'
1211
1212 lines = _get_file_content(ebuild, commit).splitlines()
1213 if len(lines) == 1:
1214 # This is most likely a symlink, so skip it entirely.
1215 continue
1216
1217 for line in lines:
1218 m = get_eapi.match(line)
1219 if m:
1220 # Once we hit the first EAPI line in this ebuild, stop processing.
1221 # The spec requires that there only be one and it be first, so
1222 # checking all possible values is pointless. We also assume that
1223 # it's "the" EAPI line and not something in the middle of a heredoc.
1224 eapi = m.group(1)
1225 break
1226
1227 if eapi in BAD_EAPIS:
1228 bad_ebuilds.append((ebuild, eapi))
1229
1230 if bad_ebuilds:
1231 # pylint: disable=C0301
Mike Frysingerc6114f42020-05-15 00:55:05 -04001232 url = 'https://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001233 # pylint: enable=C0301
1234 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -05001235 'These ebuilds are using old EAPIs. If these are imported from\n'
1236 'Gentoo, then you may ignore and upload once with the --no-verify\n'
Mike Frysingerc6114f42020-05-15 00:55:05 -04001237 'flag. Otherwise, please update to 7 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001238 '\t%s\n'
1239 'See this guide for more details:\n%s\n' %
1240 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
1241
Mike Frysinger8cf80812019-09-16 23:49:29 -04001242 return None
1243
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001244
Mike Frysinger89bdb852014-02-01 05:26:26 -05001245def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -05001246 """Make sure we use the new style KEYWORDS when possible in ebuilds.
1247
1248 If an ebuild generally does not care about the arch it is running on, then
1249 ebuilds should flag it with one of:
1250 KEYWORDS="*" # A stable ebuild.
1251 KEYWORDS="~*" # An unstable ebuild.
1252 KEYWORDS="-* ..." # Is known to only work on specific arches.
1253
1254 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001255 project: The Project to look at
Mike Frysingerc51ece72014-01-17 16:23:40 -05001256 commit: The commit to look at
1257
1258 Returns:
1259 A HookFailure or None.
1260 """
Bob Haarman0dc1f942020-10-03 00:06:59 +00001261 ALLOWLIST = set(('*', '-*', '~*'))
Mike Frysingerc51ece72014-01-17 16:23:40 -05001262
1263 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
1264
Mike Frysinger89bdb852014-02-01 05:26:26 -05001265 ebuilds_re = [r'\.ebuild$']
1266 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1267 ebuilds_re)
1268
Mike Frysinger8d42d742014-09-22 15:50:21 -04001269 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -05001270 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -04001271 # We get the full content rather than a diff as the latter does not work
1272 # on new files (like when adding new ebuilds).
1273 lines = _get_file_content(ebuild, commit).splitlines()
1274 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -05001275 m = get_keywords.match(line)
1276 if m:
1277 keywords = set(m.group(1).split())
Bob Haarman0dc1f942020-10-03 00:06:59 +00001278 if not keywords or ALLOWLIST - keywords != ALLOWLIST:
Mike Frysingerc51ece72014-01-17 16:23:40 -05001279 continue
1280
Mike Frysinger8d42d742014-09-22 15:50:21 -04001281 bad_ebuilds.append(ebuild)
1282
1283 if bad_ebuilds:
1284 return HookFailure(
1285 '%s\n'
1286 'Please update KEYWORDS to use a glob:\n'
1287 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
1288 ' KEYWORDS="*"\n'
1289 'If the ebuild should be marked unstable (normal for '
1290 'cros-workon / 9999 ebuilds):\n'
1291 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -04001292 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -04001293 'then use -* like so:\n'
1294 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -05001295
Mike Frysinger8cf80812019-09-16 23:49:29 -04001296 return None
1297
Mike Frysingerc51ece72014-01-17 16:23:40 -05001298
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001299def _check_ebuild_licenses(_project, commit):
1300 """Check if the LICENSE field in the ebuild is correct."""
Brian Norris7a610e82016-02-17 12:24:54 -08001301 affected_paths = _get_affected_files(commit, relative=True)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001302 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
1303
1304 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -08001305 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001306
1307 for ebuild in touched_ebuilds:
Alex Kleinb5953522018-08-03 11:44:21 -06001308 # e.g. path/to/overlay/category/package/package.ebuild -> path/to/overlay
1309 overlay_path = os.sep.join(ebuild.split(os.sep)[:-3])
1310
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001311 try:
Brian Norris7a610e82016-02-17 12:24:54 -08001312 ebuild_content = _get_file_content(ebuild, commit)
Alex Kleinb5953522018-08-03 11:44:21 -06001313 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
1314 overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001315 except ValueError as e:
Mike Frysingerf1ee2bf2019-09-16 23:47:33 -04001316 return HookFailure(str(e), [ebuild])
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001317
Sergey Frolovc1bd8782021-01-20 19:35:44 -07001318 # Virtual packages must use "metapackage" license.
1319 if ebuild.split('/')[-3] == 'virtual':
1320 if license_types != ['metapackage']:
1321 return HookFailure('Virtual package must use LICENSE="metapackage".',
1322 [ebuild])
1323
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001324 # Also ignore licenses ending with '?'
1325 for license_type in [x for x in license_types
1326 if x not in LICENSES_IGNORE and not x.endswith('?')]:
1327 try:
Alex Kleinb5953522018-08-03 11:44:21 -06001328 licenses_lib.Licensing.FindLicenseType(license_type,
1329 overlay_path=overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001330 except AssertionError as e:
Mike Frysingerf1ee2bf2019-09-16 23:47:33 -04001331 return HookFailure(str(e), [ebuild])
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001332
Mike Frysinger8cf80812019-09-16 23:49:29 -04001333 return None
1334
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001335
Mike Frysingerd9ed85f2021-02-03 12:38:24 -05001336def _check_ebuild_owners(project, commit):
Mike Frysingerb04778f2020-11-30 02:41:14 -05001337 """Require all new packages include an OWNERS file."""
1338 # Look for all adds/removes since we're going to ignore changes that only
1339 # update a package. We only want to flag new package imports for now.
1340 affected_files_objs = _get_affected_files(
1341 commit, include_deletes=True, include_symlinks=True, relative=True,
1342 full_details=True)
1343
1344 # If this CL doesn't include any ebuilds, don't bother complaining.
1345 new_ebuilds = [x for x in affected_files_objs
1346 if x.status == 'A' and x.src_file.endswith('.ebuild')]
1347 if not new_ebuilds:
1348 return None
1349
1350 # Check each package dir.
1351 packages_missing_owners = []
1352 package_dirs = sorted(set(os.path.dirname(x.src_file) for x in new_ebuilds))
1353 for package_dir in package_dirs:
1354 package_files = [
1355 x for x in affected_files_objs
1356 if (x.src_file and x.src_file.startswith(f'{package_dir}/')) or
1357 (x.dst_file and x.dst_file.startswith(f'{package_dir}/'))]
1358
1359 # Only complain about new ebuilds, not existing ones. For now.
1360 # We'll assume that "all adds" means it's a new package.
1361 if any(x for x in package_files if x.status != 'A'):
1362 continue
1363
1364 # See if there's an OWNERS file in there already.
Mike Frysingerd9ed85f2021-02-03 12:38:24 -05001365 data = _get_file_content(os.path.join(package_dir, 'OWNERS'), commit)
1366 if not data:
1367 # Allow specific overlays to declare OWNERS for all packages.
1368 if (project.name == 'chromiumos/overlays/board-overlays' or
1369 re.match(r'^chromeos/overlays/(baseboard|chipset|project|overlay)-',
1370 project.name)):
1371 data = _get_file_content(os.path.join(os.path.dirname(os.path.dirname(
1372 package_dir)), 'OWNERS'), commit)
1373
1374 if not data:
1375 packages_missing_owners.append(package_dir)
1376 continue
1377
1378 # Require specific people and not just *.
1379 lines = {x for x in data.splitlines() if x.split('#', 1)[0].strip()}
1380 if not lines - {'*'}:
Mike Frysingerb04778f2020-11-30 02:41:14 -05001381 packages_missing_owners.append(package_dir)
1382
1383 if packages_missing_owners:
1384 return HookFailure(
Mike Frysingerd9ed85f2021-02-03 12:38:24 -05001385 'All new packages must have an OWNERS file filled out.',
Mike Frysingerb04778f2020-11-30 02:41:14 -05001386 packages_missing_owners)
1387
1388 return None
1389
1390
Mike Frysinger6ee76b82020-11-20 01:16:06 -05001391def _check_ebuild_r0(_project, commit):
1392 """Do not allow ebuilds to end with -r0 versions."""
1393 ebuilds = _filter_files(
1394 _get_affected_files(commit, include_symlinks=True, relative=True),
1395 (r'-r0\.ebuild$',))
1396 if ebuilds:
1397 return HookFailure(
1398 'The -r0 in ebuilds is redundant and confusing. Simply remove it.\n'
1399 'For example: git mv foo-1.0-r0.ebuild foo-1.0.ebuild',
1400 ebuilds)
1401
1402 return None
1403
1404
Mike Frysingercd363c82014-02-01 05:20:18 -05001405def _check_ebuild_virtual_pv(project, commit):
1406 """Enforce the virtual PV policies."""
1407 # If this is the portage-stable overlay, then ignore the check.
1408 # We want to import virtuals as-is from upstream Gentoo.
Bob Haarman0dc1f942020-10-03 00:06:59 +00001409 allowlist = (
Mike Frysingercd363c82014-02-01 05:20:18 -05001410 'chromiumos/overlays/portage-stable',
1411 )
Bob Haarman0dc1f942020-10-03 00:06:59 +00001412 if project.name in allowlist:
Mike Frysingercd363c82014-02-01 05:20:18 -05001413 return None
1414
1415 # We assume the repo name is the same as the dir name on disk.
1416 # It would be dumb to not have them match though.
Alex Deymo643ac4c2015-09-03 10:40:50 -07001417 project_base = os.path.basename(project.name)
Mike Frysingercd363c82014-02-01 05:20:18 -05001418
1419 is_variant = lambda x: x.startswith('overlay-variant-')
1420 is_board = lambda x: x.startswith('overlay-')
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001421 is_baseboard = lambda x: x.startswith('baseboard-')
1422 is_chipset = lambda x: x.startswith('chipset-')
1423 is_project = lambda x: x.startswith('project-')
Mike Frysingercd363c82014-02-01 05:20:18 -05001424 is_private = lambda x: x.endswith('-private')
1425
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001426 is_special_overlay = lambda x: (is_board(x) or is_chipset(x) or
1427 is_baseboard(x) or is_project(x))
1428
Mike Frysingercd363c82014-02-01 05:20:18 -05001429 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
1430
1431 ebuilds_re = [r'\.ebuild$']
1432 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1433 ebuilds_re)
1434 bad_ebuilds = []
1435
1436 for ebuild in ebuilds:
1437 m = get_pv.match(ebuild)
1438 if m:
1439 overlay = m.group(1)
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001440 if not overlay or not is_special_overlay(overlay):
Alex Deymo643ac4c2015-09-03 10:40:50 -07001441 overlay = project_base
Mike Frysingercd363c82014-02-01 05:20:18 -05001442
1443 pv = m.group(3).split('-', 1)[0]
1444
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001445 # Virtual versions >= 4 are special cases used above the standard
1446 # versioning structure, e.g. if one has a board inheriting a board.
Julius Werner0299a2b2020-07-31 18:20:43 -07001447 if pv[0] >= '4':
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001448 want_pv = pv
Mike Frysingercd363c82014-02-01 05:20:18 -05001449 elif is_board(overlay):
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001450 if is_private(overlay):
1451 want_pv = '3.5' if is_variant(overlay) else '3'
1452 elif is_board(overlay):
1453 want_pv = '2.5' if is_variant(overlay) else '2'
1454 elif is_baseboard(overlay):
Julius Werner0299a2b2020-07-31 18:20:43 -07001455 want_pv = '1.9.5' if is_private(overlay) else '1.9'
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001456 elif is_chipset(overlay):
Julius Werner0299a2b2020-07-31 18:20:43 -07001457 want_pv = '1.8.5' if is_private(overlay) else '1.8'
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001458 elif is_project(overlay):
1459 want_pv = '1.7' if is_private(overlay) else '1.5'
Mike Frysingercd363c82014-02-01 05:20:18 -05001460 else:
1461 want_pv = '1'
1462
1463 if pv != want_pv:
1464 bad_ebuilds.append((ebuild, pv, want_pv))
1465
1466 if bad_ebuilds:
1467 # pylint: disable=C0301
1468 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
1469 # pylint: enable=C0301
1470 return HookFailure(
1471 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
1472 '\t%s\n'
1473 'If this is an upstream Gentoo virtual, then you may ignore this\n'
1474 'check (and re-run w/--no-verify). Otherwise, please see this\n'
1475 'page for more details:\n%s\n' %
1476 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
1477 for x in bad_ebuilds]), url))
1478
Mike Frysinger8cf80812019-09-16 23:49:29 -04001479 return None
1480
Mike Frysingercd363c82014-02-01 05:20:18 -05001481
Daniel Erat9d203ff2015-02-17 10:12:21 -07001482def _check_portage_make_use_var(_project, commit):
1483 """Verify that $USE is set correctly in make.conf and make.defaults."""
1484 files = _filter_files(_get_affected_files(commit, relative=True),
1485 [r'(^|/)make.(conf|defaults)$'])
1486
1487 errors = []
1488 for path in files:
1489 basename = os.path.basename(path)
1490
1491 # Has a USE= line already been encountered in this file?
1492 saw_use = False
1493
1494 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
1495 if not line.startswith('USE='):
1496 continue
1497
1498 preserves_use = '${USE}' in line or '$USE' in line
1499
1500 if (basename == 'make.conf' or
1501 (basename == 'make.defaults' and saw_use)) and not preserves_use:
1502 errors.append('%s:%d: missing ${USE}' % (path, i))
1503 elif basename == 'make.defaults' and not saw_use and preserves_use:
1504 errors.append('%s:%d: ${USE} referenced in initial declaration' %
1505 (path, i))
1506
1507 saw_use = True
1508
1509 if errors:
1510 return HookFailure(
1511 'One or more Portage make files appear to set USE incorrectly.\n'
1512 '\n'
1513 'All USE assignments in make.conf and all assignments after the\n'
1514 'initial declaration in make.defaults should contain "${USE}" to\n'
1515 'preserve previously-set flags.\n'
1516 '\n'
1517 'The initial USE declaration in make.defaults should not contain\n'
1518 '"${USE}".\n',
1519 errors)
1520
Mike Frysinger8cf80812019-09-16 23:49:29 -04001521 return None
1522
Daniel Erat9d203ff2015-02-17 10:12:21 -07001523
Mike Frysingerae409522014-02-01 03:16:11 -05001524def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001525 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001526 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001527 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001528 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001529 if not m:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001530 return HookFailure('Last paragraph of description must include Change-Id.')
Ryan Cui1562fb82011-05-09 11:01:31 -07001531
LaMont Jonesfb5e8bf2020-03-03 12:50:06 -07001532 # S-o-b and some other tags always allowed to follow Change-ID in the footer.
1533 allowed_tags = ['Signed-off-by', 'Cq-Cl-Tag', 'Cq-Include-Trybots']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001534
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001535 end = desc[m.end():].strip().splitlines()
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001536 cherry_pick_marker = 'cherry picked from commit'
1537
1538 if end and cherry_pick_marker in end[-1]:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001539 # Cherry picked patches allow more tags in the last paragraph.
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001540 allowed_tags += ['Commit-Queue', 'Commit-Ready', 'Reviewed-by',
1541 'Reviewed-on', 'Tested-by']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001542 end = end[:-1]
1543
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001544 # Note that descriptions could have multiple cherry pick markers.
1545 tag_search = r'^(%s:|\(%s) ' % (':|'.join(allowed_tags), cherry_pick_marker)
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001546
1547 if [x for x in end if not re.search(tag_search, x)]:
1548 return HookFailure('Only "%s:" tag(s) may follow the Change-Id.' %
1549 ':", "'.join(allowed_tags))
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001550
Mike Frysinger8cf80812019-09-16 23:49:29 -04001551 return None
1552
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001553
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001554def _check_commit_message_style(_project, commit):
1555 """Verify that the commit message matches our style.
1556
1557 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
1558 commit hooks.
1559 """
Mike Frysingered9b2a02019-12-12 18:52:32 -05001560 SEE_ALSO = 'Please review the documentation:\n%s' % (DOC_COMMIT_MSG_URL,)
Mike Frysinger4efdee72019-11-04 10:57:01 -05001561
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001562 desc = _get_commit_desc(commit)
1563
1564 # The first line should be by itself.
1565 lines = desc.splitlines()
1566 if len(lines) > 1 and lines[1]:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001567 return HookFailure('The second line of the commit message must be blank.'
1568 '\n%s' % (SEE_ALSO,))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001569
1570 # The first line should be one sentence.
1571 if '. ' in lines[0]:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001572 return HookFailure('The first line cannot be more than one sentence.\n%s' %
1573 (SEE_ALSO,))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001574
1575 # The first line cannot be too long.
1576 MAX_FIRST_LINE_LEN = 100
Mike Frysingerd2451822021-02-17 13:52:19 -05001577 first_line = lines[0]
1578 if len(first_line) > MAX_FIRST_LINE_LEN:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001579 return HookFailure('The first line must be less than %i chars.\n%s' %
1580 (MAX_FIRST_LINE_LEN, SEE_ALSO))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001581
Mike Frysingerd2451822021-02-17 13:52:19 -05001582 # Don't allow random git keywords.
1583 if first_line.startswith('fixup!') or first_line.startswith('squash!'):
1584 return HookFailure(
1585 'Git fixup/squash commit detected: rebase your local branch, or '
1586 'cleanup the commit message')
1587
Mike Frysinger8cf80812019-09-16 23:49:29 -04001588 return None
1589
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001590
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001591def _check_cros_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001592 """Verifies the Chromium OS license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -07001593
Mike Frysinger98638102014-08-28 00:15:08 -04001594 Should be following the spec:
1595 http://dev.chromium.org/developers/coding-style#TOC-File-headers
1596 """
1597 # For older years, be a bit more flexible as our policy says leave them be.
1598 LICENSE_HEADER = (
Keigo Oka7e880ac2019-07-03 15:03:43 +09001599 r'.*Copyright(?: \(c\))? (20[0-9]{2})(?:-20[0-9]{2})? The Chromium OS '
1600 r'Authors\. All rights reserved\.\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001601 r'.*Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001602 r'be\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001603 r'.*found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -05001604 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001605 )
1606 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1607
1608 # For newer years, be stricter.
Keigo Oka7e880ac2019-07-03 15:03:43 +09001609 BAD_COPYRIGHT_LINE = (
Brian Norris68838dd2018-09-26 18:30:24 -07001610 r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001611 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001612 )
Keigo Oka7e880ac2019-07-03 15:03:43 +09001613 bad_copyright_re = re.compile(BAD_COPYRIGHT_LINE)
Mike Frysinger98638102014-08-28 00:15:08 -04001614
Shuhei Takahashiabc20f32017-07-10 19:35:45 +09001615 included, excluded = _parse_common_inclusion_options(options)
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001616
Mike Frysinger98638102014-08-28 00:15:08 -04001617 bad_files = []
1618 bad_copyright_files = []
Keigo Oka7e880ac2019-07-03 15:03:43 +09001619 bad_year_files = []
1620
Ken Turnerd07564b2018-02-08 17:57:59 +11001621 files = _filter_files(
1622 _get_affected_files(commit, relative=True),
1623 included + COMMON_INCLUDED_PATHS,
1624 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001625 existing_files = set(_get_affected_files(commit, relative=True,
1626 include_adds=False))
Mike Frysinger98638102014-08-28 00:15:08 -04001627
Keigo Oka7e880ac2019-07-03 15:03:43 +09001628 current_year = str(datetime.datetime.now().year)
Mike Frysinger98638102014-08-28 00:15:08 -04001629 for f in files:
1630 contents = _get_file_content(f, commit)
1631 if not contents:
1632 # Ignore empty files.
1633 continue
1634
Keigo Oka7e880ac2019-07-03 15:03:43 +09001635 m = license_re.search(contents)
1636 if not m:
Mike Frysinger98638102014-08-28 00:15:08 -04001637 bad_files.append(f)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001638 elif bad_copyright_re.search(contents):
Mike Frysinger98638102014-08-28 00:15:08 -04001639 bad_copyright_files.append(f)
1640
Keigo Oka7e880ac2019-07-03 15:03:43 +09001641 if m and f not in existing_files:
1642 year = m.group(1)
1643 if year != current_year:
1644 bad_year_files.append(f)
1645
1646 errors = []
Mike Frysinger98638102014-08-28 00:15:08 -04001647 if bad_files:
1648 msg = '%s:\n%s\n%s' % (
1649 'License must match', license_re.pattern,
1650 'Found a bad header in these files:')
Keigo Oka7e880ac2019-07-03 15:03:43 +09001651 errors.append(HookFailure(msg, bad_files))
Mike Frysinger98638102014-08-28 00:15:08 -04001652 if bad_copyright_files:
1653 msg = 'Do not use (c) in copyright headers in new files:'
Keigo Oka7e880ac2019-07-03 15:03:43 +09001654 errors.append(HookFailure(msg, bad_copyright_files))
1655 if bad_year_files:
1656 msg = 'Use current year (%s) in copyright headers in new files:' % (
1657 current_year)
1658 errors.append(HookFailure(msg, bad_year_files))
Ryan Cuiec4d6332011-05-02 14:15:25 -07001659
Keigo Oka7e880ac2019-07-03 15:03:43 +09001660 return errors
Ryan Cuiec4d6332011-05-02 14:15:25 -07001661
Mike Frysinger8cf80812019-09-16 23:49:29 -04001662
Amin Hassani391efa92018-01-26 17:58:05 -08001663def _check_aosp_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001664 """Verifies the AOSP license/copyright header.
1665
1666 AOSP uses the Apache2 License:
1667 https://source.android.com/source/licenses.html
1668 """
1669 LICENSE_HEADER = (
1670 r"""^[#/\*]*
1671[#/\*]* ?Copyright( \([cC]\))? 20[-0-9]{2,7} The Android Open Source Project
1672[#/\*]* ?
1673[#/\*]* ?Licensed under the Apache License, Version 2.0 \(the "License"\);
1674[#/\*]* ?you may not use this file except in compliance with the License\.
1675[#/\*]* ?You may obtain a copy of the License at
1676[#/\*]* ?
1677[#/\*]* ? http://www\.apache\.org/licenses/LICENSE-2\.0
1678[#/\*]* ?
1679[#/\*]* ?Unless required by applicable law or agreed to in writing, software
1680[#/\*]* ?distributed under the License is distributed on an "AS IS" BASIS,
1681[#/\*]* ?WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or """
1682 r"""implied\.
1683[#/\*]* ?See the License for the specific language governing permissions and
1684[#/\*]* ?limitations under the License\.
1685[#/\*]*$
1686"""
1687 )
1688 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1689
Amin Hassani391efa92018-01-26 17:58:05 -08001690 included, excluded = _parse_common_inclusion_options(options)
1691
Ken Turnerd07564b2018-02-08 17:57:59 +11001692 files = _filter_files(
1693 _get_affected_files(commit, relative=True),
1694 included + COMMON_INCLUDED_PATHS,
1695 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Alex Deymof5792ce2015-08-24 22:50:08 -07001696
1697 bad_files = []
1698 for f in files:
1699 contents = _get_file_content(f, commit)
1700 if not contents:
1701 # Ignore empty files.
1702 continue
1703
1704 if not license_re.search(contents):
1705 bad_files.append(f)
1706
1707 if bad_files:
1708 msg = ('License must match:\n%s\nFound a bad header in these files:' %
1709 license_re.pattern)
1710 return HookFailure(msg, bad_files)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001711 return None
Alex Deymof5792ce2015-08-24 22:50:08 -07001712
1713
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001714def _check_layout_conf(_project, commit):
1715 """Verifies the metadata/layout.conf file."""
1716 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001717 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001718 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001719 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001720
Mike Frysinger94a670c2014-09-19 12:46:26 -04001721 # Handle multiple overlays in a single commit (like the public tree).
1722 for f in _get_affected_files(commit, relative=True):
1723 if f.endswith(repo_name):
1724 repo_names.append(f)
1725 elif f.endswith(layout_path):
1726 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001727
1728 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -04001729 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001730 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -04001731 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001732
Mike Frysinger94a670c2014-09-19 12:46:26 -04001733 # Gather all the errors in one pass so we show one full message.
1734 all_errors = {}
1735 for layout_path in layout_paths:
1736 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001737
Mike Frysinger94a670c2014-09-19 12:46:26 -04001738 # Make sure the config file is sorted.
1739 data = [x for x in _get_file_content(layout_path, commit).splitlines()
1740 if x and x[0] != '#']
1741 if sorted(data) != data:
1742 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001743
Mike Frysinger94a670c2014-09-19 12:46:26 -04001744 # Require people to set specific values all the time.
1745 settings = (
1746 # TODO: Enable this for everyone. http://crbug.com/408038
Shuhei Takahashi3cbb8dd2019-10-29 12:37:11 +09001747 # ('fast caching', 'cache-format = md5-dict'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001748 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -05001749 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
1750 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001751 )
1752 for reason, line in settings:
1753 if line not in data:
1754 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001755
Mike Frysinger94a670c2014-09-19 12:46:26 -04001756 # Require one of these settings.
Mike Frysinger5fae62d2015-11-11 20:12:15 -05001757 if 'use-manifests = strict' not in data:
1758 errors += ['enable file checking with: use-manifests = strict']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001759
Mike Frysinger94a670c2014-09-19 12:46:26 -04001760 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -04001761 for line in data:
1762 if line.startswith('repo-name = '):
1763 break
1764 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001765 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001766
Mike Frysinger94a670c2014-09-19 12:46:26 -04001767 # Summarize all the errors we saw (if any).
1768 lines = ''
1769 for layout_path, errors in all_errors.items():
1770 if errors:
1771 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1772 if lines:
1773 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1774 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001775
Mike Frysinger8cf80812019-09-16 23:49:29 -04001776 return None
1777
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001778
Keigo Oka4a09bd92019-05-07 14:01:00 +09001779def _check_no_new_gyp(_project, commit):
1780 """Verifies no project starts to use GYP."""
Keigo Oka4a09bd92019-05-07 14:01:00 +09001781 gypfiles = _filter_files(
1782 _get_affected_files(commit, include_symlinks=True, relative=True),
1783 [r'\.gyp$'])
1784
1785 if gypfiles:
1786 return HookFailure('GYP is deprecated and not allowed in a new project:',
1787 gypfiles)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001788 return None
Keigo Oka4a09bd92019-05-07 14:01:00 +09001789
1790
Ryan Cuiec4d6332011-05-02 14:15:25 -07001791# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001792
Ryan Cui1562fb82011-05-09 11:01:31 -07001793
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001794def _check_clang_format(_project, commit, options=()):
1795 """Runs clang-format on the given project"""
1796 hooks_dir = _get_hooks_dir()
1797 options = list(options)
1798 if commit == PRE_SUBMIT:
1799 options.append('--commit=HEAD')
1800 else:
1801 options.extend(['--commit', commit])
Brian Norris0c62a142018-12-11 13:24:29 -08001802 cmd = [os.path.join(hooks_dir, 'clang-format.py')] + options
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001803 cmd_result = cros_build_lib.run(cmd,
1804 print_cmd=False,
1805 stdout=True,
1806 encoding='utf-8',
1807 errors='replace',
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05001808 stderr=subprocess.STDOUT,
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001809 check=False)
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001810 if cmd_result.returncode:
1811 return HookFailure('clang-format.py errors/warnings\n\n' +
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001812 cmd_result.stdout)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001813 return None
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001814
1815
Mike Frysingerae409522014-02-01 03:16:11 -05001816def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001817 """Runs checkpatch.pl on the given project"""
Tomasz Figabd8faf62020-09-22 14:06:05 +00001818 # Bypass checkpatch for upstream or almost upstream commits, since we do not
1819 # intend to modify the upstream commits when landing them to our branches.
1820 # Any fixes should sent as independent patches.
1821 # The check is retained for FROMLIST and BACKPORT commits, as by definition
1822 # those can be still fixed up.
1823 desc = _get_commit_desc(commit)
1824 if desc.startswith('UPSTREAM:') or desc.startswith('FROMGIT:'):
1825 return None
1826
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001827 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001828 options = list(options)
Brian Norris4cee0502020-06-12 17:02:56 -07001829 if options and options[0].startswith('./') and os.path.exists(options[0]):
1830 cmdpath = options.pop(0)
1831 else:
1832 cmdpath = os.path.join(hooks_dir, 'checkpatch.pl')
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001833 if commit == PRE_SUBMIT:
1834 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1835 # this case.
1836 options.append('--ignore=MISSING_SIGN_OFF')
Filipe Brandenburger28d48d62015-10-07 09:48:54 -07001837 # Always ignore the check for the MAINTAINERS file. We do not track that
1838 # information on that file in our source trees, so let's suppress the
1839 # warning.
1840 options.append('--ignore=FILE_PATH_CHANGES')
Filipe Brandenburgerce978322015-10-09 10:04:15 -07001841 # Do not complain about the Change-Id: fields, since we use Gerrit.
1842 # Upstream does not want those lines (since they do not use Gerrit), but
1843 # we always do, so disable the check globally.
Daisuke Nojiri4844f892015-10-08 09:54:33 -07001844 options.append('--ignore=GERRIT_CHANGE_ID')
Brian Norris4cee0502020-06-12 17:02:56 -07001845 cmd = [cmdpath] + options + ['-']
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001846 cmd_result = cros_build_lib.run(
1847 cmd, print_cmd=False, input=_get_patch(commit).encode('utf-8'),
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05001848 stdout=True, stderr=subprocess.STDOUT, check=False, encoding='utf-8',
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001849 errors='replace')
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001850 if cmd_result.returncode:
Brian Norris4cee0502020-06-12 17:02:56 -07001851 return HookFailure('%s errors/warnings\n\n%s' %
1852 (cmdpath, cmd_result.stdout))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001853 return None
Ryan Cuiec4d6332011-05-02 14:15:25 -07001854
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001855
Brian Norris23c62e92018-11-14 12:25:51 -08001856def _run_kerneldoc(_project, commit, options=()):
1857 """Runs kernel-doc validator on the given project"""
1858 included, excluded = _parse_common_inclusion_options(options)
1859 files = _filter_files(_get_affected_files(commit, relative=True),
1860 included, excluded)
1861 if files:
1862 hooks_dir = _get_hooks_dir()
Brian Norris0c62a142018-12-11 13:24:29 -08001863 cmd = [os.path.join(hooks_dir, 'kernel-doc'), '-none'] + files
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05001864 output = _run_command(cmd, stderr=subprocess.STDOUT)
Brian Norris23c62e92018-11-14 12:25:51 -08001865 if output:
Brian Norris0c62a142018-12-11 13:24:29 -08001866 return HookFailure('kernel-doc errors/warnings:',
1867 items=output.splitlines())
Mike Frysinger8cf80812019-09-16 23:49:29 -04001868 return None
Brian Norris23c62e92018-11-14 12:25:51 -08001869
1870
Mike Frysingerae409522014-02-01 03:16:11 -05001871def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001872 """Makes sure kernel config changes are not mixed with code changes"""
1873 files = _get_affected_files(commit)
Mike Frysingerf252cfc2019-12-09 17:34:28 -05001874 if len(_filter_files(files, [r'chromeos/config'])) not in [0, len(files)]:
Olof Johanssona96810f2012-09-04 16:20:03 -07001875 return HookFailure('Changes to chromeos/config/ and regular files must '
1876 'be in separate commits:\n%s' % '\n'.join(files))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001877 return None
Anton Staaf815d6852011-08-22 10:08:45 -07001878
Mike Frysingerae409522014-02-01 03:16:11 -05001879
1880def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001881 """Checks that all JSON files are syntactically valid."""
Mike Frysinger908be682018-01-04 02:21:50 -05001882 ret = []
1883
1884 files = _filter_files(_get_affected_files(commit, relative=True),
1885 [r'.*\.json$'])
1886 for f in files:
1887 data = _get_file_content(f, commit)
Dale Curtis2975c432011-05-03 17:25:20 -07001888 try:
Mike Frysinger908be682018-01-04 02:21:50 -05001889 json.loads(data)
1890 except Exception as e:
1891 ret.append('%s: Invalid JSON: %s' % (f, e))
1892
1893 if ret:
1894 return HookFailure('\n'.join(ret))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001895 return None
Dale Curtis2975c432011-05-03 17:25:20 -07001896
1897
Mike Frysingerae409522014-02-01 03:16:11 -05001898def _check_manifests(_project, commit):
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001899 """Make sure Manifest files only have comments & DIST lines."""
1900 ret = []
Mike Frysinger52b537e2013-08-22 22:59:53 -04001901
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001902 manifests = _filter_files(_get_affected_files(commit, relative=True),
1903 [r'.*/Manifest$'])
1904 for path in manifests:
1905 data = _get_file_content(path, commit)
1906
1907 # Disallow blank files.
1908 if not data.strip():
1909 ret.append('%s: delete empty file' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001910 continue
1911
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001912 # Make sure the last newline isn't omitted.
1913 if data[-1] != '\n':
1914 ret.append('%s: missing trailing newline' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001915
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001916 # Do not allow leading or trailing blank lines.
1917 lines = data.splitlines()
1918 if not lines[0]:
1919 ret.append('%s: delete leading blank lines' % (path,))
1920 if not lines[-1]:
1921 ret.append('%s: delete trailing blank lines' % (path,))
1922
1923 for line in lines:
1924 # Disallow leading/trailing whitespace.
1925 if line != line.strip():
1926 ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
1927
1928 # Allow blank lines & comments.
1929 line = line.split('#', 1)[0]
1930 if not line:
1931 continue
1932
1933 # All other linse should start with DIST.
1934 if not line.startswith('DIST '):
1935 ret.append('%s: remove non-DIST lines: %s' % (path, line))
1936 break
1937
1938 if ret:
1939 return HookFailure('\n'.join(ret))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001940 return None
Mike Frysinger52b537e2013-08-22 22:59:53 -04001941
1942
Mike Frysingerae409522014-02-01 03:16:11 -05001943def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001944 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001945 if commit == PRE_SUBMIT:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001946 return None
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001947 BRANCH_RE = r'\nBRANCH=\S+'
1948
1949 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1950 msg = ('Changelist description needs BRANCH field (after first line)\n'
1951 'E.g. BRANCH=none or BRANCH=link,snow')
1952 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001953 return None
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001954
1955
Mike Frysinger45334bd2019-11-04 10:42:33 -05001956def _check_change_has_no_branch_field(_project, commit):
1957 """Verify 'BRANCH=' field does not exist in the commit message."""
1958 if commit == PRE_SUBMIT:
1959 return None
1960 BRANCH_RE = r'\nBRANCH=\S+'
1961
1962 if re.search(BRANCH_RE, _get_commit_desc(commit)):
1963 msg = 'This checkout does not use BRANCH= fields. Delete them.'
1964 return HookFailure(msg)
1965 return None
1966
1967
Mike Frysingerae409522014-02-01 03:16:11 -05001968def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001969 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001970 if commit == PRE_SUBMIT:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001971 return None
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001972 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1973
1974 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1975 msg = ('Changelist description needs Signed-off-by: field\n'
1976 'E.g. Signed-off-by: My Name <me@chromium.org>')
1977 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001978 return None
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001979
1980
Mike Frysinger9ab64b12019-11-04 10:53:08 -05001981def _check_change_has_no_signoff_field(_project, commit):
1982 """Verify 'Signed-off-by:' field does not exist in the commit message."""
1983 if commit == PRE_SUBMIT:
1984 return None
1985 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1986
1987 if re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1988 msg = 'This checkout does not use Signed-off-by: tags. Delete them.'
1989 return HookFailure(msg)
1990 return None
1991
1992
Jon Salz3ee59de2012-08-18 13:54:22 +08001993def _run_project_hook_script(script, project, commit):
1994 """Runs a project hook script.
1995
1996 The script is run with the following environment variables set:
1997 PRESUBMIT_PROJECT: The affected project
1998 PRESUBMIT_COMMIT: The affected commit
1999 PRESUBMIT_FILES: A newline-separated list of affected files
2000
2001 The script is considered to fail if the exit code is non-zero. It should
2002 write an error message to stdout.
2003 """
2004 env = dict(os.environ)
Alex Deymo643ac4c2015-09-03 10:40:50 -07002005 env['PRESUBMIT_PROJECT'] = project.name
Jon Salz3ee59de2012-08-18 13:54:22 +08002006 env['PRESUBMIT_COMMIT'] = commit
2007
2008 # Put affected files in an environment variable
2009 files = _get_affected_files(commit)
2010 env['PRESUBMIT_FILES'] = '\n'.join(files)
2011
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002012 cmd_result = cros_build_lib.run(cmd=script,
2013 env=env,
2014 shell=True,
2015 print_cmd=False,
2016 input=os.devnull,
2017 stdout=True,
2018 encoding='utf-8',
2019 errors='replace',
Mike Frysingerd0c5c8b2019-12-17 02:28:26 -05002020 stderr=subprocess.STDOUT,
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002021 check=False)
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002022 if cmd_result.returncode:
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002023 stdout = cmd_result.stdout
Jon Salz7b618af2012-08-31 06:03:16 +08002024 if stdout:
2025 stdout = re.sub('(?m)^', ' ', stdout)
2026 return HookFailure('Hook script "%s" failed with code %d%s' %
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002027 (script, cmd_result.returncode,
Jon Salz3ee59de2012-08-18 13:54:22 +08002028 ':\n' + stdout if stdout else ''))
Mike Frysinger8cf80812019-09-16 23:49:29 -04002029 return None
Jon Salz3ee59de2012-08-18 13:54:22 +08002030
2031
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002032def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05002033 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002034
Brian Norris77608e12018-04-06 10:38:43 -07002035 files = _get_affected_files(commit, include_deletes=True, relative=True)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002036 prefix = os.path.commonprefix(files)
2037 prefix = os.path.dirname(prefix)
2038
2039 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07002040 if not prefix:
Mike Frysinger8cf80812019-09-16 23:49:29 -04002041 return None
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002042
2043 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07002044
2045 # The common files may all be within a subdirectory of the main project
2046 # directory, so walk up the tree until we find an alias file.
2047 # _get_affected_files() should return relative paths, but check against '/' to
2048 # ensure that this loop terminates even if it receives an absolute path.
2049 while prefix and prefix != '/':
2050 alias_file = os.path.join(prefix, '.project_alias')
2051
2052 # If an alias exists, use it.
2053 if os.path.isfile(alias_file):
2054 project_name = osutils.ReadFile(alias_file).strip()
2055
2056 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002057
2058 if not _get_commit_desc(commit).startswith(project_name + ': '):
2059 return HookFailure('The commit title for changes affecting only %s'
2060 ' should start with \"%s: \"'
2061 % (project_name, project_name))
Mike Frysinger8cf80812019-09-16 23:49:29 -04002062 return None
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07002063
2064
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002065def _check_filepath_chartype(_project, commit):
2066 """Checks that FilePath::CharType stuff is not used."""
2067
2068 FILEPATH_REGEXP = re.compile('|'.join(
2069 [r'(?:base::)?FilePath::(?:Char|String|StringPiece)Type',
Satoru Takabayashi4ca37922018-08-08 10:16:38 +09002070 r'(?:base::)?FilePath::FromUTF8Unsafe',
2071 r'AsUTF8Unsafe',
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002072 r'FILE_PATH_LITERAL']))
2073 files = _filter_files(_get_affected_files(commit, relative=True),
2074 [r'.*\.(cc|h)$'])
2075
2076 errors = []
2077 for afile in files:
2078 for line_num, line in _get_file_diff(afile, commit):
2079 m = re.search(FILEPATH_REGEXP, line)
2080 if m:
2081 errors.append('%s, line %s has %s' % (afile, line_num, m.group(0)))
2082
2083 if errors:
2084 msg = 'Please assume FilePath::CharType is char (crbug.com/870621):'
2085 return HookFailure(msg, errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -04002086 return None
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002087
2088
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002089def _check_exec_files(_project, commit):
2090 """Make +x bits on files."""
2091 # List of files that should never be +x.
2092 NO_EXEC = (
2093 'ChangeLog*',
2094 'COPYING',
2095 'make.conf',
2096 'make.defaults',
2097 'Manifest',
2098 'OWNERS',
2099 'package.use',
2100 'package.keywords',
2101 'package.mask',
2102 'parent',
2103 'README',
2104 'TODO',
2105 '.gitignore',
2106 '*.[achly]',
2107 '*.[ch]xx',
2108 '*.boto',
2109 '*.cc',
2110 '*.cfg',
2111 '*.conf',
2112 '*.config',
2113 '*.cpp',
2114 '*.css',
2115 '*.ebuild',
2116 '*.eclass',
Tatsuhisa Yamaguchi3b053632019-01-10 14:45:14 +09002117 '*.gn',
2118 '*.gni',
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002119 '*.gyp',
2120 '*.gypi',
2121 '*.htm',
2122 '*.html',
2123 '*.ini',
2124 '*.js',
2125 '*.json',
2126 '*.md',
2127 '*.mk',
2128 '*.patch',
2129 '*.policy',
2130 '*.proto',
2131 '*.raw',
2132 '*.rules',
2133 '*.service',
2134 '*.target',
2135 '*.txt',
2136 '*.xml',
2137 '*.yaml',
2138 )
2139
2140 def FinalName(obj):
2141 # If the file is being deleted, then the dst_file is not set.
2142 if obj.dst_file is None:
2143 return obj.src_file
2144 else:
2145 return obj.dst_file
2146
2147 bad_files = []
2148 files = _get_affected_files(commit, relative=True, full_details=True)
2149 for f in files:
2150 mode = int(f.dst_mode, 8)
2151 if not mode & 0o111:
2152 continue
2153 name = FinalName(f)
2154 for no_exec in NO_EXEC:
2155 if fnmatch.fnmatch(name, no_exec):
2156 bad_files.append(name)
2157 break
2158
2159 if bad_files:
2160 return HookFailure('These files should not be executable. '
2161 'Please `chmod -x` them.', bad_files)
Mike Frysinger8cf80812019-09-16 23:49:29 -04002162 return None
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002163
2164
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002165# Base
2166
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002167# A list of hooks which are not project specific and check patch description
2168# (as opposed to patch body).
2169_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07002170 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07002171 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07002172 _check_change_has_test_field,
2173 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04002174 _check_commit_message_style,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08002175 _check_change_is_contribution,
Jack Neus8edbf642019-07-10 16:08:31 -06002176 _check_change_no_include_oem,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002177]
2178
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002179# A list of hooks that are not project-specific
2180_COMMON_HOOKS = [
Keiichi Watanabe6ab73fd2020-07-14 23:42:02 +09002181 _check_cargo_clippy,
Aviv Keshet5ac59522017-01-31 14:28:27 -08002182 _check_cros_license,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05002183 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05002184 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08002185 _check_ebuild_licenses,
Mike Frysingerb04778f2020-11-30 02:41:14 -05002186 _check_ebuild_owners,
Mike Frysinger6ee76b82020-11-20 01:16:06 -05002187 _check_ebuild_r0,
Mike Frysingercd363c82014-02-01 05:20:18 -05002188 _check_ebuild_virtual_pv,
Mike Frysingerf9d41b32017-02-23 15:20:04 -05002189 _check_exec_files,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002190 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07002191 _check_gofmt,
Bernie Thompson8e26f742020-07-23 14:32:31 -07002192 _check_keywords,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04002193 _check_layout_conf,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002194 _check_no_long_lines,
Keigo Oka4a09bd92019-05-07 14:01:00 +09002195 _check_no_new_gyp,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002196 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07002197 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07002198 _check_portage_make_use_var,
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -06002199 _check_rustfmt,
Aviv Keshet5ac59522017-01-31 14:28:27 -08002200 _check_tabbed_indents,
Ryan Cui9b651632011-05-11 11:38:58 -07002201]
Ryan Cuiec4d6332011-05-02 14:15:25 -07002202
Ryan Cui1562fb82011-05-09 11:01:31 -07002203
Ryan Cui9b651632011-05-11 11:38:58 -07002204# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04002205# that the flag controls (value).
2206_HOOK_FLAGS = {
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07002207 'clang_format_check': _check_clang_format,
Mike Frysingera7642f52015-03-25 18:31:42 -04002208 'checkpatch_check': _run_checkpatch,
Brian Norris23c62e92018-11-14 12:25:51 -08002209 'kerneldoc_check': _run_kerneldoc,
Ryan Cui9b651632011-05-11 11:38:58 -07002210 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04002211 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07002212 'long_line_check': _check_no_long_lines,
Alex Deymof5792ce2015-08-24 22:50:08 -07002213 'cros_license_check': _check_cros_license,
2214 'aosp_license_check': _check_aosp_license,
LaMont Jones872fe762020-02-10 15:36:14 -07002215 'gofmt_check': _check_gofmt,
Bernie Thompson8e26f742020-07-23 14:32:31 -07002216 'keyword_check': _check_keywords,
Ryan Cui9b651632011-05-11 11:38:58 -07002217 'tab_check': _check_no_tabs,
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08002218 'tabbed_indent_required_check': _check_tabbed_indents,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07002219 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08002220 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07002221 'bug_field_check': _check_change_has_bug_field,
2222 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07002223 'manifest_check': _check_manifests,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08002224 'contribution_check': _check_change_is_contribution,
Mike Frysinger47bd1252018-06-11 12:12:20 -04002225 'project_prefix_check': _check_project_prefix,
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09002226 'filepath_chartype_check': _check_filepath_chartype,
Keiichi Watanabe6ab73fd2020-07-14 23:42:02 +09002227 'cargo_clippy_check': _check_cargo_clippy,
Julius Werner291d8602020-07-31 17:34:02 -07002228 'exec_files_check': _check_exec_files,
Mike Frysingera5c2a5b2020-12-18 01:57:13 -05002229 'kernel_splitconfig_check': _kernel_configcheck,
Ryan Cui9b651632011-05-11 11:38:58 -07002230}
2231
2232
Mike Frysinger3554bc92015-03-11 04:59:21 -04002233def _get_override_hooks(config):
2234 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07002235
2236 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08002237
2238 Args:
2239 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07002240 """
2241 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002242 SECTION_OPTIONS = 'Hook Overrides Options'
Ryan Cui9b651632011-05-11 11:38:58 -07002243
Mike Frysinger56e8de02019-07-31 14:40:14 -04002244 valid_keys = set(_HOOK_FLAGS.keys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002245 hooks = _HOOK_FLAGS.copy()
Mike Frysinger180ecd62020-08-19 00:41:51 -04002246 hook_overrides = set(
2247 config.options(SECTION) if config.has_section(SECTION) else [])
2248
2249 unknown_keys = hook_overrides - valid_keys
2250 if unknown_keys:
2251 raise ValueError(f'{_CONFIG_FILE}: [{SECTION}]: unknown keys: '
2252 f'{unknown_keys}')
Mike Frysinger3554bc92015-03-11 04:59:21 -04002253
2254 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07002255 disable_flags = []
Mike Frysinger180ecd62020-08-19 00:41:51 -04002256 for flag in valid_keys:
2257 if flag in hook_overrides:
2258 try:
2259 enabled = config.getboolean(SECTION, flag)
2260 except ValueError as e:
2261 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
2262 (flag, _CONFIG_FILE, e))
2263 elif hooks[flag] in _COMMON_HOOKS:
2264 # Enable common hooks by default so we process custom options below.
2265 enabled = True
2266 else:
2267 # All other hooks we left as a tristate. We use this below for a few
2268 # hooks to control default behavior.
2269 enabled = None
Mike Frysinger3554bc92015-03-11 04:59:21 -04002270
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002271 if enabled:
2272 enable_flags.append(flag)
Mike Frysinger180ecd62020-08-19 00:41:51 -04002273 elif enabled is not None:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002274 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07002275
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002276 # See if this hook has custom options.
2277 if enabled:
2278 try:
2279 options = config.get(SECTION_OPTIONS, flag)
2280 hooks[flag] = functools.partial(hooks[flag], options=options.split())
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002281 hooks[flag].__name__ = flag
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04002282 except (configparser.NoOptionError, configparser.NoSectionError):
Mike Frysingerf8ce1712015-03-25 18:32:33 -04002283 pass
2284
2285 enabled_hooks = set(hooks[x] for x in enable_flags)
2286 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger45334bd2019-11-04 10:42:33 -05002287
Mike Frysinger9ab64b12019-11-04 10:53:08 -05002288 if _check_change_has_signoff_field not in enabled_hooks:
Drew Davenport85848e52020-01-15 14:40:29 -07002289 if _check_change_has_signoff_field not in disabled_hooks:
2290 enabled_hooks.add(_check_change_has_no_signoff_field)
Mike Frysinger45334bd2019-11-04 10:42:33 -05002291 if _check_change_has_branch_field not in enabled_hooks:
2292 enabled_hooks.add(_check_change_has_no_branch_field)
2293
Mike Frysinger3554bc92015-03-11 04:59:21 -04002294 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07002295
2296
Jon Salz3ee59de2012-08-18 13:54:22 +08002297def _get_project_hook_scripts(config):
2298 """Returns a list of project-specific hook scripts.
2299
2300 Args:
2301 config: A ConfigParser for the project's config file.
2302 """
2303 SECTION = 'Hook Scripts'
2304 if not config.has_section(SECTION):
2305 return []
2306
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002307 return config.items(SECTION)
Jon Salz3ee59de2012-08-18 13:54:22 +08002308
2309
Mike Frysingerff916c62020-12-18 01:58:08 -05002310def _get_project_hooks(presubmit, config_file=None):
Ryan Cui9b651632011-05-11 11:38:58 -07002311 """Returns a list of hooks that need to be run for a project.
2312
2313 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002314
2315 Args:
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002316 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002317 config_file: A string, the config file. Defaults to _CONFIG_FILE.
Ryan Cui9b651632011-05-11 11:38:58 -07002318 """
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04002319 config = configparser.RawConfigParser()
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002320 if config_file is None:
2321 config_file = _CONFIG_FILE
2322 if not os.path.exists(config_file):
Jon Salz3ee59de2012-08-18 13:54:22 +08002323 # Just use an empty config file
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04002324 config = configparser.RawConfigParser()
Shuhei Takahashi3cbb8dd2019-10-29 12:37:11 +09002325 else:
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002326 config.read(config_file)
Jon Salz3ee59de2012-08-18 13:54:22 +08002327
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002328 if presubmit:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07002329 hooks = _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002330 else:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07002331 hooks = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002332
Mike Frysinger3554bc92015-03-11 04:59:21 -04002333 enabled_hooks, disabled_hooks = _get_override_hooks(config)
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07002334 hooks = [hook for hook in hooks if hook not in disabled_hooks]
2335
2336 # If a list is both in _COMMON_HOOKS and also enabled explicitly through an
2337 # override, keep the override only. Note that the override may end up being
2338 # a functools.partial, in which case we need to extract the .func to compare
2339 # it to the common hooks.
2340 unwrapped_hooks = [getattr(hook, 'func', hook) for hook in enabled_hooks]
2341 hooks = [hook for hook in hooks if hook not in unwrapped_hooks]
2342
2343 hooks = list(enabled_hooks) + hooks
Ryan Cui9b651632011-05-11 11:38:58 -07002344
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002345 for name, script in _get_project_hook_scripts(config):
2346 func = functools.partial(_run_project_hook_script, script)
2347 func.__name__ = name
2348 hooks.append(func)
Jon Salz3ee59de2012-08-18 13:54:22 +08002349
Ryan Cui9b651632011-05-11 11:38:58 -07002350 return hooks
2351
2352
Alex Deymo643ac4c2015-09-03 10:40:50 -07002353def _run_project_hooks(project_name, proj_dir=None,
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002354 commit_list=None, presubmit=False,
2355 config_file=None):
Ryan Cui1562fb82011-05-09 11:01:31 -07002356 """For each project run its project specific hook from the hooks dictionary.
2357
2358 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002359 project_name: The name of project to run hooks for.
Doug Anderson44a644f2011-11-02 10:37:37 -07002360 proj_dir: If non-None, this is the directory the project is in. If None,
2361 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07002362 commit_list: A list of commits to run hooks against. If None or empty list
2363 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002364 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002365 config_file: A string, the presubmit config file. If not specified,
2366 defaults to PRESUBMIT.cfg in the project directory.
Ryan Cui1562fb82011-05-09 11:01:31 -07002367
2368 Returns:
2369 Boolean value of whether any errors were ecountered while running the hooks.
2370 """
Doug Anderson44a644f2011-11-02 10:37:37 -07002371 if proj_dir is None:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002372 proj_dirs = _run_command(
2373 ['repo', 'forall', project_name, '-c', 'pwd']).split()
Mike Frysingere52b1bc2019-09-16 23:45:41 -04002374 if not proj_dirs:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002375 print('%s cannot be found.' % project_name, file=sys.stderr)
David James2edd9002013-10-11 14:09:19 -07002376 print('Please specify a valid project.', file=sys.stderr)
2377 return True
2378 if len(proj_dirs) > 1:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002379 print('%s is associated with multiple directories.' % project_name,
David James2edd9002013-10-11 14:09:19 -07002380 file=sys.stderr)
2381 print('Please specify a directory to help disambiguate.', file=sys.stderr)
2382 return True
2383 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07002384
Ryan Cuiec4d6332011-05-02 14:15:25 -07002385 pwd = os.getcwd()
2386 # hooks assume they are run from the root of the project
2387 os.chdir(proj_dir)
2388
Mike Frysingered1b95a2019-12-12 19:04:51 -05002389 color = terminal.Color()
2390
Alex Deymo643ac4c2015-09-03 10:40:50 -07002391 remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
2392 '--symbolic-full-name', '@{u}']).strip()
2393 if not remote_branch:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002394 print("Your project %s doesn't track any remote repo." % project_name,
Alex Deymo643ac4c2015-09-03 10:40:50 -07002395 file=sys.stderr)
2396 remote = None
2397 else:
Josh Pratt15d13ab2018-08-13 11:52:48 +10002398 branch_items = remote_branch.split('/', 1)
2399 if len(branch_items) != 2:
2400 PrintErrorForProject(
2401 project_name,
2402 HookFailure(
2403 'Cannot get remote and branch name (%s)' % remote_branch))
2404 os.chdir(pwd)
2405 return True
2406 remote, _branch = branch_items
Alex Deymo643ac4c2015-09-03 10:40:50 -07002407
2408 project = Project(name=project_name, dir=proj_dir, remote=remote)
2409
Doug Anderson14749562013-06-26 13:38:29 -07002410 if not commit_list:
2411 try:
2412 commit_list = _get_commits()
2413 except VerifyException as e:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002414 PrintErrorForProject(project.name, HookFailure(str(e)))
Doug Anderson14749562013-06-26 13:38:29 -07002415 os.chdir(pwd)
2416 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07002417
Mike Frysingerff916c62020-12-18 01:58:08 -05002418 hooks = _get_project_hooks(presubmit, config_file)
Ryan Cui1562fb82011-05-09 11:01:31 -07002419 error_found = False
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002420 commit_count = len(commit_list)
Mike Frysingerb99b3772019-08-17 14:19:44 -04002421 hook_count = len(hooks)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002422 for i, commit in enumerate(commit_list):
Mike Frysingerb2496652019-09-12 23:35:46 -04002423 CACHE.clear()
2424
LaMont Jones69d3e182020-03-11 15:00:53 -06002425 # If run with --pre-submit, then commit is PRE_SUBMIT, and not a commit.
2426 # Use that as the description.
2427 desc = commit if commit == PRE_SUBMIT else _get_commit_desc(commit)
Mike Frysingered1b95a2019-12-12 19:04:51 -05002428 print('[%s %i/%i %s] %s' %
2429 (color.Color(color.CYAN, 'COMMIT'), i + 1, commit_count, commit[0:12],
2430 desc.splitlines()[0]))
2431
Mike Frysingerb99b3772019-08-17 14:19:44 -04002432 for h, hook in enumerate(hooks):
Mike Frysingered1b95a2019-12-12 19:04:51 -05002433 output = ('[%s %i/%i PRESUBMIT.cfg] %s' %
2434 (color.Color(color.YELLOW, 'RUNNING'), h + 1, hook_count,
2435 hook.__name__))
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002436 print(output, end='\r')
2437 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07002438 hook_error = hook(project, commit)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002439 print(' ' * len(output), end='\r')
2440 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07002441 if hook_error:
Mike Frysingered1b95a2019-12-12 19:04:51 -05002442 if not isinstance(hook_error, list):
2443 hook_error = [hook_error]
2444 PrintErrorsForCommit(color, hook, project.name, hook_error)
Ryan Cui1562fb82011-05-09 11:01:31 -07002445 error_found = True
Don Garrettdba548a2011-05-05 15:17:14 -07002446
Ryan Cuiec4d6332011-05-02 14:15:25 -07002447 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07002448 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002449
Mike Frysingerae409522014-02-01 03:16:11 -05002450
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002451# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002452
Ryan Cui1562fb82011-05-09 11:01:31 -07002453
Mike Frysingerae409522014-02-01 03:16:11 -05002454def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08002455 """Main function invoked directly by repo.
2456
2457 This function will exit directly upon error so that repo doesn't print some
2458 obscure error message.
2459
2460 Args:
2461 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07002462 worktree_list: A list of directories. It should be the same length as
2463 project_list, so that each entry in project_list matches with a directory
2464 in worktree_list. If None, we will attempt to calculate the directories
2465 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08002466 kwargs: Leave this here for forward-compatibility.
2467 """
Mike Frysingered1b95a2019-12-12 19:04:51 -05002468 start_time = datetime.datetime.now()
Ryan Cui1562fb82011-05-09 11:01:31 -07002469 found_error = False
David James2edd9002013-10-11 14:09:19 -07002470 if not worktree_list:
2471 worktree_list = [None] * len(project_list)
2472 for project, worktree in zip(project_list, worktree_list):
2473 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07002474 found_error = True
2475
Mike Frysingered1b95a2019-12-12 19:04:51 -05002476 end_time = datetime.datetime.now()
2477 color = terminal.Color()
Mike Frysingerae409522014-02-01 03:16:11 -05002478 if found_error:
Mike Frysingered1b95a2019-12-12 19:04:51 -05002479 msg = ('%s: Preupload failed due to above error(s).\n'
Ryan Cui9b651632011-05-11 11:38:58 -07002480 '- To disable some source style checks, and for other hints, see '
Mike Frysingered1b95a2019-12-12 19:04:51 -05002481 '<checkout_dir>/src/repohooks/README.md\n'
2482 "- To upload only current project, run 'repo upload .'" %
2483 (color.Color(color.RED, 'FATAL'),))
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04002484 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07002485 sys.exit(1)
Mike Frysingered1b95a2019-12-12 19:04:51 -05002486 else:
2487 msg = ('[%s] repohooks passed in %s' %
2488 (color.Color(color.GREEN, 'PASSED'), end_time - start_time))
2489 print(msg)
Anush Elangovan63afad72011-03-23 00:41:27 -07002490
Ryan Cui1562fb82011-05-09 11:01:31 -07002491
Doug Anderson44a644f2011-11-02 10:37:37 -07002492def _identify_project(path):
2493 """Identify the repo project associated with the given path.
2494
2495 Returns:
2496 A string indicating what project is associated with the path passed in or
2497 a blank string upon failure.
2498 """
2499 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002500 stderr=True, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002501
2502
Mike Frysinger55f85b52014-12-18 14:45:21 -05002503def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07002504 """Run hooks directly (outside of the context of repo).
2505
Doug Anderson44a644f2011-11-02 10:37:37 -07002506 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05002507 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07002508
2509 Returns:
2510 0 if no pre-upload failures, 1 if failures.
2511
2512 Raises:
2513 BadInvocation: On some types of invocation errors.
2514 """
Mike Frysinger66142932014-12-18 14:55:57 -05002515 parser = commandline.ArgumentParser(description=__doc__)
2516 parser.add_argument('--dir', default=None,
2517 help='The directory that the project lives in. If not '
2518 'specified, use the git project root based on the cwd.')
2519 parser.add_argument('--project', default=None,
2520 help='The project repo path; this can affect how the '
2521 'hooks get run, since some hooks are project-specific. '
2522 'For chromite this is chromiumos/chromite. If not '
2523 'specified, the repo tool will be used to figure this '
2524 'out based on the dir.')
2525 parser.add_argument('--rerun-since', default=None,
Vadim Bendebury75447b92018-01-10 12:06:01 -08002526 help='Rerun hooks on old commits since some point '
2527 'in the past. The argument could be a date (should '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002528 "match git log's concept of a date, e.g. 2012-06-20), "
Vadim Bendebury75447b92018-01-10 12:06:01 -08002529 'or a SHA1, or just a number of commits to check (from 1 '
2530 'to 99). This option is mutually exclusive with '
2531 '--pre-submit.')
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002532 parser.add_argument('--pre-submit', action='store_true',
Mike Frysinger66142932014-12-18 14:55:57 -05002533 help='Run the check against the pending commit. '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002534 "This option should be used at the 'git commit' "
2535 "phase as opposed to 'repo upload'. This option "
Mike Frysinger66142932014-12-18 14:55:57 -05002536 'is mutually exclusive with --rerun-since.')
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002537 parser.add_argument('--presubmit-config',
2538 help='Specify presubmit config file to be used.')
Mike Frysinger66142932014-12-18 14:55:57 -05002539 parser.add_argument('commits', nargs='*',
2540 help='Check specific commits')
2541 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07002542
Doug Anderson14749562013-06-26 13:38:29 -07002543 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05002544 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002545 raise BadInvocation("Can't pass commits and use rerun-since: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002546 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07002547
Vadim Bendebury75447b92018-01-10 12:06:01 -08002548 if len(opts.rerun_since) < 3 and opts.rerun_since.isdigit():
2549 # This must be the number of commits to check. We don't expect the user
2550 # to want to check more than 99 commits.
2551 limit = '-n%s' % opts.rerun_since
2552 elif git.IsSHA1(opts.rerun_since, False):
2553 limit = '%s..' % opts.rerun_since
2554 else:
2555 # This better be a date.
2556 limit = '--since=%s' % opts.rerun_since
2557 cmd = ['git', 'log', limit, '--pretty=%H']
Doug Anderson14749562013-06-26 13:38:29 -07002558 all_commits = _run_command(cmd).splitlines()
2559 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
2560
2561 # Eliminate chrome-bot commits but keep ordering the same...
2562 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05002563 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07002564
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002565 if opts.pre_submit:
2566 raise BadInvocation('rerun-since and pre-submit can not be '
2567 'used together')
2568 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05002569 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002570 raise BadInvocation("Can't pass commits and use pre-submit: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002571 ' '.join(opts.commits))
2572 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07002573
2574 # Check/normlaize git dir; if unspecified, we'll use the root of the git
2575 # project from CWD
2576 if opts.dir is None:
Mike Frysinger9ba001a2020-11-20 01:02:11 -05002577 git_dir = _run_command(['git', 'rev-parse', '--show-toplevel'],
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002578 stderr=True).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002579 if not git_dir:
2580 raise BadInvocation('The current directory is not part of a git project.')
Mike Frysinger9ba001a2020-11-20 01:02:11 -05002581 opts.dir = git_dir
Doug Anderson44a644f2011-11-02 10:37:37 -07002582 elif not os.path.isdir(opts.dir):
2583 raise BadInvocation('Invalid dir: %s' % opts.dir)
2584 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
2585 raise BadInvocation('Not a git directory: %s' % opts.dir)
2586
2587 # Identify the project if it wasn't specified; this _requires_ the repo
2588 # tool to be installed and for the project to be part of a repo checkout.
2589 if not opts.project:
2590 opts.project = _identify_project(opts.dir)
2591 if not opts.project:
2592 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
2593
Doug Anderson14749562013-06-26 13:38:29 -07002594 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05002595 commit_list=opts.commits,
Sonny Sasaka5a905ea2020-07-24 15:30:12 -07002596 presubmit=opts.pre_submit,
2597 config_file=opts.presubmit_config)
Doug Anderson44a644f2011-11-02 10:37:37 -07002598 if found_error:
2599 return 1
2600 return 0
2601
2602
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002603if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05002604 sys.exit(direct_main(sys.argv[1:]))