blob: 9dc25271825121d47ac4549ed9a610d83171338d [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
9You can add new checks by adding a functions to the HOOKS constants.
10"""
11
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040012from __future__ import print_function
13
Keigo Okadd908822019-06-04 11:30:25 +090014import argparse
Alex Deymo643ac4c2015-09-03 10:40:50 -070015import collections
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
Ryan Cuiec4d6332011-05-02 14:15:25 -070022import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070023import sys
Peter Ammon811f6702014-06-12 15:45:38 -070024import stat
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070025
Ryan Cui1562fb82011-05-09 11:01:31 -070026from errors import (VerifyException, HookFailure, PrintErrorForProject,
27 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070028
Mike Frysinger919c7032019-09-13 17:48:08 -040029if __name__ in ('__builtin__', 'builtins'):
Mike Frysinger653cd262019-09-20 14:05:02 -040030 # If repo imports us, the __name__ will be __builtin__, and the cwd will be in
31 # the top level of the checkout (i.e. $CHROMEOS_CHECKOUT). chromite will be
32 # in that directory, so add it to our path. This works whether we're running
33 # the repo in $CHROMEOS_CHECKOUT/.repo/repo/ or a custom version in a
34 # completely different tree.
35 # TODO(vapier): Python 2 used "__builtin__" while Python 3 uses "builtins".
Mike Frysinger6850d512018-05-21 12:12:14 -040036 sys.path.insert(0, os.getcwd())
37
Mike Frysinger653cd262019-09-20 14:05:02 -040038elif __name__ == '__main__':
39 # If we're run directly, we'll find chromite relative to the repohooks dir in
40 # $CHROMEOS_CHECKOUT/src/repohooks, so go up two dirs.
David Jamesc3b68b32013-04-03 09:17:03 -070041 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
42
Mike Frysingerfd481ce2019-09-13 18:14:48 -040043# The sys.path monkey patching confuses the linter.
44# pylint: disable=wrong-import-position
Mike Frysinger66142932014-12-18 14:55:57 -050045from chromite.lib import commandline
Rahul Chaudhry0e515342015-08-07 12:00:43 -070046from chromite.lib import cros_build_lib
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050047from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070048from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070049from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040050from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070051
Vadim Bendebury2b62d742014-06-22 13:14:51 -070052PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070053
54COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050055 # C++ and friends
Mike Frysinger24dd3c52019-08-17 14:22:48 -040056 r'.*\.c$', r'.*\.cc$', r'.*\.cpp$', r'.*\.h$', r'.*\.m$', r'.*\.mm$',
57 r'.*\.inl$', r'.*\.asm$', r'.*\.hxx$', r'.*\.hpp$', r'.*\.s$', r'.*\.S$',
Mike Frysingerae409522014-02-01 03:16:11 -050058 # Scripts
Mike Frysinger24dd3c52019-08-17 14:22:48 -040059 r'.*\.js$', r'.*\.py$', r'.*\.sh$', r'.*\.rb$', r'.*\.pl$', r'.*\.pm$',
Mike Frysingerae409522014-02-01 03:16:11 -050060 # No extension at all, note that ALL CAPS files are black listed in
61 # COMMON_EXCLUDED_LIST below.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040062 r'(^|.*[\\\/])[^.]+$',
Mike Frysingerae409522014-02-01 03:16:11 -050063 # Other
Mike Frysinger24dd3c52019-08-17 14:22:48 -040064 r'.*\.java$', r'.*\.mk$', r'.*\.am$',
65 r'.*\.policy$', r'.*\.conf$', r'.*\.go$',
66 r'(^OWNERS|/OWNERS)',
Ryan Cuiec4d6332011-05-02 14:15:25 -070067]
68
Ryan Cui1562fb82011-05-09 11:01:31 -070069
Ryan Cuiec4d6332011-05-02 14:15:25 -070070COMMON_EXCLUDED_PATHS = [
Daniel Erate3ea3fc2015-02-13 15:27:52 -070071 # For ebuild trees, ignore any caches and manifest data.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040072 r'.*/Manifest$',
73 r'.*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$',
Doug Anderson5bfb6792011-10-25 16:45:41 -070074
Daniel Erate3ea3fc2015-02-13 15:27:52 -070075 # Ignore profiles data (like overlay-tegra2/profiles).
Mike Frysinger24dd3c52019-08-17 14:22:48 -040076 r'(^|.*/)overlay-.*/profiles/.*',
77 r'^profiles/.*$',
Mike Frysinger98638102014-08-28 00:15:08 -040078
C Shapiro8f90e9b2017-06-28 09:54:50 -060079 # Ignore config files in ebuild setup.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040080 r'(^|.*/)overlay-.*/chromeos-base/chromeos-bsp.*/files/.*',
81 r'^chromeos-base/chromeos-bsp.*/files/.*',
C Shapiro8f90e9b2017-06-28 09:54:50 -060082
Daniel Erate3ea3fc2015-02-13 15:27:52 -070083 # Ignore minified js and jquery.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040084 r'.*\.min\.js',
85 r'.*jquery.*\.js',
Mike Frysinger33a458d2014-03-03 17:00:51 -050086
87 # Ignore license files as the content is often taken verbatim.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040088 r'.*/licenses/.*',
Alex Klein619c0912019-01-30 17:13:23 -070089
Mike Frysinger13650402019-07-31 14:31:46 -040090 # Exclude generated protobuf bindings.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040091 r'.*_pb2\.py$',
92 r'.*\.pb\.go$',
Ryan Cuiec4d6332011-05-02 14:15:25 -070093]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070094
Ken Turnerd07564b2018-02-08 17:57:59 +110095LICENSE_EXCLUDED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -040096 r'^(.*/)?OWNERS$',
Ken Turnerd07564b2018-02-08 17:57:59 +110097]
Ryan Cui1562fb82011-05-09 11:01:31 -070098
Ryan Cui9b651632011-05-11 11:38:58 -070099_CONFIG_FILE = 'PRESUBMIT.cfg'
100
101
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700102# File containing wildcards, one per line, matching files that should be
103# excluded from presubmit checks. Lines beginning with '#' are ignored.
104_IGNORE_FILE = '.presubmitignore'
105
Doug Anderson44a644f2011-11-02 10:37:37 -0700106# Exceptions
107
108
109class BadInvocation(Exception):
110 """An Exception indicating a bad invocation of the program."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700111
112
Ryan Cui1562fb82011-05-09 11:01:31 -0700113# General Helpers
114
Sean Paulba01d402011-05-05 11:36:23 -0400115
Mike Frysingerb2496652019-09-12 23:35:46 -0400116class Cache(object):
117 """General helper for caching git content."""
118
119 def __init__(self):
120 self._cache = {}
121
122 def get_subcache(self, scope):
123 return self._cache.setdefault(scope, {})
124
125 def clear(self):
126 self._cache.clear()
127
128CACHE = Cache()
129
130
Alex Deymo643ac4c2015-09-03 10:40:50 -0700131Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
132
133
Mike Frysinger526a5f82019-09-13 18:05:30 -0400134def _run_command(cmd, **kwargs):
Doug Anderson44a644f2011-11-02 10:37:37 -0700135 """Executes the passed in command and returns raw stdout output.
136
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400137 This is a convenience func to set some run defaults differently.
Mike Frysinger526a5f82019-09-13 18:05:30 -0400138
Doug Anderson44a644f2011-11-02 10:37:37 -0700139 Args:
140 cmd: The command to run; should be a list of strings.
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400141 **kwargs: Same as cros_build_lib.run.
Doug Anderson44a644f2011-11-02 10:37:37 -0700142
143 Returns:
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700144 The stdout from the process (discards stderr and returncode).
Doug Anderson44a644f2011-11-02 10:37:37 -0700145 """
Mike Frysinger526a5f82019-09-13 18:05:30 -0400146 kwargs.setdefault('print_cmd', False)
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400147 kwargs.setdefault('stdout', True)
148 kwargs.setdefault('check', False)
Mike Frysinger054c1592019-11-18 03:51:54 -0500149 result = cros_build_lib.run(cmd, **kwargs)
Mike Frysinger7bb709f2019-09-29 23:20:12 -0400150 # NB: We decode this directly rather than through kwargs as our tests rely
151 # on this post-processing behavior currently.
Mike Frysinger71e643e2019-09-13 17:26:39 -0400152 return result.output.decode('utf-8', 'replace')
Ryan Cui72834d12011-05-05 14:51:33 -0700153
Ryan Cui1562fb82011-05-09 11:01:31 -0700154
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700155def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700156 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700157 if __name__ == '__main__':
158 # Works when file is run on its own (__file__ is defined)...
159 return os.path.abspath(os.path.dirname(__file__))
160 else:
161 # We need to do this when we're run through repo. Since repo executes
162 # us with execfile(), we don't get __file__ defined.
163 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
164 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700165
Ryan Cui1562fb82011-05-09 11:01:31 -0700166
Ryan Cuiec4d6332011-05-02 14:15:25 -0700167def _match_regex_list(subject, expressions):
168 """Try to match a list of regular expressions to a string.
169
170 Args:
171 subject: The string to match regexes on
172 expressions: A list of regular expressions to check for matches with.
173
174 Returns:
175 Whether the passed in subject matches any of the passed in regexes.
176 """
177 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500178 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700179 return True
180 return False
181
Ryan Cui1562fb82011-05-09 11:01:31 -0700182
Mike Frysingerae409522014-02-01 03:16:11 -0500183def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700184 """Filter out files based on the conditions passed in.
185
186 Args:
187 files: list of filepaths to filter
188 include_list: list of regex that when matched with a file path will cause it
189 to be added to the output list unless the file is also matched with a
190 regex in the exclude_list.
191 exclude_list: list of regex that when matched with a file will prevent it
192 from being added to the output list, even if it is also matched with a
193 regex in the include_list.
194
195 Returns:
196 A list of filepaths that contain files matched in the include_list and not
197 in the exclude_list.
198 """
199 filtered = []
200 for f in files:
201 if (_match_regex_list(f, include_list) and
202 not _match_regex_list(f, exclude_list)):
203 filtered.append(f)
204 return filtered
205
Ryan Cuiec4d6332011-05-02 14:15:25 -0700206
207# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700208
209
Ryan Cui4725d952011-05-05 15:41:19 -0700210def _get_upstream_branch():
211 """Returns the upstream tracking branch of the current branch.
212
213 Raises:
214 Error if there is no tracking branch
215 """
216 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
217 current_branch = current_branch.replace('refs/heads/', '')
218 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700219 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700220
221 cfg_option = 'branch.' + current_branch + '.%s'
222 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
223 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
224 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700225 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700226
227 return full_upstream.replace('heads', 'remotes/' + remote)
228
Ryan Cui1562fb82011-05-09 11:01:31 -0700229
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700230def _get_patch(commit):
231 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700232 if commit == PRE_SUBMIT:
233 return _run_command(['git', 'diff', '--cached', 'HEAD'])
234 else:
235 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700236
Ryan Cui1562fb82011-05-09 11:01:31 -0700237
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500238def _get_file_content(path, commit):
239 """Returns the content of a file at a specific commit.
240
241 We can't rely on the file as it exists in the filesystem as people might be
242 uploading a series of changes which modifies the file multiple times.
243
244 Note: The "content" of a symlink is just the target. So if you're expecting
245 a full file, you should check that first. One way to detect is that the
246 content will not have any newlines.
247 """
Mike Frysingerb2496652019-09-12 23:35:46 -0400248 # Make sure people don't accidentally pass in full paths which will never
249 # work. You need to use relative=True with _get_affected_files.
250 if path.startswith('/'):
251 raise ValueError('_get_file_content must be called with relative paths: %s'
252 % (path,))
253
254 # {<commit>: {<path1>: <content>, <path2>: <content>}}
255 cache = CACHE.get_subcache('get_file_content')
256 if path in cache:
257 return cache[path]
258
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700259 if commit == PRE_SUBMIT:
Mike Frysingerb2496652019-09-12 23:35:46 -0400260 content = _run_command(['git', 'diff', 'HEAD', path])
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700261 else:
Mike Frysingerb2496652019-09-12 23:35:46 -0400262 content = _run_command(['git', 'show', '%s:%s' % (commit, path)])
263 cache[path] = content
264 return content
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500265
266
Mike Frysingerae409522014-02-01 03:16:11 -0500267def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700268 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysingerb2496652019-09-12 23:35:46 -0400269 # {<commit>: {<path1>: <content>, <path2>: <content>}}
270 cache = CACHE.get_subcache('get_file_diff')
271 if path in cache:
272 return cache[path]
273
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700274 if commit == PRE_SUBMIT:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800275 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff', 'HEAD',
276 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700277 else:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800278 command = ['git', 'show', '-p', '--pretty=format:', '--no-ext-diff', commit,
279 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700280 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700281
282 new_lines = []
283 line_num = 0
284 for line in output.splitlines():
285 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
286 if m:
287 line_num = int(m.groups(1)[0])
288 continue
289 if line.startswith('+') and not line.startswith('++'):
Mike Frysinger71e643e2019-09-13 17:26:39 -0400290 new_lines.append((line_num, line[1:]))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700291 if not line.startswith('-'):
292 line_num += 1
Mike Frysingerb2496652019-09-12 23:35:46 -0400293 cache[path] = new_lines
Ryan Cuiec4d6332011-05-02 14:15:25 -0700294 return new_lines
295
Ryan Cui1562fb82011-05-09 11:01:31 -0700296
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700297def _get_ignore_wildcards(directory, cache):
298 """Get wildcards listed in a directory's _IGNORE_FILE.
299
300 Args:
301 directory: A string containing a directory path.
302 cache: A dictionary (opaque to caller) caching previously-read wildcards.
303
304 Returns:
305 A list of wildcards from _IGNORE_FILE or an empty list if _IGNORE_FILE
306 wasn't present.
307 """
308 # In the cache, keys are directories and values are lists of wildcards from
309 # _IGNORE_FILE within those directories (and empty if no file was present).
310 if directory not in cache:
311 wildcards = []
312 dotfile_path = os.path.join(directory, _IGNORE_FILE)
313 if os.path.exists(dotfile_path):
314 # TODO(derat): Consider using _get_file_content() to get the file as of
315 # this commit instead of the on-disk version. This may have a noticeable
316 # performance impact, as each call to _get_file_content() runs git.
317 with open(dotfile_path, 'r') as dotfile:
318 for line in dotfile.readlines():
319 line = line.strip()
320 if line.startswith('#'):
321 continue
322 if line.endswith('/'):
323 line += '*'
324 wildcards.append(line)
325 cache[directory] = wildcards
326
327 return cache[directory]
328
329
330def _path_is_ignored(path, cache):
331 """Check whether a path is ignored by _IGNORE_FILE.
332
333 Args:
334 path: A string containing a path.
335 cache: A dictionary (opaque to caller) caching previously-read wildcards.
336
337 Returns:
338 True if a file named _IGNORE_FILE in one of the passed-in path's parent
339 directories contains a wildcard matching the path.
340 """
341 # Skip ignore files.
342 if os.path.basename(path) == _IGNORE_FILE:
343 return True
344
345 path = os.path.abspath(path)
346 base = os.getcwd()
347
348 prefix = os.path.dirname(path)
349 while prefix.startswith(base):
350 rel_path = path[len(prefix) + 1:]
351 for wildcard in _get_ignore_wildcards(prefix, cache):
352 if fnmatch.fnmatch(rel_path, wildcard):
353 return True
354 prefix = os.path.dirname(prefix)
355
356 return False
357
358
Mike Frysinger292b45d2014-11-25 01:17:10 -0500359def _get_affected_files(commit, include_deletes=False, relative=False,
360 include_symlinks=False, include_adds=True,
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700361 full_details=False, use_ignore_files=True):
Peter Ammon811f6702014-06-12 15:45:38 -0700362 """Returns list of file paths that were modified/added, excluding symlinks.
363
364 Args:
365 commit: The commit
366 include_deletes: If true, we'll include deleted files in the result
367 relative: Whether to return relative or full paths to files
Mike Frysinger292b45d2014-11-25 01:17:10 -0500368 include_symlinks: If true, we'll include symlinks in the result
369 include_adds: If true, we'll include new files in the result
370 full_details: If False, return filenames, else return structured results.
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700371 use_ignore_files: Whether we ignore files matched by _IGNORE_FILE files.
Peter Ammon811f6702014-06-12 15:45:38 -0700372
373 Returns:
374 A list of modified/added (and perhaps deleted) files
375 """
Mike Frysinger292b45d2014-11-25 01:17:10 -0500376 if not relative and full_details:
377 raise ValueError('full_details only supports relative paths currently')
378
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700379 if commit == PRE_SUBMIT:
380 return _run_command(['git', 'diff-index', '--cached',
381 '--name-only', 'HEAD']).split()
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500382
383 path = os.getcwd()
Mike Frysingerb2496652019-09-12 23:35:46 -0400384 # {<commit>: {<path1>: <content>, <path2>: <content>}}
385 cache = CACHE.get_subcache('get_affected_files')
386 if path not in cache:
387 cache[path] = git.RawDiff(path, '%s^!' % commit)
388 files = cache[path]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500389
390 # Filter out symlinks.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500391 if not include_symlinks:
392 files = [x for x in files if not stat.S_ISLNK(int(x.dst_mode, 8))]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500393
394 if not include_deletes:
395 files = [x for x in files if x.status != 'D']
396
Mike Frysinger292b45d2014-11-25 01:17:10 -0500397 if not include_adds:
398 files = [x for x in files if x.status != 'A']
399
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700400 if use_ignore_files:
401 cache = {}
402 is_ignored = lambda x: _path_is_ignored(x.dst_file or x.src_file, cache)
403 files = [x for x in files if not is_ignored(x)]
404
Mike Frysinger292b45d2014-11-25 01:17:10 -0500405 if full_details:
406 # Caller wants the raw objects to parse status/etc... themselves.
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500407 return files
408 else:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500409 # Caller only cares about filenames.
410 files = [x.dst_file if x.dst_file else x.src_file for x in files]
411 if relative:
412 return files
413 else:
414 return [os.path.join(path, x) for x in files]
Peter Ammon811f6702014-06-12 15:45:38 -0700415
416
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700417def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700418 """Returns a list of commits for this review."""
Mike Frysingere300c7d2019-09-12 23:33:52 -0400419 cmd = ['git', 'log', '--no-merges', '--format=%H',
420 '%s..' % _get_upstream_branch()]
Ryan Cui72834d12011-05-05 14:51:33 -0700421 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700422
Ryan Cui1562fb82011-05-09 11:01:31 -0700423
Ryan Cuiec4d6332011-05-02 14:15:25 -0700424def _get_commit_desc(commit):
425 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700426 if commit == PRE_SUBMIT:
427 return ''
Mike Frysingerb2496652019-09-12 23:35:46 -0400428
429 # {<commit>: <content>}
430 cache = CACHE.get_subcache('get_commit_desc')
431 if commit not in cache:
Mike Frysinger4efdee72019-11-04 10:57:01 -0500432 cache[commit] = _run_command(['git', 'log', '--format=%B',
Mike Frysingerb2496652019-09-12 23:35:46 -0400433 commit + '^!'])
434 return cache[commit]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700435
436
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800437def _check_lines_in_diff(commit, files, check_callable, error_description):
438 """Checks given file for errors via the given check.
439
440 This is a convenience function for common per-line checks. It goes through all
441 files and returns a HookFailure with the error description listing all the
442 failures.
443
444 Args:
445 commit: The commit we're working on.
446 files: The files to check.
447 check_callable: A callable that takes a line and returns True if this line
448 _fails_ the check.
449 error_description: A string describing the error.
450 """
451 errors = []
452 for afile in files:
453 for line_num, line in _get_file_diff(afile, commit):
454 if check_callable(line):
455 errors.append('%s, line %s' % (afile, line_num))
456 if errors:
457 return HookFailure(error_description, errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400458 return None
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800459
460
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900461def _parse_common_inclusion_options(options):
462 """Parses common hook options for including/excluding files.
463
464 Args:
465 options: Option string list.
466
467 Returns:
468 (included, excluded) where each one is a list of regex strings.
469 """
470 parser = argparse.ArgumentParser()
471 parser.add_argument('--exclude_regex', action='append')
472 parser.add_argument('--include_regex', action='append')
473 opts = parser.parse_args(options)
474 included = opts.include_regex or []
475 excluded = opts.exclude_regex or []
476 return included, excluded
477
478
Ryan Cuiec4d6332011-05-02 14:15:25 -0700479# Common Hooks
480
Ryan Cui1562fb82011-05-09 11:01:31 -0700481
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900482def _check_no_long_lines(_project, commit, options=()):
Mike Frysinger55f85b52014-12-18 14:45:21 -0500483 """Checks there are no lines longer than MAX_LEN in any of the text files."""
Keigo Oka9732e382019-06-28 17:44:59 +0900484 LONG_LINE_OK_PATHS = [
485 # Go has no line length limit.
486 # https://golang.org/doc/effective_go.html#formatting
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400487 r'.*\.go$',
Keigo Oka9732e382019-06-28 17:44:59 +0900488 ]
Mike Frysinger55f85b52014-12-18 14:45:21 -0500489
Ryan Cuiec4d6332011-05-02 14:15:25 -0700490 MAX_LEN = 80
491
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900492 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700493 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900494 included + COMMON_INCLUDED_PATHS,
Keigo Oka9732e382019-06-28 17:44:59 +0900495 excluded + COMMON_EXCLUDED_PATHS + LONG_LINE_OK_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700496
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900497 errors = []
Ryan Cuiec4d6332011-05-02 14:15:25 -0700498 for afile in files:
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700499 skip_regexps = (
500 r'https?://',
501 r'^#\s*(define|include|import|pragma|if|ifndef|endif)\b',
502 )
503
504 if os.path.basename(afile).startswith('OWNERS'):
505 # File paths can get long, and there's no way to break them up into
506 # multiple lines.
507 skip_regexps += (
508 r'^include\b',
509 r'file:',
510 )
511
512 skip_regexps = [re.compile(x) for x in skip_regexps]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700513 for line_num, line in _get_file_diff(afile, commit):
514 # Allow certain lines to exceed the maxlen rule.
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700515 if len(line) <= MAX_LEN or any(x.search(line) for x in skip_regexps):
Jon Salz98255932012-08-18 14:48:02 +0800516 continue
517
518 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
519 if len(errors) == 5: # Just show the first 5 errors.
520 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700521
522 if errors:
523 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700524 return HookFailure(msg, errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400525 return None
Ryan Cui1562fb82011-05-09 11:01:31 -0700526
Ryan Cuiec4d6332011-05-02 14:15:25 -0700527
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900528def _check_no_stray_whitespace(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700529 """Checks that there is no stray whitespace at source lines end."""
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900530 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700531 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900532 included + COMMON_INCLUDED_PATHS,
533 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800534 return _check_lines_in_diff(commit, files,
535 lambda line: line.rstrip() != line,
536 'Found line ending with white space in:')
Ryan Cui1562fb82011-05-09 11:01:31 -0700537
Ryan Cuiec4d6332011-05-02 14:15:25 -0700538
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900539def _check_no_tabs(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700540 """Checks there are no unexpanded tabs."""
Mike Frysingercd134512017-10-26 04:36:33 -0400541 # Don't add entire repos here. Update the PRESUBMIT.cfg in each repo instead.
542 # We only whitelist known specific filetypes here that show up in all repos.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700543 TAB_OK_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400544 r'.*\.ebuild$',
545 r'.*\.eclass$',
546 r'.*\.go$',
547 r'.*/[M|m]akefile$',
548 r'.*\.mk$',
Ryan Cuiec4d6332011-05-02 14:15:25 -0700549 ]
550
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900551 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700552 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900553 included + COMMON_INCLUDED_PATHS,
554 excluded + COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800555 return _check_lines_in_diff(commit, files,
556 lambda line: '\t' in line,
557 'Found a tab character in:')
Ryan Cuiec4d6332011-05-02 14:15:25 -0700558
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800559
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900560def _check_tabbed_indents(_project, commit, options=()):
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800561 """Checks that indents use tabs only."""
562 TABS_REQUIRED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400563 r'.*\.ebuild$',
564 r'.*\.eclass$',
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800565 ]
566 LEADING_SPACE_RE = re.compile('[\t]* ')
567
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900568 included, excluded = _parse_common_inclusion_options(options)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800569 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900570 included + TABS_REQUIRED_PATHS,
571 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800572 return _check_lines_in_diff(
573 commit, files,
574 lambda line: LEADING_SPACE_RE.match(line) is not None,
575 'Found a space in indentation (must be all tabs):')
Ryan Cui1562fb82011-05-09 11:01:31 -0700576
Ryan Cuiec4d6332011-05-02 14:15:25 -0700577
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700578def _check_gofmt(_project, commit):
579 """Checks that Go files are formatted with gofmt."""
580 errors = []
581 files = _filter_files(_get_affected_files(commit, relative=True),
582 [r'\.go$'])
583
584 for gofile in files:
585 contents = _get_file_content(gofile, commit)
Mike Frysingeraa7dc942019-09-25 00:07:24 -0400586 output = _run_command(cmd=['gofmt', '-l'], input=contents.encode('utf-8'),
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700587 combine_stdout_stderr=True)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700588 if output:
589 errors.append(gofile)
590 if errors:
591 return HookFailure('Files not formatted with gofmt:', errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400592 return None
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700593
594
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600595def _check_rustfmt(_project, commit):
596 """Checks that Rust files are formatted with rustfmt."""
597 errors = []
598 files = _filter_files(_get_affected_files(commit, relative=True),
599 [r'\.rs$'])
600
601 for rustfile in files:
602 contents = _get_file_content(rustfile, commit)
Mike Frysingeraa7dc942019-09-25 00:07:24 -0400603 output = _run_command(cmd=['rustfmt'], input=contents.encode('utf-8'),
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600604 combine_stdout_stderr=True)
605 if output != contents:
606 errors.append(rustfile)
607 if errors:
608 return HookFailure('Files not formatted with rustfmt: '
609 "(run 'cargo fmt' to fix)", errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400610 return None
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600611
612
Mike Frysingerae409522014-02-01 03:16:11 -0500613def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700614 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700615 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700616
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700617 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700618 msg = 'Changelist description needs TEST field (after first line)'
619 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400620 return None
Ryan Cui1562fb82011-05-09 11:01:31 -0700621
Ryan Cuiec4d6332011-05-02 14:15:25 -0700622
Mike Frysingerae409522014-02-01 03:16:11 -0500623def _check_change_has_valid_cq_depend(_project, commit):
Jason D. Clinton299e3222019-05-23 09:42:03 -0600624 """Check for a correctly formatted Cq-Depend field in the commit message."""
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700625 desc = _get_commit_desc(commit)
Jason D. Clinton299e3222019-05-23 09:42:03 -0600626 msg = 'Changelist has invalid Cq-Depend target.'
627 example = 'Example: Cq-Depend: chromium:1234, chrome-internal:2345'
David Jamesc3b68b32013-04-03 09:17:03 -0700628 try:
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700629 patch.GetPaladinDeps(desc)
David Jamesc3b68b32013-04-03 09:17:03 -0700630 except ValueError as ex:
631 return HookFailure(msg, [example, str(ex)])
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700632 # Check that Cq-Depend is in the same paragraph as Change-Id.
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500633 msg = 'Cq-Depend is not in the same paragraph as Change-Id.'
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700634 paragraphs = desc.split('\n\n')
635 for paragraph in paragraphs:
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500636 if (re.search(r'^Cq-Depend:', paragraph, re.M) and not
637 re.search('^Change-Id:', paragraph, re.M)):
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700638 return HookFailure(msg)
Mike Frysingere39d0cd2019-11-25 13:30:06 -0500639
640 # We no longer support CQ-DEPEND= lines.
641 if re.search(r'^CQ-DEPEND[=:]', desc, re.M):
642 return HookFailure(
643 'CQ-DEPEND= is no longer supported. Please see:\n'
644 'https://chromium.googlesource.com/chromiumos/docs/+/HEAD/'
645 'contributing.md#CQ-DEPEND')
646
Mike Frysinger8cf80812019-09-16 23:49:29 -0400647 return None
David Jamesc3b68b32013-04-03 09:17:03 -0700648
649
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800650def _check_change_is_contribution(_project, commit):
651 """Check that the change is a contribution."""
652 NO_CONTRIB = 'not a contribution'
653 if NO_CONTRIB in _get_commit_desc(commit).lower():
654 msg = ('Changelist is not a contribution, this cannot be accepted.\n'
655 'Please remove the "%s" text from the commit message.') % NO_CONTRIB
656 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -0400657 return None
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800658
659
Alex Deymo643ac4c2015-09-03 10:40:50 -0700660def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700661 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700662 OLD_BUG_RE = r'\nBUG=.*chromium-os'
663 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
664 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
665 'the chromium tracker in your BUG= line now.')
666 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700667
Alex Deymo643ac4c2015-09-03 10:40:50 -0700668 # Android internal and external projects use "Bug: " to track bugs in
669 # buganizer.
670 BUG_COLON_REMOTES = (
671 'aosp',
672 'goog',
673 )
674 if project.remote in BUG_COLON_REMOTES:
675 BUG_RE = r'\nBug: ?([Nn]one|\d+)'
676 if not re.search(BUG_RE, _get_commit_desc(commit)):
677 msg = ('Changelist description needs BUG field (after first line):\n'
678 'Bug: 9999 (for buganizer)\n'
679 'BUG=None')
680 return HookFailure(msg)
681 else:
Jorge Lucangeli Obesdce214e2017-10-25 15:04:39 -0400682 BUG_RE = r'\nBUG=([Nn]one|(chromium|b):\d+)'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700683 if not re.search(BUG_RE, _get_commit_desc(commit)):
684 msg = ('Changelist description needs BUG field (after first line):\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700685 'BUG=chromium:9999 (for public tracker)\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700686 'BUG=b:9999 (for buganizer)\n'
687 'BUG=None')
688 return HookFailure(msg)
Ryan Cui1562fb82011-05-09 11:01:31 -0700689
Mike Frysinger8cf80812019-09-16 23:49:29 -0400690 return None
691
Ryan Cuiec4d6332011-05-02 14:15:25 -0700692
Jack Neus8edbf642019-07-10 16:08:31 -0600693def _check_change_no_include_oem(project, commit):
694 """Check that the change does not reference OEMs."""
695 ALLOWLIST = {
696 'chromiumos/platform/ec',
697 # Used by unit tests.
698 'project',
699 }
700 if project.name not in ALLOWLIST:
701 return None
702
Mike Frysingerbb34a222019-07-31 14:40:46 -0400703 TAGS = {
Jack Neus8edbf642019-07-10 16:08:31 -0600704 'Reviewed-on',
705 'Reviewed-by',
706 'Signed-off-by',
707 'Commit-Ready',
708 'Tested-by',
709 'Commit-Queue',
Jack Neus8edbf642019-07-10 16:08:31 -0600710 'Acked-by',
711 'Modified-by',
712 'CC',
713 'Suggested-by',
714 'Reported-by',
715 'Acked-for-chrome-by',
Mike Frysingerbb34a222019-07-31 14:40:46 -0400716 }
Jack Neus8edbf642019-07-10 16:08:31 -0600717
718 # Ignore tags, which could reasonably contain OEM names
719 # (e.g. Reviewed-by: foo@oem.corp-partner.google.com).
Jack Neus8edbf642019-07-10 16:08:31 -0600720 commit_message = ' '.join(
Mike Frysingerbb34a222019-07-31 14:40:46 -0400721 x for x in _get_commit_desc(commit).splitlines()
722 if ':' not in x or x.split(':', 1)[0] not in TAGS)
723
Jack Neus8edbf642019-07-10 16:08:31 -0600724 commit_message = re.sub(r'[\s_-]+', ' ', commit_message)
725
726 # Exercise caution when expanding these lists. Adding a name
727 # could indicate a new relationship with a company!
728 OEMS = ['hp', 'hewlett packard', 'dell', 'lenovo', 'acer', 'asus', 'samsung']
729 ODMS = [
730 'bitland', 'compal', 'haier', 'huaqin', 'inventec', 'lg', 'pegatron',
731 'pegatron(ems)', 'quanta', 'samsung', 'wistron'
732 ]
733
734 for name_type, name_list in [('OEM', OEMS), ('ODM', ODMS)]:
735 # Construct regex
736 name_re = r'\b(%s)\b' % '|'.join([re.escape(x) for x in name_list])
737 matches = [x[0] for x in re.findall(name_re, commit_message, re.IGNORECASE)]
Mike Frysingere52b1bc2019-09-16 23:45:41 -0400738 if matches:
Jack Neus8edbf642019-07-10 16:08:31 -0600739 # If there's a match, throw an error.
740 error_msg = ('Changelist description contains the name of an'
741 ' %s: "%s".' % (name_type, '","'.join(matches)))
742 return HookFailure(error_msg)
743
Mike Frysinger8cf80812019-09-16 23:49:29 -0400744 return None
745
Jack Neus8edbf642019-07-10 16:08:31 -0600746
Mike Frysinger292b45d2014-11-25 01:17:10 -0500747def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -0700748 """Check that we're not missing a revbump of an ebuild in the given commit.
749
750 If the given commit touches files in a directory that has ebuilds somewhere
751 up the directory hierarchy, it's very likely that we need an ebuild revbump
752 in order for those changes to take effect.
753
754 It's not totally trivial to detect a revbump, so at least detect that an
755 ebuild with a revision number in it was touched. This should handle the
756 common case where we use a symlink to do the revbump.
757
758 TODO: it would be nice to enhance this hook to:
759 * Handle cases where people revbump with a slightly different syntax. I see
760 one ebuild (puppy) that revbumps with _pN. This is a false positive.
761 * Catches cases where people aren't using symlinks for revbumps. If they
762 edit a revisioned file directly (and are expected to rename it for revbump)
763 we'll miss that. Perhaps we could detect that the file touched is a
764 symlink?
765
766 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
767 still better off than without this check.
768
769 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700770 project: The Project to look at
Doug Anderson42b8a052013-06-26 10:45:36 -0700771 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -0500772 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -0700773
774 Returns:
775 A HookFailure or None.
776 """
Mike Frysinger011af942014-01-17 16:12:22 -0500777 # If this is the portage-stable overlay, then ignore the check. It's rare
778 # that we're doing anything other than importing files from upstream, so
779 # forcing a rev bump makes no sense.
780 whitelist = (
781 'chromiumos/overlays/portage-stable',
782 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700783 if project.name in whitelist:
Mike Frysinger011af942014-01-17 16:12:22 -0500784 return None
785
Mike Frysinger292b45d2014-11-25 01:17:10 -0500786 def FinalName(obj):
787 # If the file is being deleted, then the dst_file is not set.
788 if obj.dst_file is None:
789 return obj.src_file
790 else:
791 return obj.dst_file
792
793 affected_path_objs = _get_affected_files(
794 commit, include_deletes=True, include_symlinks=True, relative=True,
795 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -0700796
797 # Don't yell about changes to whitelisted files...
Mike Frysinger550c05c2019-12-09 18:16:36 -0500798 whitelist = {'ChangeLog', 'Manifest', 'metadata.xml'}
Mike Frysinger292b45d2014-11-25 01:17:10 -0500799 affected_path_objs = [x for x in affected_path_objs
800 if os.path.basename(FinalName(x)) not in whitelist]
801 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -0700802 return None
803
804 # If we've touched any file named with a -rN.ebuild then we'll say we're
805 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500806 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
807 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700808 if touched_revved_ebuild:
809 return None
810
Mike Frysinger292b45d2014-11-25 01:17:10 -0500811 # If we're creating new ebuilds from scratch, then we don't need an uprev.
812 # Find all the dirs that new ebuilds and ignore their files/.
813 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
814 if FinalName(x).endswith('.ebuild') and x.status == 'A']
815 affected_path_objs = [obj for obj in affected_path_objs
816 if not any(FinalName(obj).startswith(x)
817 for x in ebuild_dirs)]
818 if not affected_path_objs:
Mike Frysinger8cf80812019-09-16 23:49:29 -0400819 return None
Mike Frysinger292b45d2014-11-25 01:17:10 -0500820
Doug Anderson42b8a052013-06-26 10:45:36 -0700821 # We want to examine the current contents of all directories that are parents
822 # of files that were touched (up to the top of the project).
823 #
824 # ...note: we use the current directory contents even though it may have
825 # changed since the commit we're looking at. This is just a heuristic after
826 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500827 if project_top is None:
828 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -0700829 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -0500830 for obj in affected_path_objs:
831 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -0700832 while os.path.exists(path) and not os.path.samefile(path, project_top):
833 dirs_to_check.add(path)
834 path = os.path.dirname(path)
835
836 # Look through each directory. If it's got an ebuild in it then we'll
837 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -0800838 affected_paths = set(os.path.join(project_top, FinalName(x))
839 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700840 for dir_path in dirs_to_check:
841 contents = os.listdir(dir_path)
842 ebuilds = [os.path.join(dir_path, path)
843 for path in contents if path.endswith('.ebuild')]
844 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
845
C Shapiroae157ae2017-09-18 16:24:03 -0600846 affected_paths_under_9999_ebuilds = set()
847 for affected_path in affected_paths:
848 for ebuild_9999 in ebuilds_9999:
849 ebuild_dir = os.path.dirname(ebuild_9999)
850 if affected_path.startswith(ebuild_dir):
851 affected_paths_under_9999_ebuilds.add(affected_path)
852
853 # If every file changed exists under a 9999 ebuild, then skip
854 if len(affected_paths_under_9999_ebuilds) == len(affected_paths):
855 continue
856
Doug Anderson42b8a052013-06-26 10:45:36 -0700857 # If the -9999.ebuild file was touched the bot will uprev for us.
858 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -0500859 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -0700860 continue
861
862 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500863 return HookFailure('Changelist probably needs a revbump of an ebuild, '
864 'or a -r1.ebuild symlink if this is a new ebuild:\n'
865 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -0700866
867 return None
868
869
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500870def _check_ebuild_eapi(project, commit):
Mike Frysinger948284a2018-02-01 15:22:56 -0500871 """Make sure we have people use EAPI=5 or newer with custom ebuilds.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500872
873 We want to get away from older EAPI's as it makes life confusing and they
874 have less builtin error checking.
875
876 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700877 project: The Project to look at
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500878 commit: The commit to look at
879
880 Returns:
881 A HookFailure or None.
882 """
883 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500884 # that we're doing anything other than importing files from upstream, and
885 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500886 whitelist = (
887 'chromiumos/overlays/portage-stable',
888 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700889 if project.name in whitelist:
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500890 return None
891
Mike Frysinger948284a2018-02-01 15:22:56 -0500892 BAD_EAPIS = ('0', '1', '2', '3', '4')
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500893
894 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
895
896 ebuilds_re = [r'\.ebuild$']
897 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
898 ebuilds_re)
899 bad_ebuilds = []
900
901 for ebuild in ebuilds:
902 # If the ebuild does not specify an EAPI, it defaults to 0.
903 eapi = '0'
904
905 lines = _get_file_content(ebuild, commit).splitlines()
906 if len(lines) == 1:
907 # This is most likely a symlink, so skip it entirely.
908 continue
909
910 for line in lines:
911 m = get_eapi.match(line)
912 if m:
913 # Once we hit the first EAPI line in this ebuild, stop processing.
914 # The spec requires that there only be one and it be first, so
915 # checking all possible values is pointless. We also assume that
916 # it's "the" EAPI line and not something in the middle of a heredoc.
917 eapi = m.group(1)
918 break
919
920 if eapi in BAD_EAPIS:
921 bad_ebuilds.append((ebuild, eapi))
922
923 if bad_ebuilds:
924 # pylint: disable=C0301
925 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
926 # pylint: enable=C0301
927 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500928 'These ebuilds are using old EAPIs. If these are imported from\n'
929 'Gentoo, then you may ignore and upload once with the --no-verify\n'
Mike Frysinger948284a2018-02-01 15:22:56 -0500930 'flag. Otherwise, please update to 5 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500931 '\t%s\n'
932 'See this guide for more details:\n%s\n' %
933 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
934
Mike Frysinger8cf80812019-09-16 23:49:29 -0400935 return None
936
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500937
Mike Frysinger89bdb852014-02-01 05:26:26 -0500938def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500939 """Make sure we use the new style KEYWORDS when possible in ebuilds.
940
941 If an ebuild generally does not care about the arch it is running on, then
942 ebuilds should flag it with one of:
943 KEYWORDS="*" # A stable ebuild.
944 KEYWORDS="~*" # An unstable ebuild.
945 KEYWORDS="-* ..." # Is known to only work on specific arches.
946
947 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700948 project: The Project to look at
Mike Frysingerc51ece72014-01-17 16:23:40 -0500949 commit: The commit to look at
950
951 Returns:
952 A HookFailure or None.
953 """
954 WHITELIST = set(('*', '-*', '~*'))
955
956 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
957
Mike Frysinger89bdb852014-02-01 05:26:26 -0500958 ebuilds_re = [r'\.ebuild$']
959 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
960 ebuilds_re)
961
Mike Frysinger8d42d742014-09-22 15:50:21 -0400962 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500963 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400964 # We get the full content rather than a diff as the latter does not work
965 # on new files (like when adding new ebuilds).
966 lines = _get_file_content(ebuild, commit).splitlines()
967 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500968 m = get_keywords.match(line)
969 if m:
970 keywords = set(m.group(1).split())
971 if not keywords or WHITELIST - keywords != WHITELIST:
972 continue
973
Mike Frysinger8d42d742014-09-22 15:50:21 -0400974 bad_ebuilds.append(ebuild)
975
976 if bad_ebuilds:
977 return HookFailure(
978 '%s\n'
979 'Please update KEYWORDS to use a glob:\n'
980 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
981 ' KEYWORDS="*"\n'
982 'If the ebuild should be marked unstable (normal for '
983 'cros-workon / 9999 ebuilds):\n'
984 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -0400985 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -0400986 'then use -* like so:\n'
987 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500988
Mike Frysinger8cf80812019-09-16 23:49:29 -0400989 return None
990
Mike Frysingerc51ece72014-01-17 16:23:40 -0500991
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800992def _check_ebuild_licenses(_project, commit):
993 """Check if the LICENSE field in the ebuild is correct."""
Brian Norris7a610e82016-02-17 12:24:54 -0800994 affected_paths = _get_affected_files(commit, relative=True)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800995 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
996
997 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800998 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800999
1000 for ebuild in touched_ebuilds:
1001 # Skip virutal packages.
1002 if ebuild.split('/')[-3] == 'virtual':
1003 continue
1004
Alex Kleinb5953522018-08-03 11:44:21 -06001005 # e.g. path/to/overlay/category/package/package.ebuild -> path/to/overlay
1006 overlay_path = os.sep.join(ebuild.split(os.sep)[:-3])
1007
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001008 try:
Brian Norris7a610e82016-02-17 12:24:54 -08001009 ebuild_content = _get_file_content(ebuild, commit)
Alex Kleinb5953522018-08-03 11:44:21 -06001010 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
1011 overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001012 except ValueError as e:
Mike Frysingerf1ee2bf2019-09-16 23:47:33 -04001013 return HookFailure(str(e), [ebuild])
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001014
1015 # Also ignore licenses ending with '?'
1016 for license_type in [x for x in license_types
1017 if x not in LICENSES_IGNORE and not x.endswith('?')]:
1018 try:
Alex Kleinb5953522018-08-03 11:44:21 -06001019 licenses_lib.Licensing.FindLicenseType(license_type,
1020 overlay_path=overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001021 except AssertionError as e:
Mike Frysingerf1ee2bf2019-09-16 23:47:33 -04001022 return HookFailure(str(e), [ebuild])
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001023
Mike Frysinger8cf80812019-09-16 23:49:29 -04001024 return None
1025
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001026
Mike Frysingercd363c82014-02-01 05:20:18 -05001027def _check_ebuild_virtual_pv(project, commit):
1028 """Enforce the virtual PV policies."""
1029 # If this is the portage-stable overlay, then ignore the check.
1030 # We want to import virtuals as-is from upstream Gentoo.
1031 whitelist = (
1032 'chromiumos/overlays/portage-stable',
1033 )
Alex Deymo643ac4c2015-09-03 10:40:50 -07001034 if project.name in whitelist:
Mike Frysingercd363c82014-02-01 05:20:18 -05001035 return None
1036
1037 # We assume the repo name is the same as the dir name on disk.
1038 # It would be dumb to not have them match though.
Alex Deymo643ac4c2015-09-03 10:40:50 -07001039 project_base = os.path.basename(project.name)
Mike Frysingercd363c82014-02-01 05:20:18 -05001040
1041 is_variant = lambda x: x.startswith('overlay-variant-')
1042 is_board = lambda x: x.startswith('overlay-')
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001043 is_baseboard = lambda x: x.startswith('baseboard-')
1044 is_chipset = lambda x: x.startswith('chipset-')
1045 is_project = lambda x: x.startswith('project-')
Mike Frysingercd363c82014-02-01 05:20:18 -05001046 is_private = lambda x: x.endswith('-private')
1047
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001048 is_special_overlay = lambda x: (is_board(x) or is_chipset(x) or
1049 is_baseboard(x) or is_project(x))
1050
Mike Frysingercd363c82014-02-01 05:20:18 -05001051 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
1052
1053 ebuilds_re = [r'\.ebuild$']
1054 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1055 ebuilds_re)
1056 bad_ebuilds = []
1057
1058 for ebuild in ebuilds:
1059 m = get_pv.match(ebuild)
1060 if m:
1061 overlay = m.group(1)
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001062 if not overlay or not is_special_overlay(overlay):
Alex Deymo643ac4c2015-09-03 10:40:50 -07001063 overlay = project_base
Mike Frysingercd363c82014-02-01 05:20:18 -05001064
1065 pv = m.group(3).split('-', 1)[0]
1066
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001067 # Virtual versions >= 4 are special cases used above the standard
1068 # versioning structure, e.g. if one has a board inheriting a board.
1069 if float(pv) >= 4:
1070 want_pv = pv
Mike Frysingercd363c82014-02-01 05:20:18 -05001071 elif is_board(overlay):
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001072 if is_private(overlay):
1073 want_pv = '3.5' if is_variant(overlay) else '3'
1074 elif is_board(overlay):
1075 want_pv = '2.5' if is_variant(overlay) else '2'
1076 elif is_baseboard(overlay):
1077 want_pv = '1.9'
1078 elif is_chipset(overlay):
1079 want_pv = '1.8'
1080 elif is_project(overlay):
1081 want_pv = '1.7' if is_private(overlay) else '1.5'
Mike Frysingercd363c82014-02-01 05:20:18 -05001082 else:
1083 want_pv = '1'
1084
1085 if pv != want_pv:
1086 bad_ebuilds.append((ebuild, pv, want_pv))
1087
1088 if bad_ebuilds:
1089 # pylint: disable=C0301
1090 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
1091 # pylint: enable=C0301
1092 return HookFailure(
1093 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
1094 '\t%s\n'
1095 'If this is an upstream Gentoo virtual, then you may ignore this\n'
1096 'check (and re-run w/--no-verify). Otherwise, please see this\n'
1097 'page for more details:\n%s\n' %
1098 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
1099 for x in bad_ebuilds]), url))
1100
Mike Frysinger8cf80812019-09-16 23:49:29 -04001101 return None
1102
Mike Frysingercd363c82014-02-01 05:20:18 -05001103
Daniel Erat9d203ff2015-02-17 10:12:21 -07001104def _check_portage_make_use_var(_project, commit):
1105 """Verify that $USE is set correctly in make.conf and make.defaults."""
1106 files = _filter_files(_get_affected_files(commit, relative=True),
1107 [r'(^|/)make.(conf|defaults)$'])
1108
1109 errors = []
1110 for path in files:
1111 basename = os.path.basename(path)
1112
1113 # Has a USE= line already been encountered in this file?
1114 saw_use = False
1115
1116 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
1117 if not line.startswith('USE='):
1118 continue
1119
1120 preserves_use = '${USE}' in line or '$USE' in line
1121
1122 if (basename == 'make.conf' or
1123 (basename == 'make.defaults' and saw_use)) and not preserves_use:
1124 errors.append('%s:%d: missing ${USE}' % (path, i))
1125 elif basename == 'make.defaults' and not saw_use and preserves_use:
1126 errors.append('%s:%d: ${USE} referenced in initial declaration' %
1127 (path, i))
1128
1129 saw_use = True
1130
1131 if errors:
1132 return HookFailure(
1133 'One or more Portage make files appear to set USE incorrectly.\n'
1134 '\n'
1135 'All USE assignments in make.conf and all assignments after the\n'
1136 'initial declaration in make.defaults should contain "${USE}" to\n'
1137 'preserve previously-set flags.\n'
1138 '\n'
1139 'The initial USE declaration in make.defaults should not contain\n'
1140 '"${USE}".\n',
1141 errors)
1142
Mike Frysinger8cf80812019-09-16 23:49:29 -04001143 return None
1144
Daniel Erat9d203ff2015-02-17 10:12:21 -07001145
Mike Frysingerae409522014-02-01 03:16:11 -05001146def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001147 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001148 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001149 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001150 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001151 if not m:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001152 return HookFailure('Last paragraph of description must include Change-Id.')
Ryan Cui1562fb82011-05-09 11:01:31 -07001153
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001154 # S-o-b tags always allowed to follow Change-ID.
1155 allowed_tags = ['Signed-off-by']
1156
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001157 end = desc[m.end():].strip().splitlines()
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001158 cherry_pick_marker = 'cherry picked from commit'
1159
1160 if end and cherry_pick_marker in end[-1]:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001161 # Cherry picked patches allow more tags in the last paragraph.
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001162 allowed_tags += ['Commit-Queue', 'Commit-Ready', 'Reviewed-by',
1163 'Reviewed-on', 'Tested-by']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001164 end = end[:-1]
1165
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001166 # Note that descriptions could have multiple cherry pick markers.
1167 tag_search = r'^(%s:|\(%s) ' % (':|'.join(allowed_tags), cherry_pick_marker)
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001168
1169 if [x for x in end if not re.search(tag_search, x)]:
1170 return HookFailure('Only "%s:" tag(s) may follow the Change-Id.' %
1171 ':", "'.join(allowed_tags))
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001172
Mike Frysinger8cf80812019-09-16 23:49:29 -04001173 return None
1174
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001175
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001176def _check_commit_message_style(_project, commit):
1177 """Verify that the commit message matches our style.
1178
1179 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
1180 commit hooks.
1181 """
Mike Frysinger4efdee72019-11-04 10:57:01 -05001182 DOC = ('https://chromium.googlesource.com/chromiumos/docs/+/HEAD/'
1183 'contributing.md#Commit-messages')
1184 SEE_ALSO = 'Please review the documentation:\n%s' % (DOC,)
1185
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001186 desc = _get_commit_desc(commit)
1187
1188 # The first line should be by itself.
1189 lines = desc.splitlines()
1190 if len(lines) > 1 and lines[1]:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001191 return HookFailure('The second line of the commit message must be blank.'
1192 '\n%s' % (SEE_ALSO,))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001193
1194 # The first line should be one sentence.
1195 if '. ' in lines[0]:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001196 return HookFailure('The first line cannot be more than one sentence.\n%s' %
1197 (SEE_ALSO,))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001198
1199 # The first line cannot be too long.
1200 MAX_FIRST_LINE_LEN = 100
1201 if len(lines[0]) > MAX_FIRST_LINE_LEN:
Mike Frysinger4efdee72019-11-04 10:57:01 -05001202 return HookFailure('The first line must be less than %i chars.\n%s' %
1203 (MAX_FIRST_LINE_LEN, SEE_ALSO))
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001204
Mike Frysinger8cf80812019-09-16 23:49:29 -04001205 return None
1206
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001207
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001208def _check_cros_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001209 """Verifies the Chromium OS license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -07001210
Mike Frysinger98638102014-08-28 00:15:08 -04001211 Should be following the spec:
1212 http://dev.chromium.org/developers/coding-style#TOC-File-headers
1213 """
1214 # For older years, be a bit more flexible as our policy says leave them be.
1215 LICENSE_HEADER = (
Keigo Oka7e880ac2019-07-03 15:03:43 +09001216 r'.*Copyright(?: \(c\))? (20[0-9]{2})(?:-20[0-9]{2})? The Chromium OS '
1217 r'Authors\. All rights reserved\.\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001218 r'.*Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001219 r'be\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001220 r'.*found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -05001221 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001222 )
1223 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1224
1225 # For newer years, be stricter.
Keigo Oka7e880ac2019-07-03 15:03:43 +09001226 BAD_COPYRIGHT_LINE = (
Brian Norris68838dd2018-09-26 18:30:24 -07001227 r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001228 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001229 )
Keigo Oka7e880ac2019-07-03 15:03:43 +09001230 bad_copyright_re = re.compile(BAD_COPYRIGHT_LINE)
Mike Frysinger98638102014-08-28 00:15:08 -04001231
Shuhei Takahashiabc20f32017-07-10 19:35:45 +09001232 included, excluded = _parse_common_inclusion_options(options)
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001233
Mike Frysinger98638102014-08-28 00:15:08 -04001234 bad_files = []
1235 bad_copyright_files = []
Keigo Oka7e880ac2019-07-03 15:03:43 +09001236 bad_year_files = []
1237
Ken Turnerd07564b2018-02-08 17:57:59 +11001238 files = _filter_files(
1239 _get_affected_files(commit, relative=True),
1240 included + COMMON_INCLUDED_PATHS,
1241 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001242 existing_files = set(_get_affected_files(commit, relative=True,
1243 include_adds=False))
Mike Frysinger98638102014-08-28 00:15:08 -04001244
Keigo Oka7e880ac2019-07-03 15:03:43 +09001245 current_year = str(datetime.datetime.now().year)
Mike Frysinger98638102014-08-28 00:15:08 -04001246 for f in files:
1247 contents = _get_file_content(f, commit)
1248 if not contents:
1249 # Ignore empty files.
1250 continue
1251
Keigo Oka7e880ac2019-07-03 15:03:43 +09001252 m = license_re.search(contents)
1253 if not m:
Mike Frysinger98638102014-08-28 00:15:08 -04001254 bad_files.append(f)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001255 elif bad_copyright_re.search(contents):
Mike Frysinger98638102014-08-28 00:15:08 -04001256 bad_copyright_files.append(f)
1257
Keigo Oka7e880ac2019-07-03 15:03:43 +09001258 if m and f not in existing_files:
1259 year = m.group(1)
1260 if year != current_year:
1261 bad_year_files.append(f)
1262
1263 errors = []
Mike Frysinger98638102014-08-28 00:15:08 -04001264 if bad_files:
1265 msg = '%s:\n%s\n%s' % (
1266 'License must match', license_re.pattern,
1267 'Found a bad header in these files:')
Keigo Oka7e880ac2019-07-03 15:03:43 +09001268 errors.append(HookFailure(msg, bad_files))
Mike Frysinger98638102014-08-28 00:15:08 -04001269 if bad_copyright_files:
1270 msg = 'Do not use (c) in copyright headers in new files:'
Keigo Oka7e880ac2019-07-03 15:03:43 +09001271 errors.append(HookFailure(msg, bad_copyright_files))
1272 if bad_year_files:
1273 msg = 'Use current year (%s) in copyright headers in new files:' % (
1274 current_year)
1275 errors.append(HookFailure(msg, bad_year_files))
Ryan Cuiec4d6332011-05-02 14:15:25 -07001276
Keigo Oka7e880ac2019-07-03 15:03:43 +09001277 return errors
Ryan Cuiec4d6332011-05-02 14:15:25 -07001278
Mike Frysinger8cf80812019-09-16 23:49:29 -04001279
Amin Hassani391efa92018-01-26 17:58:05 -08001280def _check_aosp_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001281 """Verifies the AOSP license/copyright header.
1282
1283 AOSP uses the Apache2 License:
1284 https://source.android.com/source/licenses.html
1285 """
1286 LICENSE_HEADER = (
1287 r"""^[#/\*]*
1288[#/\*]* ?Copyright( \([cC]\))? 20[-0-9]{2,7} The Android Open Source Project
1289[#/\*]* ?
1290[#/\*]* ?Licensed under the Apache License, Version 2.0 \(the "License"\);
1291[#/\*]* ?you may not use this file except in compliance with the License\.
1292[#/\*]* ?You may obtain a copy of the License at
1293[#/\*]* ?
1294[#/\*]* ? http://www\.apache\.org/licenses/LICENSE-2\.0
1295[#/\*]* ?
1296[#/\*]* ?Unless required by applicable law or agreed to in writing, software
1297[#/\*]* ?distributed under the License is distributed on an "AS IS" BASIS,
1298[#/\*]* ?WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or """
1299 r"""implied\.
1300[#/\*]* ?See the License for the specific language governing permissions and
1301[#/\*]* ?limitations under the License\.
1302[#/\*]*$
1303"""
1304 )
1305 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1306
Amin Hassani391efa92018-01-26 17:58:05 -08001307 included, excluded = _parse_common_inclusion_options(options)
1308
Ken Turnerd07564b2018-02-08 17:57:59 +11001309 files = _filter_files(
1310 _get_affected_files(commit, relative=True),
1311 included + COMMON_INCLUDED_PATHS,
1312 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Alex Deymof5792ce2015-08-24 22:50:08 -07001313
1314 bad_files = []
1315 for f in files:
1316 contents = _get_file_content(f, commit)
1317 if not contents:
1318 # Ignore empty files.
1319 continue
1320
1321 if not license_re.search(contents):
1322 bad_files.append(f)
1323
1324 if bad_files:
1325 msg = ('License must match:\n%s\nFound a bad header in these files:' %
1326 license_re.pattern)
1327 return HookFailure(msg, bad_files)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001328 return None
Alex Deymof5792ce2015-08-24 22:50:08 -07001329
1330
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001331def _check_layout_conf(_project, commit):
1332 """Verifies the metadata/layout.conf file."""
1333 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001334 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001335 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001336 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001337
Mike Frysinger94a670c2014-09-19 12:46:26 -04001338 # Handle multiple overlays in a single commit (like the public tree).
1339 for f in _get_affected_files(commit, relative=True):
1340 if f.endswith(repo_name):
1341 repo_names.append(f)
1342 elif f.endswith(layout_path):
1343 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001344
1345 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -04001346 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001347 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -04001348 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001349
Mike Frysinger94a670c2014-09-19 12:46:26 -04001350 # Gather all the errors in one pass so we show one full message.
1351 all_errors = {}
1352 for layout_path in layout_paths:
1353 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001354
Mike Frysinger94a670c2014-09-19 12:46:26 -04001355 # Make sure the config file is sorted.
1356 data = [x for x in _get_file_content(layout_path, commit).splitlines()
1357 if x and x[0] != '#']
1358 if sorted(data) != data:
1359 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001360
Mike Frysinger94a670c2014-09-19 12:46:26 -04001361 # Require people to set specific values all the time.
1362 settings = (
1363 # TODO: Enable this for everyone. http://crbug.com/408038
Shuhei Takahashi3cbb8dd2019-10-29 12:37:11 +09001364 # ('fast caching', 'cache-format = md5-dict'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001365 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -05001366 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
1367 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001368 )
1369 for reason, line in settings:
1370 if line not in data:
1371 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001372
Mike Frysinger94a670c2014-09-19 12:46:26 -04001373 # Require one of these settings.
Mike Frysinger5fae62d2015-11-11 20:12:15 -05001374 if 'use-manifests = strict' not in data:
1375 errors += ['enable file checking with: use-manifests = strict']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001376
Mike Frysinger94a670c2014-09-19 12:46:26 -04001377 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -04001378 for line in data:
1379 if line.startswith('repo-name = '):
1380 break
1381 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001382 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001383
Mike Frysinger94a670c2014-09-19 12:46:26 -04001384 # Summarize all the errors we saw (if any).
1385 lines = ''
1386 for layout_path, errors in all_errors.items():
1387 if errors:
1388 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1389 if lines:
1390 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1391 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001392
Mike Frysinger8cf80812019-09-16 23:49:29 -04001393 return None
1394
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001395
Keigo Oka4a09bd92019-05-07 14:01:00 +09001396def _check_no_new_gyp(_project, commit):
1397 """Verifies no project starts to use GYP."""
1398 whitelist = [
Keigo Oka4a09bd92019-05-07 14:01:00 +09001399 'chromeos/ap',
1400 'chromeos/ap-daemons',
Keigo Oka150a6fd2019-06-04 11:30:25 +09001401 'chromeos/ap/security',
1402 'chromeos/ap/wireless',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001403 'chromeos/platform/actions',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001404 'chromeos/platform/drivefs-google3',
1405 'chromeos/platform/experimental-touch-fw',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001406 'chromeos/thermald',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001407 'chromiumos/platform2',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001408 'weave/libweave',
1409 ]
1410 if _project.name in whitelist:
1411 return None
1412
1413 gypfiles = _filter_files(
1414 _get_affected_files(commit, include_symlinks=True, relative=True),
1415 [r'\.gyp$'])
1416
1417 if gypfiles:
1418 return HookFailure('GYP is deprecated and not allowed in a new project:',
1419 gypfiles)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001420 return None
Keigo Oka4a09bd92019-05-07 14:01:00 +09001421
1422
Ryan Cuiec4d6332011-05-02 14:15:25 -07001423# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001424
Ryan Cui1562fb82011-05-09 11:01:31 -07001425
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001426def _check_clang_format(_project, commit, options=()):
1427 """Runs clang-format on the given project"""
1428 hooks_dir = _get_hooks_dir()
1429 options = list(options)
1430 if commit == PRE_SUBMIT:
1431 options.append('--commit=HEAD')
1432 else:
1433 options.extend(['--commit', commit])
Brian Norris0c62a142018-12-11 13:24:29 -08001434 cmd = [os.path.join(hooks_dir, 'clang-format.py')] + options
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001435 cmd_result = cros_build_lib.run(cmd,
1436 print_cmd=False,
1437 stdout=True,
1438 encoding='utf-8',
1439 errors='replace',
1440 combine_stdout_stderr=True,
1441 check=False)
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001442 if cmd_result.returncode:
1443 return HookFailure('clang-format.py errors/warnings\n\n' +
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001444 cmd_result.stdout)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001445 return None
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001446
1447
Mike Frysingerae409522014-02-01 03:16:11 -05001448def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001449 """Runs checkpatch.pl on the given project"""
1450 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001451 options = list(options)
1452 if commit == PRE_SUBMIT:
1453 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1454 # this case.
1455 options.append('--ignore=MISSING_SIGN_OFF')
Filipe Brandenburger28d48d62015-10-07 09:48:54 -07001456 # Always ignore the check for the MAINTAINERS file. We do not track that
1457 # information on that file in our source trees, so let's suppress the
1458 # warning.
1459 options.append('--ignore=FILE_PATH_CHANGES')
Filipe Brandenburgerce978322015-10-09 10:04:15 -07001460 # Do not complain about the Change-Id: fields, since we use Gerrit.
1461 # Upstream does not want those lines (since they do not use Gerrit), but
1462 # we always do, so disable the check globally.
Daisuke Nojiri4844f892015-10-08 09:54:33 -07001463 options.append('--ignore=GERRIT_CHANGE_ID')
Brian Norris0c62a142018-12-11 13:24:29 -08001464 cmd = [os.path.join(hooks_dir, 'checkpatch.pl')] + options + ['-']
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001465 cmd_result = cros_build_lib.run(
1466 cmd, print_cmd=False, input=_get_patch(commit).encode('utf-8'),
1467 stdout=True, combine_stdout_stderr=True, check=False, encoding='utf-8',
1468 errors='replace')
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001469 if cmd_result.returncode:
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001470 return HookFailure('checkpatch.pl errors/warnings\n\n' + cmd_result.stdout)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001471 return None
Ryan Cuiec4d6332011-05-02 14:15:25 -07001472
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001473
Brian Norris23c62e92018-11-14 12:25:51 -08001474def _run_kerneldoc(_project, commit, options=()):
1475 """Runs kernel-doc validator on the given project"""
1476 included, excluded = _parse_common_inclusion_options(options)
1477 files = _filter_files(_get_affected_files(commit, relative=True),
1478 included, excluded)
1479 if files:
1480 hooks_dir = _get_hooks_dir()
Brian Norris0c62a142018-12-11 13:24:29 -08001481 cmd = [os.path.join(hooks_dir, 'kernel-doc'), '-none'] + files
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001482 output = _run_command(cmd, combine_stdout_stderr=True)
Brian Norris23c62e92018-11-14 12:25:51 -08001483 if output:
Brian Norris0c62a142018-12-11 13:24:29 -08001484 return HookFailure('kernel-doc errors/warnings:',
1485 items=output.splitlines())
Mike Frysinger8cf80812019-09-16 23:49:29 -04001486 return None
Brian Norris23c62e92018-11-14 12:25:51 -08001487
1488
Mike Frysingerae409522014-02-01 03:16:11 -05001489def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001490 """Makes sure kernel config changes are not mixed with code changes"""
1491 files = _get_affected_files(commit)
Mike Frysingerf252cfc2019-12-09 17:34:28 -05001492 if len(_filter_files(files, [r'chromeos/config'])) not in [0, len(files)]:
Olof Johanssona96810f2012-09-04 16:20:03 -07001493 return HookFailure('Changes to chromeos/config/ and regular files must '
1494 'be in separate commits:\n%s' % '\n'.join(files))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001495 return None
Anton Staaf815d6852011-08-22 10:08:45 -07001496
Mike Frysingerae409522014-02-01 03:16:11 -05001497
1498def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001499 """Checks that all JSON files are syntactically valid."""
Mike Frysinger908be682018-01-04 02:21:50 -05001500 ret = []
1501
1502 files = _filter_files(_get_affected_files(commit, relative=True),
1503 [r'.*\.json$'])
1504 for f in files:
1505 data = _get_file_content(f, commit)
Dale Curtis2975c432011-05-03 17:25:20 -07001506 try:
Mike Frysinger908be682018-01-04 02:21:50 -05001507 json.loads(data)
1508 except Exception as e:
1509 ret.append('%s: Invalid JSON: %s' % (f, e))
1510
1511 if ret:
1512 return HookFailure('\n'.join(ret))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001513 return None
Dale Curtis2975c432011-05-03 17:25:20 -07001514
1515
Mike Frysingerae409522014-02-01 03:16:11 -05001516def _check_manifests(_project, commit):
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001517 """Make sure Manifest files only have comments & DIST lines."""
1518 ret = []
Mike Frysinger52b537e2013-08-22 22:59:53 -04001519
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001520 manifests = _filter_files(_get_affected_files(commit, relative=True),
1521 [r'.*/Manifest$'])
1522 for path in manifests:
1523 data = _get_file_content(path, commit)
1524
1525 # Disallow blank files.
1526 if not data.strip():
1527 ret.append('%s: delete empty file' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001528 continue
1529
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001530 # Make sure the last newline isn't omitted.
1531 if data[-1] != '\n':
1532 ret.append('%s: missing trailing newline' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001533
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001534 # Do not allow leading or trailing blank lines.
1535 lines = data.splitlines()
1536 if not lines[0]:
1537 ret.append('%s: delete leading blank lines' % (path,))
1538 if not lines[-1]:
1539 ret.append('%s: delete trailing blank lines' % (path,))
1540
1541 for line in lines:
1542 # Disallow leading/trailing whitespace.
1543 if line != line.strip():
1544 ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
1545
1546 # Allow blank lines & comments.
1547 line = line.split('#', 1)[0]
1548 if not line:
1549 continue
1550
1551 # All other linse should start with DIST.
1552 if not line.startswith('DIST '):
1553 ret.append('%s: remove non-DIST lines: %s' % (path, line))
1554 break
1555
1556 if ret:
1557 return HookFailure('\n'.join(ret))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001558 return None
Mike Frysinger52b537e2013-08-22 22:59:53 -04001559
1560
Mike Frysingerae409522014-02-01 03:16:11 -05001561def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001562 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001563 if commit == PRE_SUBMIT:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001564 return None
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001565 BRANCH_RE = r'\nBRANCH=\S+'
1566
1567 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1568 msg = ('Changelist description needs BRANCH field (after first line)\n'
1569 'E.g. BRANCH=none or BRANCH=link,snow')
1570 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001571 return None
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001572
1573
Mike Frysinger45334bd2019-11-04 10:42:33 -05001574def _check_change_has_no_branch_field(_project, commit):
1575 """Verify 'BRANCH=' field does not exist in the commit message."""
1576 if commit == PRE_SUBMIT:
1577 return None
1578 BRANCH_RE = r'\nBRANCH=\S+'
1579
1580 if re.search(BRANCH_RE, _get_commit_desc(commit)):
1581 msg = 'This checkout does not use BRANCH= fields. Delete them.'
1582 return HookFailure(msg)
1583 return None
1584
1585
Mike Frysingerae409522014-02-01 03:16:11 -05001586def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001587 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001588 if commit == PRE_SUBMIT:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001589 return None
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001590 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1591
1592 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1593 msg = ('Changelist description needs Signed-off-by: field\n'
1594 'E.g. Signed-off-by: My Name <me@chromium.org>')
1595 return HookFailure(msg)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001596 return None
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001597
1598
Mike Frysinger9ab64b12019-11-04 10:53:08 -05001599def _check_change_has_no_signoff_field(_project, commit):
1600 """Verify 'Signed-off-by:' field does not exist in the commit message."""
1601 if commit == PRE_SUBMIT:
1602 return None
1603 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1604
1605 if re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1606 msg = 'This checkout does not use Signed-off-by: tags. Delete them.'
1607 return HookFailure(msg)
1608 return None
1609
1610
Jon Salz3ee59de2012-08-18 13:54:22 +08001611def _run_project_hook_script(script, project, commit):
1612 """Runs a project hook script.
1613
1614 The script is run with the following environment variables set:
1615 PRESUBMIT_PROJECT: The affected project
1616 PRESUBMIT_COMMIT: The affected commit
1617 PRESUBMIT_FILES: A newline-separated list of affected files
1618
1619 The script is considered to fail if the exit code is non-zero. It should
1620 write an error message to stdout.
1621 """
1622 env = dict(os.environ)
Alex Deymo643ac4c2015-09-03 10:40:50 -07001623 env['PRESUBMIT_PROJECT'] = project.name
Jon Salz3ee59de2012-08-18 13:54:22 +08001624 env['PRESUBMIT_COMMIT'] = commit
1625
1626 # Put affected files in an environment variable
1627 files = _get_affected_files(commit)
1628 env['PRESUBMIT_FILES'] = '\n'.join(files)
1629
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001630 cmd_result = cros_build_lib.run(cmd=script,
1631 env=env,
1632 shell=True,
1633 print_cmd=False,
1634 input=os.devnull,
1635 stdout=True,
1636 encoding='utf-8',
1637 errors='replace',
1638 combine_stdout_stderr=True,
1639 check=False)
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001640 if cmd_result.returncode:
Mike Frysinger7bb709f2019-09-29 23:20:12 -04001641 stdout = cmd_result.stdout
Jon Salz7b618af2012-08-31 06:03:16 +08001642 if stdout:
1643 stdout = re.sub('(?m)^', ' ', stdout)
1644 return HookFailure('Hook script "%s" failed with code %d%s' %
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001645 (script, cmd_result.returncode,
Jon Salz3ee59de2012-08-18 13:54:22 +08001646 ':\n' + stdout if stdout else ''))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001647 return None
Jon Salz3ee59de2012-08-18 13:54:22 +08001648
1649
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001650def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05001651 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001652
Brian Norris77608e12018-04-06 10:38:43 -07001653 files = _get_affected_files(commit, include_deletes=True, relative=True)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001654 prefix = os.path.commonprefix(files)
1655 prefix = os.path.dirname(prefix)
1656
1657 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07001658 if not prefix:
Mike Frysinger8cf80812019-09-16 23:49:29 -04001659 return None
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001660
1661 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07001662
1663 # The common files may all be within a subdirectory of the main project
1664 # directory, so walk up the tree until we find an alias file.
1665 # _get_affected_files() should return relative paths, but check against '/' to
1666 # ensure that this loop terminates even if it receives an absolute path.
1667 while prefix and prefix != '/':
1668 alias_file = os.path.join(prefix, '.project_alias')
1669
1670 # If an alias exists, use it.
1671 if os.path.isfile(alias_file):
1672 project_name = osutils.ReadFile(alias_file).strip()
1673
1674 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001675
1676 if not _get_commit_desc(commit).startswith(project_name + ': '):
1677 return HookFailure('The commit title for changes affecting only %s'
1678 ' should start with \"%s: \"'
1679 % (project_name, project_name))
Mike Frysinger8cf80812019-09-16 23:49:29 -04001680 return None
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001681
1682
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001683def _check_filepath_chartype(_project, commit):
1684 """Checks that FilePath::CharType stuff is not used."""
1685
1686 FILEPATH_REGEXP = re.compile('|'.join(
1687 [r'(?:base::)?FilePath::(?:Char|String|StringPiece)Type',
Satoru Takabayashi4ca37922018-08-08 10:16:38 +09001688 r'(?:base::)?FilePath::FromUTF8Unsafe',
1689 r'AsUTF8Unsafe',
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001690 r'FILE_PATH_LITERAL']))
1691 files = _filter_files(_get_affected_files(commit, relative=True),
1692 [r'.*\.(cc|h)$'])
1693
1694 errors = []
1695 for afile in files:
1696 for line_num, line in _get_file_diff(afile, commit):
1697 m = re.search(FILEPATH_REGEXP, line)
1698 if m:
1699 errors.append('%s, line %s has %s' % (afile, line_num, m.group(0)))
1700
1701 if errors:
1702 msg = 'Please assume FilePath::CharType is char (crbug.com/870621):'
1703 return HookFailure(msg, errors)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001704 return None
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001705
1706
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001707def _check_exec_files(_project, commit):
1708 """Make +x bits on files."""
1709 # List of files that should never be +x.
1710 NO_EXEC = (
1711 'ChangeLog*',
1712 'COPYING',
1713 'make.conf',
1714 'make.defaults',
1715 'Manifest',
1716 'OWNERS',
1717 'package.use',
1718 'package.keywords',
1719 'package.mask',
1720 'parent',
1721 'README',
1722 'TODO',
1723 '.gitignore',
1724 '*.[achly]',
1725 '*.[ch]xx',
1726 '*.boto',
1727 '*.cc',
1728 '*.cfg',
1729 '*.conf',
1730 '*.config',
1731 '*.cpp',
1732 '*.css',
1733 '*.ebuild',
1734 '*.eclass',
Tatsuhisa Yamaguchi3b053632019-01-10 14:45:14 +09001735 '*.gn',
1736 '*.gni',
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001737 '*.gyp',
1738 '*.gypi',
1739 '*.htm',
1740 '*.html',
1741 '*.ini',
1742 '*.js',
1743 '*.json',
1744 '*.md',
1745 '*.mk',
1746 '*.patch',
1747 '*.policy',
1748 '*.proto',
1749 '*.raw',
1750 '*.rules',
1751 '*.service',
1752 '*.target',
1753 '*.txt',
1754 '*.xml',
1755 '*.yaml',
1756 )
1757
1758 def FinalName(obj):
1759 # If the file is being deleted, then the dst_file is not set.
1760 if obj.dst_file is None:
1761 return obj.src_file
1762 else:
1763 return obj.dst_file
1764
1765 bad_files = []
1766 files = _get_affected_files(commit, relative=True, full_details=True)
1767 for f in files:
1768 mode = int(f.dst_mode, 8)
1769 if not mode & 0o111:
1770 continue
1771 name = FinalName(f)
1772 for no_exec in NO_EXEC:
1773 if fnmatch.fnmatch(name, no_exec):
1774 bad_files.append(name)
1775 break
1776
1777 if bad_files:
1778 return HookFailure('These files should not be executable. '
1779 'Please `chmod -x` them.', bad_files)
Mike Frysinger8cf80812019-09-16 23:49:29 -04001780 return None
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001781
1782
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001783# Base
1784
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001785# A list of hooks which are not project specific and check patch description
1786# (as opposed to patch body).
1787_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001788 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001789 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001790 _check_change_has_test_field,
1791 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001792 _check_commit_message_style,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001793 _check_change_is_contribution,
Jack Neus8edbf642019-07-10 16:08:31 -06001794 _check_change_no_include_oem,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001795]
1796
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001797# A list of hooks that are not project-specific
1798_COMMON_HOOKS = [
Aviv Keshet5ac59522017-01-31 14:28:27 -08001799 _check_cros_license,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001800 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001801 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001802 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001803 _check_ebuild_virtual_pv,
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001804 _check_exec_files,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001805 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07001806 _check_gofmt,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001807 _check_layout_conf,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001808 _check_no_long_lines,
Keigo Oka4a09bd92019-05-07 14:01:00 +09001809 _check_no_new_gyp,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001810 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07001811 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001812 _check_portage_make_use_var,
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -06001813 _check_rustfmt,
Aviv Keshet5ac59522017-01-31 14:28:27 -08001814 _check_tabbed_indents,
Ryan Cui9b651632011-05-11 11:38:58 -07001815]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001816
Ryan Cui1562fb82011-05-09 11:01:31 -07001817
Ryan Cui9b651632011-05-11 11:38:58 -07001818# A dictionary of project-specific hooks(callbacks), indexed by project name.
1819# dict[project] = [callback1, callback2]
1820_PROJECT_SPECIFIC_HOOKS = {
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001821 'chromiumos/third_party/kernel': [_kernel_configcheck],
1822 'chromiumos/third_party/kernel-next': [_kernel_configcheck],
Ryan Cui9b651632011-05-11 11:38:58 -07001823}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001824
Ryan Cui1562fb82011-05-09 11:01:31 -07001825
Ryan Cui9b651632011-05-11 11:38:58 -07001826# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04001827# that the flag controls (value).
1828_HOOK_FLAGS = {
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001829 'clang_format_check': _check_clang_format,
Mike Frysingera7642f52015-03-25 18:31:42 -04001830 'checkpatch_check': _run_checkpatch,
Brian Norris23c62e92018-11-14 12:25:51 -08001831 'kerneldoc_check': _run_kerneldoc,
Ryan Cui9b651632011-05-11 11:38:58 -07001832 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04001833 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07001834 'long_line_check': _check_no_long_lines,
Alex Deymof5792ce2015-08-24 22:50:08 -07001835 'cros_license_check': _check_cros_license,
1836 'aosp_license_check': _check_aosp_license,
Ryan Cui9b651632011-05-11 11:38:58 -07001837 'tab_check': _check_no_tabs,
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001838 'tabbed_indent_required_check': _check_tabbed_indents,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001839 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001840 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001841 'bug_field_check': _check_change_has_bug_field,
1842 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07001843 'manifest_check': _check_manifests,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001844 'contribution_check': _check_change_is_contribution,
Mike Frysinger47bd1252018-06-11 12:12:20 -04001845 'project_prefix_check': _check_project_prefix,
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001846 'filepath_chartype_check': _check_filepath_chartype,
Ryan Cui9b651632011-05-11 11:38:58 -07001847}
1848
1849
Mike Frysinger3554bc92015-03-11 04:59:21 -04001850def _get_override_hooks(config):
1851 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001852
1853 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001854
1855 Args:
1856 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001857 """
1858 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001859 SECTION_OPTIONS = 'Hook Overrides Options'
Jon Salz3ee59de2012-08-18 13:54:22 +08001860 if not config.has_section(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001861 return set(), set()
Ryan Cui9b651632011-05-11 11:38:58 -07001862
Mike Frysinger56e8de02019-07-31 14:40:14 -04001863 valid_keys = set(_HOOK_FLAGS.keys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001864 hooks = _HOOK_FLAGS.copy()
Mike Frysinger3554bc92015-03-11 04:59:21 -04001865
1866 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07001867 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001868 for flag in config.options(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001869 if flag not in valid_keys:
1870 raise ValueError('Error: unknown key "%s" in hook section of "%s"' %
1871 (flag, _CONFIG_FILE))
1872
Ryan Cui9b651632011-05-11 11:38:58 -07001873 try:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001874 enabled = config.getboolean(SECTION, flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001875 except ValueError as e:
Mike Frysinger3554bc92015-03-11 04:59:21 -04001876 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
1877 (flag, _CONFIG_FILE, e))
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001878 if enabled:
1879 enable_flags.append(flag)
1880 else:
1881 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001882
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001883 # See if this hook has custom options.
1884 if enabled:
1885 try:
1886 options = config.get(SECTION_OPTIONS, flag)
1887 hooks[flag] = functools.partial(hooks[flag], options=options.split())
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001888 hooks[flag].__name__ = flag
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001889 except (configparser.NoOptionError, configparser.NoSectionError):
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001890 pass
1891
1892 enabled_hooks = set(hooks[x] for x in enable_flags)
1893 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger45334bd2019-11-04 10:42:33 -05001894
Mike Frysinger9ab64b12019-11-04 10:53:08 -05001895 if _check_change_has_signoff_field not in enabled_hooks:
1896 enabled_hooks.add(_check_change_has_no_signoff_field)
Mike Frysinger45334bd2019-11-04 10:42:33 -05001897 if _check_change_has_branch_field not in enabled_hooks:
1898 enabled_hooks.add(_check_change_has_no_branch_field)
1899
Mike Frysinger3554bc92015-03-11 04:59:21 -04001900 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001901
1902
Jon Salz3ee59de2012-08-18 13:54:22 +08001903def _get_project_hook_scripts(config):
1904 """Returns a list of project-specific hook scripts.
1905
1906 Args:
1907 config: A ConfigParser for the project's config file.
1908 """
1909 SECTION = 'Hook Scripts'
1910 if not config.has_section(SECTION):
1911 return []
1912
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001913 return config.items(SECTION)
Jon Salz3ee59de2012-08-18 13:54:22 +08001914
1915
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001916def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001917 """Returns a list of hooks that need to be run for a project.
1918
1919 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001920
1921 Args:
1922 project: A string, name of the project.
1923 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001924 """
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001925 config = configparser.RawConfigParser()
Shuhei Takahashi3cbb8dd2019-10-29 12:37:11 +09001926 if not os.path.exists(_CONFIG_FILE):
Jon Salz3ee59de2012-08-18 13:54:22 +08001927 # Just use an empty config file
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001928 config = configparser.RawConfigParser()
Shuhei Takahashi3cbb8dd2019-10-29 12:37:11 +09001929 else:
1930 config.read(_CONFIG_FILE)
Jon Salz3ee59de2012-08-18 13:54:22 +08001931
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001932 if presubmit:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001933 hooks = _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001934 else:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001935 hooks = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001936
Mike Frysinger3554bc92015-03-11 04:59:21 -04001937 enabled_hooks, disabled_hooks = _get_override_hooks(config)
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001938 hooks = [hook for hook in hooks if hook not in disabled_hooks]
1939
1940 # If a list is both in _COMMON_HOOKS and also enabled explicitly through an
1941 # override, keep the override only. Note that the override may end up being
1942 # a functools.partial, in which case we need to extract the .func to compare
1943 # it to the common hooks.
1944 unwrapped_hooks = [getattr(hook, 'func', hook) for hook in enabled_hooks]
1945 hooks = [hook for hook in hooks if hook not in unwrapped_hooks]
1946
1947 hooks = list(enabled_hooks) + hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001948
1949 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001950 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1951 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001952
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001953 for name, script in _get_project_hook_scripts(config):
1954 func = functools.partial(_run_project_hook_script, script)
1955 func.__name__ = name
1956 hooks.append(func)
Jon Salz3ee59de2012-08-18 13:54:22 +08001957
Ryan Cui9b651632011-05-11 11:38:58 -07001958 return hooks
1959
1960
Alex Deymo643ac4c2015-09-03 10:40:50 -07001961def _run_project_hooks(project_name, proj_dir=None,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001962 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001963 """For each project run its project specific hook from the hooks dictionary.
1964
1965 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001966 project_name: The name of project to run hooks for.
Doug Anderson44a644f2011-11-02 10:37:37 -07001967 proj_dir: If non-None, this is the directory the project is in. If None,
1968 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001969 commit_list: A list of commits to run hooks against. If None or empty list
1970 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001971 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001972
1973 Returns:
1974 Boolean value of whether any errors were ecountered while running the hooks.
1975 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001976 if proj_dir is None:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001977 proj_dirs = _run_command(
1978 ['repo', 'forall', project_name, '-c', 'pwd']).split()
Mike Frysingere52b1bc2019-09-16 23:45:41 -04001979 if not proj_dirs:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001980 print('%s cannot be found.' % project_name, file=sys.stderr)
David James2edd9002013-10-11 14:09:19 -07001981 print('Please specify a valid project.', file=sys.stderr)
1982 return True
1983 if len(proj_dirs) > 1:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001984 print('%s is associated with multiple directories.' % project_name,
David James2edd9002013-10-11 14:09:19 -07001985 file=sys.stderr)
1986 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1987 return True
1988 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001989
Ryan Cuiec4d6332011-05-02 14:15:25 -07001990 pwd = os.getcwd()
1991 # hooks assume they are run from the root of the project
1992 os.chdir(proj_dir)
1993
Alex Deymo643ac4c2015-09-03 10:40:50 -07001994 remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
1995 '--symbolic-full-name', '@{u}']).strip()
1996 if not remote_branch:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001997 print("Your project %s doesn't track any remote repo." % project_name,
Alex Deymo643ac4c2015-09-03 10:40:50 -07001998 file=sys.stderr)
1999 remote = None
2000 else:
Josh Pratt15d13ab2018-08-13 11:52:48 +10002001 branch_items = remote_branch.split('/', 1)
2002 if len(branch_items) != 2:
2003 PrintErrorForProject(
2004 project_name,
2005 HookFailure(
2006 'Cannot get remote and branch name (%s)' % remote_branch))
2007 os.chdir(pwd)
2008 return True
2009 remote, _branch = branch_items
Alex Deymo643ac4c2015-09-03 10:40:50 -07002010
2011 project = Project(name=project_name, dir=proj_dir, remote=remote)
2012
Doug Anderson14749562013-06-26 13:38:29 -07002013 if not commit_list:
2014 try:
2015 commit_list = _get_commits()
2016 except VerifyException as e:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002017 PrintErrorForProject(project.name, HookFailure(str(e)))
Doug Anderson14749562013-06-26 13:38:29 -07002018 os.chdir(pwd)
2019 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07002020
Alex Deymo643ac4c2015-09-03 10:40:50 -07002021 hooks = _get_project_hooks(project.name, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07002022 error_found = False
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002023 commit_count = len(commit_list)
Mike Frysingerb99b3772019-08-17 14:19:44 -04002024 hook_count = len(hooks)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002025 for i, commit in enumerate(commit_list):
Mike Frysingerb2496652019-09-12 23:35:46 -04002026 CACHE.clear()
2027
Ryan Cui1562fb82011-05-09 11:01:31 -07002028 error_list = []
Mike Frysingerb99b3772019-08-17 14:19:44 -04002029 for h, hook in enumerate(hooks):
2030 output = ('PRESUBMIT.cfg: [%i/%i]: %s: Running [%i/%i] %s' %
Ben Chaneb806d82019-09-16 11:52:52 -07002031 (i + 1, commit_count, commit, h + 1, hook_count, hook.__name__))
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002032 print(output, end='\r')
2033 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07002034 hook_error = hook(project, commit)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002035 print(' ' * len(output), end='\r')
2036 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07002037 if hook_error:
Keigo Oka7e880ac2019-07-03 15:03:43 +09002038 if isinstance(hook_error, list):
2039 error_list.extend(hook_error)
2040 else:
2041 error_list.append(hook_error)
Ryan Cui1562fb82011-05-09 11:01:31 -07002042 error_found = True
2043 if error_list:
Alex Deymo643ac4c2015-09-03 10:40:50 -07002044 PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
Ryan Cui1562fb82011-05-09 11:01:31 -07002045 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07002046
Ryan Cuiec4d6332011-05-02 14:15:25 -07002047 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07002048 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002049
Mike Frysingerae409522014-02-01 03:16:11 -05002050
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002051# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002052
Ryan Cui1562fb82011-05-09 11:01:31 -07002053
Mike Frysingerae409522014-02-01 03:16:11 -05002054def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08002055 """Main function invoked directly by repo.
2056
2057 This function will exit directly upon error so that repo doesn't print some
2058 obscure error message.
2059
2060 Args:
2061 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07002062 worktree_list: A list of directories. It should be the same length as
2063 project_list, so that each entry in project_list matches with a directory
2064 in worktree_list. If None, we will attempt to calculate the directories
2065 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08002066 kwargs: Leave this here for forward-compatibility.
2067 """
Ryan Cui1562fb82011-05-09 11:01:31 -07002068 found_error = False
David James2edd9002013-10-11 14:09:19 -07002069 if not worktree_list:
2070 worktree_list = [None] * len(project_list)
2071 for project, worktree in zip(project_list, worktree_list):
2072 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07002073 found_error = True
2074
Mike Frysingerae409522014-02-01 03:16:11 -05002075 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07002076 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07002077 '- To disable some source style checks, and for other hints, see '
2078 '<checkout_dir>/src/repohooks/README\n'
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002079 "- To upload only current project, run 'repo upload .'")
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04002080 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07002081 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07002082
Ryan Cui1562fb82011-05-09 11:01:31 -07002083
Doug Anderson44a644f2011-11-02 10:37:37 -07002084def _identify_project(path):
2085 """Identify the repo project associated with the given path.
2086
2087 Returns:
2088 A string indicating what project is associated with the path passed in or
2089 a blank string upon failure.
2090 """
2091 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002092 stderr=True, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002093
2094
Mike Frysinger55f85b52014-12-18 14:45:21 -05002095def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07002096 """Run hooks directly (outside of the context of repo).
2097
Doug Anderson44a644f2011-11-02 10:37:37 -07002098 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05002099 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07002100
2101 Returns:
2102 0 if no pre-upload failures, 1 if failures.
2103
2104 Raises:
2105 BadInvocation: On some types of invocation errors.
2106 """
Mike Frysinger66142932014-12-18 14:55:57 -05002107 parser = commandline.ArgumentParser(description=__doc__)
2108 parser.add_argument('--dir', default=None,
2109 help='The directory that the project lives in. If not '
2110 'specified, use the git project root based on the cwd.')
2111 parser.add_argument('--project', default=None,
2112 help='The project repo path; this can affect how the '
2113 'hooks get run, since some hooks are project-specific. '
2114 'For chromite this is chromiumos/chromite. If not '
2115 'specified, the repo tool will be used to figure this '
2116 'out based on the dir.')
2117 parser.add_argument('--rerun-since', default=None,
Vadim Bendebury75447b92018-01-10 12:06:01 -08002118 help='Rerun hooks on old commits since some point '
2119 'in the past. The argument could be a date (should '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002120 "match git log's concept of a date, e.g. 2012-06-20), "
Vadim Bendebury75447b92018-01-10 12:06:01 -08002121 'or a SHA1, or just a number of commits to check (from 1 '
2122 'to 99). This option is mutually exclusive with '
2123 '--pre-submit.')
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002124 parser.add_argument('--pre-submit', action='store_true',
Mike Frysinger66142932014-12-18 14:55:57 -05002125 help='Run the check against the pending commit. '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002126 "This option should be used at the 'git commit' "
2127 "phase as opposed to 'repo upload'. This option "
Mike Frysinger66142932014-12-18 14:55:57 -05002128 'is mutually exclusive with --rerun-since.')
2129 parser.add_argument('commits', nargs='*',
2130 help='Check specific commits')
2131 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07002132
Doug Anderson14749562013-06-26 13:38:29 -07002133 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05002134 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002135 raise BadInvocation("Can't pass commits and use rerun-since: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002136 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07002137
Vadim Bendebury75447b92018-01-10 12:06:01 -08002138 if len(opts.rerun_since) < 3 and opts.rerun_since.isdigit():
2139 # This must be the number of commits to check. We don't expect the user
2140 # to want to check more than 99 commits.
2141 limit = '-n%s' % opts.rerun_since
2142 elif git.IsSHA1(opts.rerun_since, False):
2143 limit = '%s..' % opts.rerun_since
2144 else:
2145 # This better be a date.
2146 limit = '--since=%s' % opts.rerun_since
2147 cmd = ['git', 'log', limit, '--pretty=%H']
Doug Anderson14749562013-06-26 13:38:29 -07002148 all_commits = _run_command(cmd).splitlines()
2149 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
2150
2151 # Eliminate chrome-bot commits but keep ordering the same...
2152 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05002153 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07002154
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002155 if opts.pre_submit:
2156 raise BadInvocation('rerun-since and pre-submit can not be '
2157 'used together')
2158 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05002159 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002160 raise BadInvocation("Can't pass commits and use pre-submit: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002161 ' '.join(opts.commits))
2162 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07002163
2164 # Check/normlaize git dir; if unspecified, we'll use the root of the git
2165 # project from CWD
2166 if opts.dir is None:
2167 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
Mike Frysinger7bb709f2019-09-29 23:20:12 -04002168 stderr=True).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002169 if not git_dir:
2170 raise BadInvocation('The current directory is not part of a git project.')
2171 opts.dir = os.path.dirname(os.path.abspath(git_dir))
2172 elif not os.path.isdir(opts.dir):
2173 raise BadInvocation('Invalid dir: %s' % opts.dir)
2174 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
2175 raise BadInvocation('Not a git directory: %s' % opts.dir)
2176
2177 # Identify the project if it wasn't specified; this _requires_ the repo
2178 # tool to be installed and for the project to be part of a repo checkout.
2179 if not opts.project:
2180 opts.project = _identify_project(opts.dir)
2181 if not opts.project:
2182 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
2183
Doug Anderson14749562013-06-26 13:38:29 -07002184 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05002185 commit_list=opts.commits,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002186 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07002187 if found_error:
2188 return 1
2189 return 0
2190
2191
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002192if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05002193 sys.exit(direct_main(sys.argv[1:]))