blob: bbbac617475bb506b9be4117bbf2c9c8ab781c72 [file] [log] [blame]
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001#!/usr/bin/env python2
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002# -*- coding: utf-8 -*-
Jon Salz98255932012-08-18 14:48:02 +08003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Mike Frysingerae409522014-02-01 03:16:11 -05007"""Presubmit checks to run when doing `repo upload`.
8
9You can add new checks by adding a functions to the HOOKS constants.
10"""
11
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040012from __future__ import print_function
13
Keigo Okadd908822019-06-04 11:30:25 +090014import argparse
Alex Deymo643ac4c2015-09-03 10:40:50 -070015import collections
Keigo Oka7e880ac2019-07-03 15:03:43 +090016import datetime
Daniel Erate3ea3fc2015-02-13 15:27:52 -070017import fnmatch
Jon Salz3ee59de2012-08-18 13:54:22 +080018import functools
Mike Frysinger13302d42019-09-13 17:21:24 -040019import io
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
Mike Frysinger7bfc89f2019-09-13 15:45:51 -040026from six.moves import configparser
27
Ryan Cui1562fb82011-05-09 11:01:31 -070028from errors import (VerifyException, HookFailure, PrintErrorForProject,
29 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070030
Mike Frysinger6850d512018-05-21 12:12:14 -040031# If repo imports us, the __name__ will be __builtin__, and the cwd will be in
32# the top level of the checkout (i.e. $CHROMEOS_CHECKOUT). chromite will be in
33# that directory, so add it to our path. This works whether we're running the
34# repo in $CHROMEOS_CHECKOUT/.repo/repo/ or a custom version in a completely
35# different tree.
Mike Frysinger919c7032019-09-13 17:48:08 -040036# TODO(vapier): Python 2 used "__builtin__" while Python 3 uses "builtins".
37if __name__ in ('__builtin__', 'builtins'):
Mike Frysinger6850d512018-05-21 12:12:14 -040038 sys.path.insert(0, os.getcwd())
39
40# If we're run directly, we'll find chromite relative to the repohooks dir in
41# $CHROMEOS_CHECKOUT/src/repohooks, so go up two dirs.
42if __name__ == '__main__':
David Jamesc3b68b32013-04-03 09:17:03 -070043 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
44
Mike Frysinger66142932014-12-18 14:55:57 -050045from chromite.lib import commandline
Aviv Keshet5ac59522017-01-31 14:28:27 -080046from chromite.lib import constants
Rahul Chaudhry0e515342015-08-07 12:00:43 -070047from chromite.lib import cros_build_lib
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050048from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070049from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070050from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040051from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070052
Vadim Bendebury2b62d742014-06-22 13:14:51 -070053PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070054
55COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050056 # C++ and friends
Mike Frysinger24dd3c52019-08-17 14:22:48 -040057 r'.*\.c$', r'.*\.cc$', r'.*\.cpp$', r'.*\.h$', r'.*\.m$', r'.*\.mm$',
58 r'.*\.inl$', r'.*\.asm$', r'.*\.hxx$', r'.*\.hpp$', r'.*\.s$', r'.*\.S$',
Mike Frysingerae409522014-02-01 03:16:11 -050059 # Scripts
Mike Frysinger24dd3c52019-08-17 14:22:48 -040060 r'.*\.js$', r'.*\.py$', r'.*\.sh$', r'.*\.rb$', r'.*\.pl$', r'.*\.pm$',
Mike Frysingerae409522014-02-01 03:16:11 -050061 # No extension at all, note that ALL CAPS files are black listed in
62 # COMMON_EXCLUDED_LIST below.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040063 r'(^|.*[\\\/])[^.]+$',
Mike Frysingerae409522014-02-01 03:16:11 -050064 # Other
Mike Frysinger24dd3c52019-08-17 14:22:48 -040065 r'.*\.java$', r'.*\.mk$', r'.*\.am$',
66 r'.*\.policy$', r'.*\.conf$', r'.*\.go$',
67 r'(^OWNERS|/OWNERS)',
Ryan Cuiec4d6332011-05-02 14:15:25 -070068]
69
Ryan Cui1562fb82011-05-09 11:01:31 -070070
Ryan Cuiec4d6332011-05-02 14:15:25 -070071COMMON_EXCLUDED_PATHS = [
Daniel Erate3ea3fc2015-02-13 15:27:52 -070072 # For ebuild trees, ignore any caches and manifest data.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040073 r'.*/Manifest$',
74 r'.*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$',
Doug Anderson5bfb6792011-10-25 16:45:41 -070075
Daniel Erate3ea3fc2015-02-13 15:27:52 -070076 # Ignore profiles data (like overlay-tegra2/profiles).
Mike Frysinger24dd3c52019-08-17 14:22:48 -040077 r'(^|.*/)overlay-.*/profiles/.*',
78 r'^profiles/.*$',
Mike Frysinger98638102014-08-28 00:15:08 -040079
C Shapiro8f90e9b2017-06-28 09:54:50 -060080 # Ignore config files in ebuild setup.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040081 r'(^|.*/)overlay-.*/chromeos-base/chromeos-bsp.*/files/.*',
82 r'^chromeos-base/chromeos-bsp.*/files/.*',
C Shapiro8f90e9b2017-06-28 09:54:50 -060083
Daniel Erate3ea3fc2015-02-13 15:27:52 -070084 # Ignore minified js and jquery.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040085 r'.*\.min\.js',
86 r'.*jquery.*\.js',
Mike Frysinger33a458d2014-03-03 17:00:51 -050087
88 # Ignore license files as the content is often taken verbatim.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040089 r'.*/licenses/.*',
Alex Klein619c0912019-01-30 17:13:23 -070090
Mike Frysinger13650402019-07-31 14:31:46 -040091 # Exclude generated protobuf bindings.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040092 r'.*_pb2\.py$',
93 r'.*\.pb\.go$',
Ryan Cuiec4d6332011-05-02 14:15:25 -070094]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070095
Ken Turnerd07564b2018-02-08 17:57:59 +110096LICENSE_EXCLUDED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -040097 r'^(.*/)?OWNERS$',
Ken Turnerd07564b2018-02-08 17:57:59 +110098]
Ryan Cui1562fb82011-05-09 11:01:31 -070099
Ryan Cui9b651632011-05-11 11:38:58 -0700100_CONFIG_FILE = 'PRESUBMIT.cfg'
101
102
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700103# File containing wildcards, one per line, matching files that should be
104# excluded from presubmit checks. Lines beginning with '#' are ignored.
105_IGNORE_FILE = '.presubmitignore'
106
Doug Anderson44a644f2011-11-02 10:37:37 -0700107# Exceptions
108
109
110class BadInvocation(Exception):
111 """An Exception indicating a bad invocation of the program."""
112 pass
113
114
Ryan Cui1562fb82011-05-09 11:01:31 -0700115# General Helpers
116
Sean Paulba01d402011-05-05 11:36:23 -0400117
Mike Frysingerb2496652019-09-12 23:35:46 -0400118class Cache(object):
119 """General helper for caching git content."""
120
121 def __init__(self):
122 self._cache = {}
123
124 def get_subcache(self, scope):
125 return self._cache.setdefault(scope, {})
126
127 def clear(self):
128 self._cache.clear()
129
130CACHE = Cache()
131
132
Alex Deymo643ac4c2015-09-03 10:40:50 -0700133Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
134
135
Mike Frysinger526a5f82019-09-13 18:05:30 -0400136def _run_command(cmd, **kwargs):
Doug Anderson44a644f2011-11-02 10:37:37 -0700137 """Executes the passed in command and returns raw stdout output.
138
Mike Frysinger526a5f82019-09-13 18:05:30 -0400139 This is a convenience func to set some RunCommand defaults differently.
140
Doug Anderson44a644f2011-11-02 10:37:37 -0700141 Args:
142 cmd: The command to run; should be a list of strings.
Mike Frysinger526a5f82019-09-13 18:05:30 -0400143 **kwargs: Same as cros_build_lib.RunCommand.
Doug Anderson44a644f2011-11-02 10:37:37 -0700144
145 Returns:
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700146 The stdout from the process (discards stderr and returncode).
Doug Anderson44a644f2011-11-02 10:37:37 -0700147 """
Mike Frysinger526a5f82019-09-13 18:05:30 -0400148 kwargs.setdefault('print_cmd', False)
149 kwargs.setdefault('stdout_to_pipe', True)
150 kwargs.setdefault('error_code_ok', True)
Mike Frysinger71e643e2019-09-13 17:26:39 -0400151 result = cros_build_lib.RunCommand(cmd, **kwargs)
152 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:
432 cache[commit] = _run_command(['git', 'log', '--format=%s%n%n%b',
433 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)
458
459
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900460def _parse_common_inclusion_options(options):
461 """Parses common hook options for including/excluding files.
462
463 Args:
464 options: Option string list.
465
466 Returns:
467 (included, excluded) where each one is a list of regex strings.
468 """
469 parser = argparse.ArgumentParser()
470 parser.add_argument('--exclude_regex', action='append')
471 parser.add_argument('--include_regex', action='append')
472 opts = parser.parse_args(options)
473 included = opts.include_regex or []
474 excluded = opts.exclude_regex or []
475 return included, excluded
476
477
Ryan Cuiec4d6332011-05-02 14:15:25 -0700478# Common Hooks
479
Ryan Cui1562fb82011-05-09 11:01:31 -0700480
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900481def _check_no_long_lines(_project, commit, options=()):
Mike Frysinger55f85b52014-12-18 14:45:21 -0500482 """Checks there are no lines longer than MAX_LEN in any of the text files."""
Keigo Oka9732e382019-06-28 17:44:59 +0900483 LONG_LINE_OK_PATHS = [
484 # Go has no line length limit.
485 # https://golang.org/doc/effective_go.html#formatting
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400486 r'.*\.go$',
Keigo Oka9732e382019-06-28 17:44:59 +0900487 ]
Mike Frysinger55f85b52014-12-18 14:45:21 -0500488
Ryan Cuiec4d6332011-05-02 14:15:25 -0700489 MAX_LEN = 80
490
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900491 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700492 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900493 included + COMMON_INCLUDED_PATHS,
Keigo Oka9732e382019-06-28 17:44:59 +0900494 excluded + COMMON_EXCLUDED_PATHS + LONG_LINE_OK_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700495
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900496 errors = []
Ryan Cuiec4d6332011-05-02 14:15:25 -0700497 for afile in files:
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700498 skip_regexps = (
499 r'https?://',
500 r'^#\s*(define|include|import|pragma|if|ifndef|endif)\b',
501 )
502
503 if os.path.basename(afile).startswith('OWNERS'):
504 # File paths can get long, and there's no way to break them up into
505 # multiple lines.
506 skip_regexps += (
507 r'^include\b',
508 r'file:',
509 )
510
511 skip_regexps = [re.compile(x) for x in skip_regexps]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700512 for line_num, line in _get_file_diff(afile, commit):
513 # Allow certain lines to exceed the maxlen rule.
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700514 if len(line) <= MAX_LEN or any(x.search(line) for x in skip_regexps):
Jon Salz98255932012-08-18 14:48:02 +0800515 continue
516
517 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
518 if len(errors) == 5: # Just show the first 5 errors.
519 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700520
521 if errors:
522 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700523 return HookFailure(msg, errors)
524
Ryan Cuiec4d6332011-05-02 14:15:25 -0700525
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900526def _check_no_stray_whitespace(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700527 """Checks that there is no stray whitespace at source lines end."""
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900528 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700529 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900530 included + COMMON_INCLUDED_PATHS,
531 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800532 return _check_lines_in_diff(commit, files,
533 lambda line: line.rstrip() != line,
534 'Found line ending with white space in:')
Ryan Cui1562fb82011-05-09 11:01:31 -0700535
Ryan Cuiec4d6332011-05-02 14:15:25 -0700536
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900537def _check_no_tabs(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700538 """Checks there are no unexpanded tabs."""
Mike Frysingercd134512017-10-26 04:36:33 -0400539 # Don't add entire repos here. Update the PRESUBMIT.cfg in each repo instead.
540 # We only whitelist known specific filetypes here that show up in all repos.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700541 TAB_OK_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400542 r'.*\.ebuild$',
543 r'.*\.eclass$',
544 r'.*\.go$',
545 r'.*/[M|m]akefile$',
546 r'.*\.mk$',
Ryan Cuiec4d6332011-05-02 14:15:25 -0700547 ]
548
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900549 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700550 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900551 included + COMMON_INCLUDED_PATHS,
552 excluded + COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800553 return _check_lines_in_diff(commit, files,
554 lambda line: '\t' in line,
555 'Found a tab character in:')
Ryan Cuiec4d6332011-05-02 14:15:25 -0700556
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800557
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900558def _check_tabbed_indents(_project, commit, options=()):
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800559 """Checks that indents use tabs only."""
560 TABS_REQUIRED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400561 r'.*\.ebuild$',
562 r'.*\.eclass$',
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800563 ]
564 LEADING_SPACE_RE = re.compile('[\t]* ')
565
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900566 included, excluded = _parse_common_inclusion_options(options)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800567 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900568 included + TABS_REQUIRED_PATHS,
569 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800570 return _check_lines_in_diff(
571 commit, files,
572 lambda line: LEADING_SPACE_RE.match(line) is not None,
573 'Found a space in indentation (must be all tabs):')
Ryan Cui1562fb82011-05-09 11:01:31 -0700574
Ryan Cuiec4d6332011-05-02 14:15:25 -0700575
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700576def _check_gofmt(_project, commit):
577 """Checks that Go files are formatted with gofmt."""
578 errors = []
579 files = _filter_files(_get_affected_files(commit, relative=True),
580 [r'\.go$'])
581
582 for gofile in files:
583 contents = _get_file_content(gofile, commit)
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700584 output = _run_command(cmd=['gofmt', '-l'], input=contents,
585 combine_stdout_stderr=True)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700586 if output:
587 errors.append(gofile)
588 if errors:
589 return HookFailure('Files not formatted with gofmt:', errors)
590
591
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600592def _check_rustfmt(_project, commit):
593 """Checks that Rust files are formatted with rustfmt."""
594 errors = []
595 files = _filter_files(_get_affected_files(commit, relative=True),
596 [r'\.rs$'])
597
598 for rustfile in files:
599 contents = _get_file_content(rustfile, commit)
600 output = _run_command(cmd=['rustfmt'], input=contents,
601 combine_stdout_stderr=True)
602 if output != contents:
603 errors.append(rustfile)
604 if errors:
605 return HookFailure('Files not formatted with rustfmt: '
606 "(run 'cargo fmt' to fix)", errors)
607
608
Mike Frysingerae409522014-02-01 03:16:11 -0500609def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700610 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700611 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700612
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700613 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700614 msg = 'Changelist description needs TEST field (after first line)'
615 return HookFailure(msg)
616
Ryan Cuiec4d6332011-05-02 14:15:25 -0700617
Mike Frysingerae409522014-02-01 03:16:11 -0500618def _check_change_has_valid_cq_depend(_project, commit):
Jason D. Clinton299e3222019-05-23 09:42:03 -0600619 """Check for a correctly formatted Cq-Depend field in the commit message."""
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700620 desc = _get_commit_desc(commit)
Jason D. Clinton299e3222019-05-23 09:42:03 -0600621 msg = 'Changelist has invalid Cq-Depend target.'
622 example = 'Example: Cq-Depend: chromium:1234, chrome-internal:2345'
David Jamesc3b68b32013-04-03 09:17:03 -0700623 try:
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700624 patch.GetPaladinDeps(desc)
David Jamesc3b68b32013-04-03 09:17:03 -0700625 except ValueError as ex:
626 return HookFailure(msg, [example, str(ex)])
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700627 # Check that Cq-Depend is in the same paragraph as Change-Id.
628 msg = 'Cq-Depend (or CQ-DEPEND) is not in the same paragraph as Change-Id.'
629 paragraphs = desc.split('\n\n')
630 for paragraph in paragraphs:
631 if (re.search(r'^Cq-Depend:', paragraph, re.M) or
632 re.search(r'^CQ-DEPEND=', paragraph, re.M)) \
633 and not re.search('^Change-Id:', paragraph, re.M):
634 return HookFailure(msg)
David Jamesc3b68b32013-04-03 09:17:03 -0700635
636
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800637def _check_change_is_contribution(_project, commit):
638 """Check that the change is a contribution."""
639 NO_CONTRIB = 'not a contribution'
640 if NO_CONTRIB in _get_commit_desc(commit).lower():
641 msg = ('Changelist is not a contribution, this cannot be accepted.\n'
642 'Please remove the "%s" text from the commit message.') % NO_CONTRIB
643 return HookFailure(msg)
644
645
Alex Deymo643ac4c2015-09-03 10:40:50 -0700646def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700647 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700648 OLD_BUG_RE = r'\nBUG=.*chromium-os'
649 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
650 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
651 'the chromium tracker in your BUG= line now.')
652 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700653
Alex Deymo643ac4c2015-09-03 10:40:50 -0700654 # Android internal and external projects use "Bug: " to track bugs in
655 # buganizer.
656 BUG_COLON_REMOTES = (
657 'aosp',
658 'goog',
659 )
660 if project.remote in BUG_COLON_REMOTES:
661 BUG_RE = r'\nBug: ?([Nn]one|\d+)'
662 if not re.search(BUG_RE, _get_commit_desc(commit)):
663 msg = ('Changelist description needs BUG field (after first line):\n'
664 'Bug: 9999 (for buganizer)\n'
665 'BUG=None')
666 return HookFailure(msg)
667 else:
Jorge Lucangeli Obesdce214e2017-10-25 15:04:39 -0400668 BUG_RE = r'\nBUG=([Nn]one|(chromium|b):\d+)'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700669 if not re.search(BUG_RE, _get_commit_desc(commit)):
670 msg = ('Changelist description needs BUG field (after first line):\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700671 'BUG=chromium:9999 (for public tracker)\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700672 'BUG=b:9999 (for buganizer)\n'
673 'BUG=None')
674 return HookFailure(msg)
Ryan Cui1562fb82011-05-09 11:01:31 -0700675
Ryan Cuiec4d6332011-05-02 14:15:25 -0700676
Jack Neus8edbf642019-07-10 16:08:31 -0600677def _check_change_no_include_oem(project, commit):
678 """Check that the change does not reference OEMs."""
679 ALLOWLIST = {
680 'chromiumos/platform/ec',
681 # Used by unit tests.
682 'project',
683 }
684 if project.name not in ALLOWLIST:
685 return None
686
Mike Frysingerbb34a222019-07-31 14:40:46 -0400687 TAGS = {
Jack Neus8edbf642019-07-10 16:08:31 -0600688 'Reviewed-on',
689 'Reviewed-by',
690 'Signed-off-by',
691 'Commit-Ready',
692 'Tested-by',
693 'Commit-Queue',
694 'Legacy-Commit-Queue',
695 'Acked-by',
696 'Modified-by',
697 'CC',
698 'Suggested-by',
699 'Reported-by',
700 'Acked-for-chrome-by',
Mike Frysingerbb34a222019-07-31 14:40:46 -0400701 }
Jack Neus8edbf642019-07-10 16:08:31 -0600702
703 # Ignore tags, which could reasonably contain OEM names
704 # (e.g. Reviewed-by: foo@oem.corp-partner.google.com).
Jack Neus8edbf642019-07-10 16:08:31 -0600705 commit_message = ' '.join(
Mike Frysingerbb34a222019-07-31 14:40:46 -0400706 x for x in _get_commit_desc(commit).splitlines()
707 if ':' not in x or x.split(':', 1)[0] not in TAGS)
708
Jack Neus8edbf642019-07-10 16:08:31 -0600709 commit_message = re.sub(r'[\s_-]+', ' ', commit_message)
710
711 # Exercise caution when expanding these lists. Adding a name
712 # could indicate a new relationship with a company!
713 OEMS = ['hp', 'hewlett packard', 'dell', 'lenovo', 'acer', 'asus', 'samsung']
714 ODMS = [
715 'bitland', 'compal', 'haier', 'huaqin', 'inventec', 'lg', 'pegatron',
716 'pegatron(ems)', 'quanta', 'samsung', 'wistron'
717 ]
718
719 for name_type, name_list in [('OEM', OEMS), ('ODM', ODMS)]:
720 # Construct regex
721 name_re = r'\b(%s)\b' % '|'.join([re.escape(x) for x in name_list])
722 matches = [x[0] for x in re.findall(name_re, commit_message, re.IGNORECASE)]
723 if len(matches):
724 # If there's a match, throw an error.
725 error_msg = ('Changelist description contains the name of an'
726 ' %s: "%s".' % (name_type, '","'.join(matches)))
727 return HookFailure(error_msg)
728
729
Mike Frysinger292b45d2014-11-25 01:17:10 -0500730def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -0700731 """Check that we're not missing a revbump of an ebuild in the given commit.
732
733 If the given commit touches files in a directory that has ebuilds somewhere
734 up the directory hierarchy, it's very likely that we need an ebuild revbump
735 in order for those changes to take effect.
736
737 It's not totally trivial to detect a revbump, so at least detect that an
738 ebuild with a revision number in it was touched. This should handle the
739 common case where we use a symlink to do the revbump.
740
741 TODO: it would be nice to enhance this hook to:
742 * Handle cases where people revbump with a slightly different syntax. I see
743 one ebuild (puppy) that revbumps with _pN. This is a false positive.
744 * Catches cases where people aren't using symlinks for revbumps. If they
745 edit a revisioned file directly (and are expected to rename it for revbump)
746 we'll miss that. Perhaps we could detect that the file touched is a
747 symlink?
748
749 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
750 still better off than without this check.
751
752 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700753 project: The Project to look at
Doug Anderson42b8a052013-06-26 10:45:36 -0700754 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -0500755 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -0700756
757 Returns:
758 A HookFailure or None.
759 """
Mike Frysinger011af942014-01-17 16:12:22 -0500760 # If this is the portage-stable overlay, then ignore the check. It's rare
761 # that we're doing anything other than importing files from upstream, so
762 # forcing a rev bump makes no sense.
763 whitelist = (
764 'chromiumos/overlays/portage-stable',
765 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700766 if project.name in whitelist:
Mike Frysinger011af942014-01-17 16:12:22 -0500767 return None
768
Mike Frysinger292b45d2014-11-25 01:17:10 -0500769 def FinalName(obj):
770 # If the file is being deleted, then the dst_file is not set.
771 if obj.dst_file is None:
772 return obj.src_file
773 else:
774 return obj.dst_file
775
776 affected_path_objs = _get_affected_files(
777 commit, include_deletes=True, include_symlinks=True, relative=True,
778 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -0700779
780 # Don't yell about changes to whitelisted files...
Aviv Keshet272f2e52016-04-25 14:49:44 -0700781 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml', 'COMMIT-QUEUE.ini')
Mike Frysinger292b45d2014-11-25 01:17:10 -0500782 affected_path_objs = [x for x in affected_path_objs
783 if os.path.basename(FinalName(x)) not in whitelist]
784 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -0700785 return None
786
787 # If we've touched any file named with a -rN.ebuild then we'll say we're
788 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500789 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
790 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700791 if touched_revved_ebuild:
792 return None
793
Mike Frysinger292b45d2014-11-25 01:17:10 -0500794 # If we're creating new ebuilds from scratch, then we don't need an uprev.
795 # Find all the dirs that new ebuilds and ignore their files/.
796 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
797 if FinalName(x).endswith('.ebuild') and x.status == 'A']
798 affected_path_objs = [obj for obj in affected_path_objs
799 if not any(FinalName(obj).startswith(x)
800 for x in ebuild_dirs)]
801 if not affected_path_objs:
802 return
803
Doug Anderson42b8a052013-06-26 10:45:36 -0700804 # We want to examine the current contents of all directories that are parents
805 # of files that were touched (up to the top of the project).
806 #
807 # ...note: we use the current directory contents even though it may have
808 # changed since the commit we're looking at. This is just a heuristic after
809 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500810 if project_top is None:
811 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -0700812 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -0500813 for obj in affected_path_objs:
814 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -0700815 while os.path.exists(path) and not os.path.samefile(path, project_top):
816 dirs_to_check.add(path)
817 path = os.path.dirname(path)
818
819 # Look through each directory. If it's got an ebuild in it then we'll
820 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -0800821 affected_paths = set(os.path.join(project_top, FinalName(x))
822 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700823 for dir_path in dirs_to_check:
824 contents = os.listdir(dir_path)
825 ebuilds = [os.path.join(dir_path, path)
826 for path in contents if path.endswith('.ebuild')]
827 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
828
C Shapiroae157ae2017-09-18 16:24:03 -0600829 affected_paths_under_9999_ebuilds = set()
830 for affected_path in affected_paths:
831 for ebuild_9999 in ebuilds_9999:
832 ebuild_dir = os.path.dirname(ebuild_9999)
833 if affected_path.startswith(ebuild_dir):
834 affected_paths_under_9999_ebuilds.add(affected_path)
835
836 # If every file changed exists under a 9999 ebuild, then skip
837 if len(affected_paths_under_9999_ebuilds) == len(affected_paths):
838 continue
839
Doug Anderson42b8a052013-06-26 10:45:36 -0700840 # If the -9999.ebuild file was touched the bot will uprev for us.
841 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -0500842 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -0700843 continue
844
845 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500846 return HookFailure('Changelist probably needs a revbump of an ebuild, '
847 'or a -r1.ebuild symlink if this is a new ebuild:\n'
848 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -0700849
850 return None
851
852
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500853def _check_ebuild_eapi(project, commit):
Mike Frysinger948284a2018-02-01 15:22:56 -0500854 """Make sure we have people use EAPI=5 or newer with custom ebuilds.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500855
856 We want to get away from older EAPI's as it makes life confusing and they
857 have less builtin error checking.
858
859 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700860 project: The Project to look at
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500861 commit: The commit to look at
862
863 Returns:
864 A HookFailure or None.
865 """
866 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500867 # that we're doing anything other than importing files from upstream, and
868 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500869 whitelist = (
870 'chromiumos/overlays/portage-stable',
871 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700872 if project.name in whitelist:
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500873 return None
874
Mike Frysinger948284a2018-02-01 15:22:56 -0500875 BAD_EAPIS = ('0', '1', '2', '3', '4')
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500876
877 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
878
879 ebuilds_re = [r'\.ebuild$']
880 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
881 ebuilds_re)
882 bad_ebuilds = []
883
884 for ebuild in ebuilds:
885 # If the ebuild does not specify an EAPI, it defaults to 0.
886 eapi = '0'
887
888 lines = _get_file_content(ebuild, commit).splitlines()
889 if len(lines) == 1:
890 # This is most likely a symlink, so skip it entirely.
891 continue
892
893 for line in lines:
894 m = get_eapi.match(line)
895 if m:
896 # Once we hit the first EAPI line in this ebuild, stop processing.
897 # The spec requires that there only be one and it be first, so
898 # checking all possible values is pointless. We also assume that
899 # it's "the" EAPI line and not something in the middle of a heredoc.
900 eapi = m.group(1)
901 break
902
903 if eapi in BAD_EAPIS:
904 bad_ebuilds.append((ebuild, eapi))
905
906 if bad_ebuilds:
907 # pylint: disable=C0301
908 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
909 # pylint: enable=C0301
910 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500911 'These ebuilds are using old EAPIs. If these are imported from\n'
912 'Gentoo, then you may ignore and upload once with the --no-verify\n'
Mike Frysinger948284a2018-02-01 15:22:56 -0500913 'flag. Otherwise, please update to 5 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500914 '\t%s\n'
915 'See this guide for more details:\n%s\n' %
916 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
917
918
Mike Frysinger89bdb852014-02-01 05:26:26 -0500919def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500920 """Make sure we use the new style KEYWORDS when possible in ebuilds.
921
922 If an ebuild generally does not care about the arch it is running on, then
923 ebuilds should flag it with one of:
924 KEYWORDS="*" # A stable ebuild.
925 KEYWORDS="~*" # An unstable ebuild.
926 KEYWORDS="-* ..." # Is known to only work on specific arches.
927
928 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700929 project: The Project to look at
Mike Frysingerc51ece72014-01-17 16:23:40 -0500930 commit: The commit to look at
931
932 Returns:
933 A HookFailure or None.
934 """
935 WHITELIST = set(('*', '-*', '~*'))
936
937 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
938
Mike Frysinger89bdb852014-02-01 05:26:26 -0500939 ebuilds_re = [r'\.ebuild$']
940 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
941 ebuilds_re)
942
Mike Frysinger8d42d742014-09-22 15:50:21 -0400943 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500944 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400945 # We get the full content rather than a diff as the latter does not work
946 # on new files (like when adding new ebuilds).
947 lines = _get_file_content(ebuild, commit).splitlines()
948 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500949 m = get_keywords.match(line)
950 if m:
951 keywords = set(m.group(1).split())
952 if not keywords or WHITELIST - keywords != WHITELIST:
953 continue
954
Mike Frysinger8d42d742014-09-22 15:50:21 -0400955 bad_ebuilds.append(ebuild)
956
957 if bad_ebuilds:
958 return HookFailure(
959 '%s\n'
960 'Please update KEYWORDS to use a glob:\n'
961 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
962 ' KEYWORDS="*"\n'
963 'If the ebuild should be marked unstable (normal for '
964 'cros-workon / 9999 ebuilds):\n'
965 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -0400966 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -0400967 'then use -* like so:\n'
968 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500969
970
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800971def _check_ebuild_licenses(_project, commit):
972 """Check if the LICENSE field in the ebuild is correct."""
Brian Norris7a610e82016-02-17 12:24:54 -0800973 affected_paths = _get_affected_files(commit, relative=True)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800974 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
975
976 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800977 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800978
979 for ebuild in touched_ebuilds:
980 # Skip virutal packages.
981 if ebuild.split('/')[-3] == 'virtual':
982 continue
983
Alex Kleinb5953522018-08-03 11:44:21 -0600984 # e.g. path/to/overlay/category/package/package.ebuild -> path/to/overlay
985 overlay_path = os.sep.join(ebuild.split(os.sep)[:-3])
986
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800987 try:
Brian Norris7a610e82016-02-17 12:24:54 -0800988 ebuild_content = _get_file_content(ebuild, commit)
Alex Kleinb5953522018-08-03 11:44:21 -0600989 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
990 overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800991 except ValueError as e:
992 return HookFailure(e.message, [ebuild])
993
994 # Also ignore licenses ending with '?'
995 for license_type in [x for x in license_types
996 if x not in LICENSES_IGNORE and not x.endswith('?')]:
997 try:
Alex Kleinb5953522018-08-03 11:44:21 -0600998 licenses_lib.Licensing.FindLicenseType(license_type,
999 overlay_path=overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001000 except AssertionError as e:
1001 return HookFailure(e.message, [ebuild])
1002
1003
Mike Frysingercd363c82014-02-01 05:20:18 -05001004def _check_ebuild_virtual_pv(project, commit):
1005 """Enforce the virtual PV policies."""
1006 # If this is the portage-stable overlay, then ignore the check.
1007 # We want to import virtuals as-is from upstream Gentoo.
1008 whitelist = (
1009 'chromiumos/overlays/portage-stable',
1010 )
Alex Deymo643ac4c2015-09-03 10:40:50 -07001011 if project.name in whitelist:
Mike Frysingercd363c82014-02-01 05:20:18 -05001012 return None
1013
1014 # We assume the repo name is the same as the dir name on disk.
1015 # It would be dumb to not have them match though.
Alex Deymo643ac4c2015-09-03 10:40:50 -07001016 project_base = os.path.basename(project.name)
Mike Frysingercd363c82014-02-01 05:20:18 -05001017
1018 is_variant = lambda x: x.startswith('overlay-variant-')
1019 is_board = lambda x: x.startswith('overlay-')
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001020 is_baseboard = lambda x: x.startswith('baseboard-')
1021 is_chipset = lambda x: x.startswith('chipset-')
1022 is_project = lambda x: x.startswith('project-')
Mike Frysingercd363c82014-02-01 05:20:18 -05001023 is_private = lambda x: x.endswith('-private')
1024
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001025 is_special_overlay = lambda x: (is_board(x) or is_chipset(x) or
1026 is_baseboard(x) or is_project(x))
1027
Mike Frysingercd363c82014-02-01 05:20:18 -05001028 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
1029
1030 ebuilds_re = [r'\.ebuild$']
1031 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1032 ebuilds_re)
1033 bad_ebuilds = []
1034
1035 for ebuild in ebuilds:
1036 m = get_pv.match(ebuild)
1037 if m:
1038 overlay = m.group(1)
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001039 if not overlay or not is_special_overlay(overlay):
Alex Deymo643ac4c2015-09-03 10:40:50 -07001040 overlay = project_base
Mike Frysingercd363c82014-02-01 05:20:18 -05001041
1042 pv = m.group(3).split('-', 1)[0]
1043
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001044 # Virtual versions >= 4 are special cases used above the standard
1045 # versioning structure, e.g. if one has a board inheriting a board.
1046 if float(pv) >= 4:
1047 want_pv = pv
Mike Frysingercd363c82014-02-01 05:20:18 -05001048 elif is_board(overlay):
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001049 if is_private(overlay):
1050 want_pv = '3.5' if is_variant(overlay) else '3'
1051 elif is_board(overlay):
1052 want_pv = '2.5' if is_variant(overlay) else '2'
1053 elif is_baseboard(overlay):
1054 want_pv = '1.9'
1055 elif is_chipset(overlay):
1056 want_pv = '1.8'
1057 elif is_project(overlay):
1058 want_pv = '1.7' if is_private(overlay) else '1.5'
Mike Frysingercd363c82014-02-01 05:20:18 -05001059 else:
1060 want_pv = '1'
1061
1062 if pv != want_pv:
1063 bad_ebuilds.append((ebuild, pv, want_pv))
1064
1065 if bad_ebuilds:
1066 # pylint: disable=C0301
1067 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
1068 # pylint: enable=C0301
1069 return HookFailure(
1070 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
1071 '\t%s\n'
1072 'If this is an upstream Gentoo virtual, then you may ignore this\n'
1073 'check (and re-run w/--no-verify). Otherwise, please see this\n'
1074 'page for more details:\n%s\n' %
1075 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
1076 for x in bad_ebuilds]), url))
1077
1078
Daniel Erat9d203ff2015-02-17 10:12:21 -07001079def _check_portage_make_use_var(_project, commit):
1080 """Verify that $USE is set correctly in make.conf and make.defaults."""
1081 files = _filter_files(_get_affected_files(commit, relative=True),
1082 [r'(^|/)make.(conf|defaults)$'])
1083
1084 errors = []
1085 for path in files:
1086 basename = os.path.basename(path)
1087
1088 # Has a USE= line already been encountered in this file?
1089 saw_use = False
1090
1091 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
1092 if not line.startswith('USE='):
1093 continue
1094
1095 preserves_use = '${USE}' in line or '$USE' in line
1096
1097 if (basename == 'make.conf' or
1098 (basename == 'make.defaults' and saw_use)) and not preserves_use:
1099 errors.append('%s:%d: missing ${USE}' % (path, i))
1100 elif basename == 'make.defaults' and not saw_use and preserves_use:
1101 errors.append('%s:%d: ${USE} referenced in initial declaration' %
1102 (path, i))
1103
1104 saw_use = True
1105
1106 if errors:
1107 return HookFailure(
1108 'One or more Portage make files appear to set USE incorrectly.\n'
1109 '\n'
1110 'All USE assignments in make.conf and all assignments after the\n'
1111 'initial declaration in make.defaults should contain "${USE}" to\n'
1112 'preserve previously-set flags.\n'
1113 '\n'
1114 'The initial USE declaration in make.defaults should not contain\n'
1115 '"${USE}".\n',
1116 errors)
1117
1118
Mike Frysingerae409522014-02-01 03:16:11 -05001119def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001120 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001121 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001122 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001123 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001124 if not m:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001125 return HookFailure('Last paragraph of description must include Change-Id.')
Ryan Cui1562fb82011-05-09 11:01:31 -07001126
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001127 # S-o-b tags always allowed to follow Change-ID.
1128 allowed_tags = ['Signed-off-by']
1129
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001130 end = desc[m.end():].strip().splitlines()
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001131 cherry_pick_marker = 'cherry picked from commit'
1132
1133 if end and cherry_pick_marker in end[-1]:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001134 # Cherry picked patches allow more tags in the last paragraph.
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001135 allowed_tags += ['Commit-Queue', 'Commit-Ready', 'Reviewed-by',
1136 'Reviewed-on', 'Tested-by']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001137 end = end[:-1]
1138
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001139 # Note that descriptions could have multiple cherry pick markers.
1140 tag_search = r'^(%s:|\(%s) ' % (':|'.join(allowed_tags), cherry_pick_marker)
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001141
1142 if [x for x in end if not re.search(tag_search, x)]:
1143 return HookFailure('Only "%s:" tag(s) may follow the Change-Id.' %
1144 ':", "'.join(allowed_tags))
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001145
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001146
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001147def _check_commit_message_style(_project, commit):
1148 """Verify that the commit message matches our style.
1149
1150 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
1151 commit hooks.
1152 """
1153 desc = _get_commit_desc(commit)
1154
1155 # The first line should be by itself.
1156 lines = desc.splitlines()
1157 if len(lines) > 1 and lines[1]:
1158 return HookFailure('The second line of the commit message must be blank.')
1159
1160 # The first line should be one sentence.
1161 if '. ' in lines[0]:
1162 return HookFailure('The first line cannot be more than one sentence.')
1163
1164 # The first line cannot be too long.
1165 MAX_FIRST_LINE_LEN = 100
1166 if len(lines[0]) > MAX_FIRST_LINE_LEN:
1167 return HookFailure('The first line must be less than %i chars.' %
1168 MAX_FIRST_LINE_LEN)
1169
1170
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001171def _check_cros_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001172 """Verifies the Chromium OS license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -07001173
Mike Frysinger98638102014-08-28 00:15:08 -04001174 Should be following the spec:
1175 http://dev.chromium.org/developers/coding-style#TOC-File-headers
1176 """
1177 # For older years, be a bit more flexible as our policy says leave them be.
1178 LICENSE_HEADER = (
Keigo Oka7e880ac2019-07-03 15:03:43 +09001179 r'.*Copyright(?: \(c\))? (20[0-9]{2})(?:-20[0-9]{2})? The Chromium OS '
1180 r'Authors\. All rights reserved\.\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001181 r'.*Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001182 r'be\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001183 r'.*found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -05001184 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001185 )
1186 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1187
1188 # For newer years, be stricter.
Keigo Oka7e880ac2019-07-03 15:03:43 +09001189 BAD_COPYRIGHT_LINE = (
Brian Norris68838dd2018-09-26 18:30:24 -07001190 r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001191 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001192 )
Keigo Oka7e880ac2019-07-03 15:03:43 +09001193 bad_copyright_re = re.compile(BAD_COPYRIGHT_LINE)
Mike Frysinger98638102014-08-28 00:15:08 -04001194
Shuhei Takahashiabc20f32017-07-10 19:35:45 +09001195 included, excluded = _parse_common_inclusion_options(options)
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001196
Mike Frysinger98638102014-08-28 00:15:08 -04001197 bad_files = []
1198 bad_copyright_files = []
Keigo Oka7e880ac2019-07-03 15:03:43 +09001199 bad_year_files = []
1200
Ken Turnerd07564b2018-02-08 17:57:59 +11001201 files = _filter_files(
1202 _get_affected_files(commit, relative=True),
1203 included + COMMON_INCLUDED_PATHS,
1204 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001205 existing_files = set(_get_affected_files(commit, relative=True,
1206 include_adds=False))
Mike Frysinger98638102014-08-28 00:15:08 -04001207
Keigo Oka7e880ac2019-07-03 15:03:43 +09001208 current_year = str(datetime.datetime.now().year)
Mike Frysinger98638102014-08-28 00:15:08 -04001209 for f in files:
1210 contents = _get_file_content(f, commit)
1211 if not contents:
1212 # Ignore empty files.
1213 continue
1214
Keigo Oka7e880ac2019-07-03 15:03:43 +09001215 m = license_re.search(contents)
1216 if not m:
Mike Frysinger98638102014-08-28 00:15:08 -04001217 bad_files.append(f)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001218 elif bad_copyright_re.search(contents):
Mike Frysinger98638102014-08-28 00:15:08 -04001219 bad_copyright_files.append(f)
1220
Keigo Oka7e880ac2019-07-03 15:03:43 +09001221 if m and f not in existing_files:
1222 year = m.group(1)
1223 if year != current_year:
1224 bad_year_files.append(f)
1225
1226 errors = []
Mike Frysinger98638102014-08-28 00:15:08 -04001227 if bad_files:
1228 msg = '%s:\n%s\n%s' % (
1229 'License must match', license_re.pattern,
1230 'Found a bad header in these files:')
Keigo Oka7e880ac2019-07-03 15:03:43 +09001231 errors.append(HookFailure(msg, bad_files))
Mike Frysinger98638102014-08-28 00:15:08 -04001232 if bad_copyright_files:
1233 msg = 'Do not use (c) in copyright headers in new files:'
Keigo Oka7e880ac2019-07-03 15:03:43 +09001234 errors.append(HookFailure(msg, bad_copyright_files))
1235 if bad_year_files:
1236 msg = 'Use current year (%s) in copyright headers in new files:' % (
1237 current_year)
1238 errors.append(HookFailure(msg, bad_year_files))
Ryan Cuiec4d6332011-05-02 14:15:25 -07001239
Keigo Oka7e880ac2019-07-03 15:03:43 +09001240 return errors
Ryan Cuiec4d6332011-05-02 14:15:25 -07001241
Amin Hassani391efa92018-01-26 17:58:05 -08001242def _check_aosp_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001243 """Verifies the AOSP license/copyright header.
1244
1245 AOSP uses the Apache2 License:
1246 https://source.android.com/source/licenses.html
1247 """
1248 LICENSE_HEADER = (
1249 r"""^[#/\*]*
1250[#/\*]* ?Copyright( \([cC]\))? 20[-0-9]{2,7} The Android Open Source Project
1251[#/\*]* ?
1252[#/\*]* ?Licensed under the Apache License, Version 2.0 \(the "License"\);
1253[#/\*]* ?you may not use this file except in compliance with the License\.
1254[#/\*]* ?You may obtain a copy of the License at
1255[#/\*]* ?
1256[#/\*]* ? http://www\.apache\.org/licenses/LICENSE-2\.0
1257[#/\*]* ?
1258[#/\*]* ?Unless required by applicable law or agreed to in writing, software
1259[#/\*]* ?distributed under the License is distributed on an "AS IS" BASIS,
1260[#/\*]* ?WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or """
1261 r"""implied\.
1262[#/\*]* ?See the License for the specific language governing permissions and
1263[#/\*]* ?limitations under the License\.
1264[#/\*]*$
1265"""
1266 )
1267 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1268
Amin Hassani391efa92018-01-26 17:58:05 -08001269 included, excluded = _parse_common_inclusion_options(options)
1270
Ken Turnerd07564b2018-02-08 17:57:59 +11001271 files = _filter_files(
1272 _get_affected_files(commit, relative=True),
1273 included + COMMON_INCLUDED_PATHS,
1274 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Alex Deymof5792ce2015-08-24 22:50:08 -07001275
1276 bad_files = []
1277 for f in files:
1278 contents = _get_file_content(f, commit)
1279 if not contents:
1280 # Ignore empty files.
1281 continue
1282
1283 if not license_re.search(contents):
1284 bad_files.append(f)
1285
1286 if bad_files:
1287 msg = ('License must match:\n%s\nFound a bad header in these files:' %
1288 license_re.pattern)
1289 return HookFailure(msg, bad_files)
1290
1291
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001292def _check_layout_conf(_project, commit):
1293 """Verifies the metadata/layout.conf file."""
1294 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001295 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001296 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001297 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001298
Mike Frysinger94a670c2014-09-19 12:46:26 -04001299 # Handle multiple overlays in a single commit (like the public tree).
1300 for f in _get_affected_files(commit, relative=True):
1301 if f.endswith(repo_name):
1302 repo_names.append(f)
1303 elif f.endswith(layout_path):
1304 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001305
1306 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -04001307 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001308 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -04001309 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001310
Mike Frysinger94a670c2014-09-19 12:46:26 -04001311 # Gather all the errors in one pass so we show one full message.
1312 all_errors = {}
1313 for layout_path in layout_paths:
1314 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001315
Mike Frysinger94a670c2014-09-19 12:46:26 -04001316 # Make sure the config file is sorted.
1317 data = [x for x in _get_file_content(layout_path, commit).splitlines()
1318 if x and x[0] != '#']
1319 if sorted(data) != data:
1320 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001321
Mike Frysinger94a670c2014-09-19 12:46:26 -04001322 # Require people to set specific values all the time.
1323 settings = (
1324 # TODO: Enable this for everyone. http://crbug.com/408038
1325 #('fast caching', 'cache-format = md5-dict'),
1326 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -05001327 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
1328 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001329 )
1330 for reason, line in settings:
1331 if line not in data:
1332 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001333
Mike Frysinger94a670c2014-09-19 12:46:26 -04001334 # Require one of these settings.
Mike Frysinger5fae62d2015-11-11 20:12:15 -05001335 if 'use-manifests = strict' not in data:
1336 errors += ['enable file checking with: use-manifests = strict']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001337
Mike Frysinger94a670c2014-09-19 12:46:26 -04001338 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -04001339 for line in data:
1340 if line.startswith('repo-name = '):
1341 break
1342 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001343 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001344
Mike Frysinger94a670c2014-09-19 12:46:26 -04001345 # Summarize all the errors we saw (if any).
1346 lines = ''
1347 for layout_path, errors in all_errors.items():
1348 if errors:
1349 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1350 if lines:
1351 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1352 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001353
1354
Keigo Oka4a09bd92019-05-07 14:01:00 +09001355def _check_no_new_gyp(_project, commit):
1356 """Verifies no project starts to use GYP."""
1357 whitelist = [
Keigo Oka4a09bd92019-05-07 14:01:00 +09001358 'chromeos/ap',
1359 'chromeos/ap-daemons',
Keigo Oka150a6fd2019-06-04 11:30:25 +09001360 'chromeos/ap/security',
1361 'chromeos/ap/wireless',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001362 'chromeos/platform/actions',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001363 'chromeos/platform/drivefs-google3',
1364 'chromeos/platform/experimental-touch-fw',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001365 'chromeos/thermald',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001366 'chromiumos/platform2',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001367 'weave/libweave',
1368 ]
1369 if _project.name in whitelist:
1370 return None
1371
1372 gypfiles = _filter_files(
1373 _get_affected_files(commit, include_symlinks=True, relative=True),
1374 [r'\.gyp$'])
1375
1376 if gypfiles:
1377 return HookFailure('GYP is deprecated and not allowed in a new project:',
1378 gypfiles)
1379
1380
Ryan Cuiec4d6332011-05-02 14:15:25 -07001381# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001382
Ryan Cui1562fb82011-05-09 11:01:31 -07001383
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001384def _check_clang_format(_project, commit, options=()):
1385 """Runs clang-format on the given project"""
1386 hooks_dir = _get_hooks_dir()
1387 options = list(options)
1388 if commit == PRE_SUBMIT:
1389 options.append('--commit=HEAD')
1390 else:
1391 options.extend(['--commit', commit])
Brian Norris0c62a142018-12-11 13:24:29 -08001392 cmd = [os.path.join(hooks_dir, 'clang-format.py')] + options
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001393 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1394 print_cmd=False,
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001395 stdout_to_pipe=True,
1396 combine_stdout_stderr=True,
1397 error_code_ok=True)
1398 if cmd_result.returncode:
1399 return HookFailure('clang-format.py errors/warnings\n\n' +
1400 cmd_result.output)
1401
1402
Mike Frysingerae409522014-02-01 03:16:11 -05001403def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001404 """Runs checkpatch.pl on the given project"""
1405 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001406 options = list(options)
1407 if commit == PRE_SUBMIT:
1408 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1409 # this case.
1410 options.append('--ignore=MISSING_SIGN_OFF')
Filipe Brandenburger28d48d62015-10-07 09:48:54 -07001411 # Always ignore the check for the MAINTAINERS file. We do not track that
1412 # information on that file in our source trees, so let's suppress the
1413 # warning.
1414 options.append('--ignore=FILE_PATH_CHANGES')
Filipe Brandenburgerce978322015-10-09 10:04:15 -07001415 # Do not complain about the Change-Id: fields, since we use Gerrit.
1416 # Upstream does not want those lines (since they do not use Gerrit), but
1417 # we always do, so disable the check globally.
Daisuke Nojiri4844f892015-10-08 09:54:33 -07001418 options.append('--ignore=GERRIT_CHANGE_ID')
Brian Norris0c62a142018-12-11 13:24:29 -08001419 cmd = [os.path.join(hooks_dir, 'checkpatch.pl')] + options + ['-']
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001420 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1421 print_cmd=False,
1422 input=_get_patch(commit),
1423 stdout_to_pipe=True,
1424 combine_stdout_stderr=True,
1425 error_code_ok=True)
1426 if cmd_result.returncode:
1427 return HookFailure('checkpatch.pl errors/warnings\n\n' + cmd_result.output)
Ryan Cuiec4d6332011-05-02 14:15:25 -07001428
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001429
Brian Norris23c62e92018-11-14 12:25:51 -08001430def _run_kerneldoc(_project, commit, options=()):
1431 """Runs kernel-doc validator on the given project"""
1432 included, excluded = _parse_common_inclusion_options(options)
1433 files = _filter_files(_get_affected_files(commit, relative=True),
1434 included, excluded)
1435 if files:
1436 hooks_dir = _get_hooks_dir()
Brian Norris0c62a142018-12-11 13:24:29 -08001437 cmd = [os.path.join(hooks_dir, 'kernel-doc'), '-none'] + files
Brian Norris23c62e92018-11-14 12:25:51 -08001438 output = _run_command(cmd=cmd, combine_stdout_stderr=True)
1439 if output:
Brian Norris0c62a142018-12-11 13:24:29 -08001440 return HookFailure('kernel-doc errors/warnings:',
1441 items=output.splitlines())
Brian Norris23c62e92018-11-14 12:25:51 -08001442
1443
Mike Frysingerae409522014-02-01 03:16:11 -05001444def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001445 """Makes sure kernel config changes are not mixed with code changes"""
1446 files = _get_affected_files(commit)
1447 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
1448 return HookFailure('Changes to chromeos/config/ and regular files must '
1449 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -07001450
Mike Frysingerae409522014-02-01 03:16:11 -05001451
1452def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001453 """Checks that all JSON files are syntactically valid."""
Mike Frysinger908be682018-01-04 02:21:50 -05001454 ret = []
1455
1456 files = _filter_files(_get_affected_files(commit, relative=True),
1457 [r'.*\.json$'])
1458 for f in files:
1459 data = _get_file_content(f, commit)
Dale Curtis2975c432011-05-03 17:25:20 -07001460 try:
Mike Frysinger908be682018-01-04 02:21:50 -05001461 json.loads(data)
1462 except Exception as e:
1463 ret.append('%s: Invalid JSON: %s' % (f, e))
1464
1465 if ret:
1466 return HookFailure('\n'.join(ret))
Dale Curtis2975c432011-05-03 17:25:20 -07001467
1468
Mike Frysingerae409522014-02-01 03:16:11 -05001469def _check_manifests(_project, commit):
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001470 """Make sure Manifest files only have comments & DIST lines."""
1471 ret = []
Mike Frysinger52b537e2013-08-22 22:59:53 -04001472
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001473 manifests = _filter_files(_get_affected_files(commit, relative=True),
1474 [r'.*/Manifest$'])
1475 for path in manifests:
1476 data = _get_file_content(path, commit)
1477
1478 # Disallow blank files.
1479 if not data.strip():
1480 ret.append('%s: delete empty file' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001481 continue
1482
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001483 # Make sure the last newline isn't omitted.
1484 if data[-1] != '\n':
1485 ret.append('%s: missing trailing newline' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001486
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001487 # Do not allow leading or trailing blank lines.
1488 lines = data.splitlines()
1489 if not lines[0]:
1490 ret.append('%s: delete leading blank lines' % (path,))
1491 if not lines[-1]:
1492 ret.append('%s: delete trailing blank lines' % (path,))
1493
1494 for line in lines:
1495 # Disallow leading/trailing whitespace.
1496 if line != line.strip():
1497 ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
1498
1499 # Allow blank lines & comments.
1500 line = line.split('#', 1)[0]
1501 if not line:
1502 continue
1503
1504 # All other linse should start with DIST.
1505 if not line.startswith('DIST '):
1506 ret.append('%s: remove non-DIST lines: %s' % (path, line))
1507 break
1508
1509 if ret:
1510 return HookFailure('\n'.join(ret))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001511
1512
Mike Frysingerae409522014-02-01 03:16:11 -05001513def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001514 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001515 if commit == PRE_SUBMIT:
1516 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001517 BRANCH_RE = r'\nBRANCH=\S+'
1518
1519 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1520 msg = ('Changelist description needs BRANCH field (after first line)\n'
1521 'E.g. BRANCH=none or BRANCH=link,snow')
1522 return HookFailure(msg)
1523
1524
Mike Frysingerae409522014-02-01 03:16:11 -05001525def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001526 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001527 if commit == PRE_SUBMIT:
1528 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001529 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1530
1531 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1532 msg = ('Changelist description needs Signed-off-by: field\n'
1533 'E.g. Signed-off-by: My Name <me@chromium.org>')
1534 return HookFailure(msg)
1535
1536
Aviv Keshet5ac59522017-01-31 14:28:27 -08001537def _check_cq_ini_well_formed(_project, commit):
1538 """Check that any modified COMMIT-QUEUE.ini files are well formed."""
1539 pattern = '.*' + constants.CQ_CONFIG_FILENAME
Mike Frysingerd0523442018-01-03 17:05:29 -05001540 files = _filter_files(_get_affected_files(commit, relative=True), (pattern,))
Aviv Keshet5ac59522017-01-31 14:28:27 -08001541
1542 # TODO(akeshet): Check not only that the file is parseable, but that all the
1543 # pre-cq configs it requests are existing ones.
1544 for f in files:
1545 try:
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001546 parser = configparser.SafeConfigParser()
Aviv Keshet5ac59522017-01-31 14:28:27 -08001547 # Prior to python3, ConfigParser has no read_string method, so we must
1548 # pass it either a file path or file like object. And we must use
1549 # _get_file_content to fetch file contents to ensure we are examining the
1550 # commit diff, rather than whatever's on disk.
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001551 # TODO(vapier): Once we migrate this to Python 3 only, cut it over.
Aviv Keshet5ac59522017-01-31 14:28:27 -08001552 contents = _get_file_content(f, commit)
Mike Frysinger13302d42019-09-13 17:21:24 -04001553 parser.readfp(io.StringIO(contents))
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001554 except configparser.Error as e:
Aviv Keshet5ac59522017-01-31 14:28:27 -08001555 msg = ('Unable to parse COMMIT-QUEUE.ini file at %s due to %s.' %
1556 (f, e))
1557 return HookFailure(msg)
1558
1559
Jon Salz3ee59de2012-08-18 13:54:22 +08001560def _run_project_hook_script(script, project, commit):
1561 """Runs a project hook script.
1562
1563 The script is run with the following environment variables set:
1564 PRESUBMIT_PROJECT: The affected project
1565 PRESUBMIT_COMMIT: The affected commit
1566 PRESUBMIT_FILES: A newline-separated list of affected files
1567
1568 The script is considered to fail if the exit code is non-zero. It should
1569 write an error message to stdout.
1570 """
1571 env = dict(os.environ)
Alex Deymo643ac4c2015-09-03 10:40:50 -07001572 env['PRESUBMIT_PROJECT'] = project.name
Jon Salz3ee59de2012-08-18 13:54:22 +08001573 env['PRESUBMIT_COMMIT'] = commit
1574
1575 # Put affected files in an environment variable
1576 files = _get_affected_files(commit)
1577 env['PRESUBMIT_FILES'] = '\n'.join(files)
1578
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001579 cmd_result = cros_build_lib.RunCommand(cmd=script,
1580 env=env,
1581 shell=True,
1582 print_cmd=False,
1583 input=os.devnull,
1584 stdout_to_pipe=True,
1585 combine_stdout_stderr=True,
1586 error_code_ok=True)
1587 if cmd_result.returncode:
1588 stdout = cmd_result.output
Jon Salz7b618af2012-08-31 06:03:16 +08001589 if stdout:
1590 stdout = re.sub('(?m)^', ' ', stdout)
1591 return HookFailure('Hook script "%s" failed with code %d%s' %
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001592 (script, cmd_result.returncode,
Jon Salz3ee59de2012-08-18 13:54:22 +08001593 ':\n' + stdout if stdout else ''))
1594
1595
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001596def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05001597 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001598
Brian Norris77608e12018-04-06 10:38:43 -07001599 files = _get_affected_files(commit, include_deletes=True, relative=True)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001600 prefix = os.path.commonprefix(files)
1601 prefix = os.path.dirname(prefix)
1602
1603 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07001604 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001605 return
1606
1607 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07001608
1609 # The common files may all be within a subdirectory of the main project
1610 # directory, so walk up the tree until we find an alias file.
1611 # _get_affected_files() should return relative paths, but check against '/' to
1612 # ensure that this loop terminates even if it receives an absolute path.
1613 while prefix and prefix != '/':
1614 alias_file = os.path.join(prefix, '.project_alias')
1615
1616 # If an alias exists, use it.
1617 if os.path.isfile(alias_file):
1618 project_name = osutils.ReadFile(alias_file).strip()
1619
1620 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001621
1622 if not _get_commit_desc(commit).startswith(project_name + ': '):
1623 return HookFailure('The commit title for changes affecting only %s'
1624 ' should start with \"%s: \"'
1625 % (project_name, project_name))
1626
1627
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001628def _check_filepath_chartype(_project, commit):
1629 """Checks that FilePath::CharType stuff is not used."""
1630
1631 FILEPATH_REGEXP = re.compile('|'.join(
1632 [r'(?:base::)?FilePath::(?:Char|String|StringPiece)Type',
Satoru Takabayashi4ca37922018-08-08 10:16:38 +09001633 r'(?:base::)?FilePath::FromUTF8Unsafe',
1634 r'AsUTF8Unsafe',
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001635 r'FILE_PATH_LITERAL']))
1636 files = _filter_files(_get_affected_files(commit, relative=True),
1637 [r'.*\.(cc|h)$'])
1638
1639 errors = []
1640 for afile in files:
1641 for line_num, line in _get_file_diff(afile, commit):
1642 m = re.search(FILEPATH_REGEXP, line)
1643 if m:
1644 errors.append('%s, line %s has %s' % (afile, line_num, m.group(0)))
1645
1646 if errors:
1647 msg = 'Please assume FilePath::CharType is char (crbug.com/870621):'
1648 return HookFailure(msg, errors)
1649
1650
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001651def _check_exec_files(_project, commit):
1652 """Make +x bits on files."""
1653 # List of files that should never be +x.
1654 NO_EXEC = (
1655 'ChangeLog*',
1656 'COPYING',
1657 'make.conf',
1658 'make.defaults',
1659 'Manifest',
1660 'OWNERS',
1661 'package.use',
1662 'package.keywords',
1663 'package.mask',
1664 'parent',
1665 'README',
1666 'TODO',
1667 '.gitignore',
1668 '*.[achly]',
1669 '*.[ch]xx',
1670 '*.boto',
1671 '*.cc',
1672 '*.cfg',
1673 '*.conf',
1674 '*.config',
1675 '*.cpp',
1676 '*.css',
1677 '*.ebuild',
1678 '*.eclass',
Tatsuhisa Yamaguchi3b053632019-01-10 14:45:14 +09001679 '*.gn',
1680 '*.gni',
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001681 '*.gyp',
1682 '*.gypi',
1683 '*.htm',
1684 '*.html',
1685 '*.ini',
1686 '*.js',
1687 '*.json',
1688 '*.md',
1689 '*.mk',
1690 '*.patch',
1691 '*.policy',
1692 '*.proto',
1693 '*.raw',
1694 '*.rules',
1695 '*.service',
1696 '*.target',
1697 '*.txt',
1698 '*.xml',
1699 '*.yaml',
1700 )
1701
1702 def FinalName(obj):
1703 # If the file is being deleted, then the dst_file is not set.
1704 if obj.dst_file is None:
1705 return obj.src_file
1706 else:
1707 return obj.dst_file
1708
1709 bad_files = []
1710 files = _get_affected_files(commit, relative=True, full_details=True)
1711 for f in files:
1712 mode = int(f.dst_mode, 8)
1713 if not mode & 0o111:
1714 continue
1715 name = FinalName(f)
1716 for no_exec in NO_EXEC:
1717 if fnmatch.fnmatch(name, no_exec):
1718 bad_files.append(name)
1719 break
1720
1721 if bad_files:
1722 return HookFailure('These files should not be executable. '
1723 'Please `chmod -x` them.', bad_files)
1724
1725
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001726# Base
1727
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001728# A list of hooks which are not project specific and check patch description
1729# (as opposed to patch body).
1730_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001731 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001732 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001733 _check_change_has_test_field,
1734 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001735 _check_commit_message_style,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001736 _check_change_is_contribution,
Jack Neus8edbf642019-07-10 16:08:31 -06001737 _check_change_no_include_oem,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001738]
1739
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001740# A list of hooks that are not project-specific
1741_COMMON_HOOKS = [
Aviv Keshet5ac59522017-01-31 14:28:27 -08001742 _check_cq_ini_well_formed,
1743 _check_cros_license,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001744 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001745 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001746 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001747 _check_ebuild_virtual_pv,
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001748 _check_exec_files,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001749 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07001750 _check_gofmt,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001751 _check_layout_conf,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001752 _check_no_long_lines,
Keigo Oka4a09bd92019-05-07 14:01:00 +09001753 _check_no_new_gyp,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001754 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07001755 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001756 _check_portage_make_use_var,
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -06001757 _check_rustfmt,
Aviv Keshet5ac59522017-01-31 14:28:27 -08001758 _check_tabbed_indents,
Ryan Cui9b651632011-05-11 11:38:58 -07001759]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001760
Ryan Cui1562fb82011-05-09 11:01:31 -07001761
Ryan Cui9b651632011-05-11 11:38:58 -07001762# A dictionary of project-specific hooks(callbacks), indexed by project name.
1763# dict[project] = [callback1, callback2]
1764_PROJECT_SPECIFIC_HOOKS = {
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001765 'chromiumos/third_party/kernel': [_kernel_configcheck],
1766 'chromiumos/third_party/kernel-next': [_kernel_configcheck],
Ryan Cui9b651632011-05-11 11:38:58 -07001767}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001768
Ryan Cui1562fb82011-05-09 11:01:31 -07001769
Ryan Cui9b651632011-05-11 11:38:58 -07001770# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04001771# that the flag controls (value).
1772_HOOK_FLAGS = {
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001773 'clang_format_check': _check_clang_format,
Mike Frysingera7642f52015-03-25 18:31:42 -04001774 'checkpatch_check': _run_checkpatch,
Brian Norris23c62e92018-11-14 12:25:51 -08001775 'kerneldoc_check': _run_kerneldoc,
Ryan Cui9b651632011-05-11 11:38:58 -07001776 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04001777 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07001778 'long_line_check': _check_no_long_lines,
Alex Deymof5792ce2015-08-24 22:50:08 -07001779 'cros_license_check': _check_cros_license,
1780 'aosp_license_check': _check_aosp_license,
Ryan Cui9b651632011-05-11 11:38:58 -07001781 'tab_check': _check_no_tabs,
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001782 'tabbed_indent_required_check': _check_tabbed_indents,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001783 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001784 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001785 'bug_field_check': _check_change_has_bug_field,
1786 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07001787 'manifest_check': _check_manifests,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001788 'contribution_check': _check_change_is_contribution,
Mike Frysinger47bd1252018-06-11 12:12:20 -04001789 'project_prefix_check': _check_project_prefix,
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001790 'filepath_chartype_check': _check_filepath_chartype,
Ryan Cui9b651632011-05-11 11:38:58 -07001791}
1792
1793
Mike Frysinger3554bc92015-03-11 04:59:21 -04001794def _get_override_hooks(config):
1795 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001796
1797 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001798
1799 Args:
1800 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001801 """
1802 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001803 SECTION_OPTIONS = 'Hook Overrides Options'
Jon Salz3ee59de2012-08-18 13:54:22 +08001804 if not config.has_section(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001805 return set(), set()
Ryan Cui9b651632011-05-11 11:38:58 -07001806
Mike Frysinger56e8de02019-07-31 14:40:14 -04001807 valid_keys = set(_HOOK_FLAGS.keys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001808 hooks = _HOOK_FLAGS.copy()
Mike Frysinger3554bc92015-03-11 04:59:21 -04001809
1810 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07001811 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001812 for flag in config.options(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001813 if flag not in valid_keys:
1814 raise ValueError('Error: unknown key "%s" in hook section of "%s"' %
1815 (flag, _CONFIG_FILE))
1816
Ryan Cui9b651632011-05-11 11:38:58 -07001817 try:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001818 enabled = config.getboolean(SECTION, flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001819 except ValueError as e:
Mike Frysinger3554bc92015-03-11 04:59:21 -04001820 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
1821 (flag, _CONFIG_FILE, e))
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001822 if enabled:
1823 enable_flags.append(flag)
1824 else:
1825 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001826
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001827 # See if this hook has custom options.
1828 if enabled:
1829 try:
1830 options = config.get(SECTION_OPTIONS, flag)
1831 hooks[flag] = functools.partial(hooks[flag], options=options.split())
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001832 hooks[flag].__name__ = flag
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001833 except (configparser.NoOptionError, configparser.NoSectionError):
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001834 pass
1835
1836 enabled_hooks = set(hooks[x] for x in enable_flags)
1837 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger3554bc92015-03-11 04:59:21 -04001838 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001839
1840
Jon Salz3ee59de2012-08-18 13:54:22 +08001841def _get_project_hook_scripts(config):
1842 """Returns a list of project-specific hook scripts.
1843
1844 Args:
1845 config: A ConfigParser for the project's config file.
1846 """
1847 SECTION = 'Hook Scripts'
1848 if not config.has_section(SECTION):
1849 return []
1850
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001851 return config.items(SECTION)
Jon Salz3ee59de2012-08-18 13:54:22 +08001852
1853
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001854def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001855 """Returns a list of hooks that need to be run for a project.
1856
1857 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001858
1859 Args:
1860 project: A string, name of the project.
1861 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001862 """
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001863 config = configparser.RawConfigParser()
Jon Salz3ee59de2012-08-18 13:54:22 +08001864 try:
1865 config.read(_CONFIG_FILE)
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001866 except configparser.Error:
Jon Salz3ee59de2012-08-18 13:54:22 +08001867 # Just use an empty config file
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001868 config = configparser.RawConfigParser()
Jon Salz3ee59de2012-08-18 13:54:22 +08001869
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001870 if presubmit:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001871 hooks = _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001872 else:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001873 hooks = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001874
Mike Frysinger3554bc92015-03-11 04:59:21 -04001875 enabled_hooks, disabled_hooks = _get_override_hooks(config)
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001876 hooks = [hook for hook in hooks if hook not in disabled_hooks]
1877
1878 # If a list is both in _COMMON_HOOKS and also enabled explicitly through an
1879 # override, keep the override only. Note that the override may end up being
1880 # a functools.partial, in which case we need to extract the .func to compare
1881 # it to the common hooks.
1882 unwrapped_hooks = [getattr(hook, 'func', hook) for hook in enabled_hooks]
1883 hooks = [hook for hook in hooks if hook not in unwrapped_hooks]
1884
1885 hooks = list(enabled_hooks) + hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001886
1887 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001888 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1889 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001890
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001891 for name, script in _get_project_hook_scripts(config):
1892 func = functools.partial(_run_project_hook_script, script)
1893 func.__name__ = name
1894 hooks.append(func)
Jon Salz3ee59de2012-08-18 13:54:22 +08001895
Ryan Cui9b651632011-05-11 11:38:58 -07001896 return hooks
1897
1898
Alex Deymo643ac4c2015-09-03 10:40:50 -07001899def _run_project_hooks(project_name, proj_dir=None,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001900 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001901 """For each project run its project specific hook from the hooks dictionary.
1902
1903 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001904 project_name: The name of project to run hooks for.
Doug Anderson44a644f2011-11-02 10:37:37 -07001905 proj_dir: If non-None, this is the directory the project is in. If None,
1906 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001907 commit_list: A list of commits to run hooks against. If None or empty list
1908 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001909 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001910
1911 Returns:
1912 Boolean value of whether any errors were ecountered while running the hooks.
1913 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001914 if proj_dir is None:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001915 proj_dirs = _run_command(
1916 ['repo', 'forall', project_name, '-c', 'pwd']).split()
David James2edd9002013-10-11 14:09:19 -07001917 if len(proj_dirs) == 0:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001918 print('%s cannot be found.' % project_name, file=sys.stderr)
David James2edd9002013-10-11 14:09:19 -07001919 print('Please specify a valid project.', file=sys.stderr)
1920 return True
1921 if len(proj_dirs) > 1:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001922 print('%s is associated with multiple directories.' % project_name,
David James2edd9002013-10-11 14:09:19 -07001923 file=sys.stderr)
1924 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1925 return True
1926 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001927
Ryan Cuiec4d6332011-05-02 14:15:25 -07001928 pwd = os.getcwd()
1929 # hooks assume they are run from the root of the project
1930 os.chdir(proj_dir)
1931
Alex Deymo643ac4c2015-09-03 10:40:50 -07001932 remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
1933 '--symbolic-full-name', '@{u}']).strip()
1934 if not remote_branch:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001935 print("Your project %s doesn't track any remote repo." % project_name,
Alex Deymo643ac4c2015-09-03 10:40:50 -07001936 file=sys.stderr)
1937 remote = None
1938 else:
Josh Pratt15d13ab2018-08-13 11:52:48 +10001939 branch_items = remote_branch.split('/', 1)
1940 if len(branch_items) != 2:
1941 PrintErrorForProject(
1942 project_name,
1943 HookFailure(
1944 'Cannot get remote and branch name (%s)' % remote_branch))
1945 os.chdir(pwd)
1946 return True
1947 remote, _branch = branch_items
Alex Deymo643ac4c2015-09-03 10:40:50 -07001948
1949 project = Project(name=project_name, dir=proj_dir, remote=remote)
1950
Doug Anderson14749562013-06-26 13:38:29 -07001951 if not commit_list:
1952 try:
1953 commit_list = _get_commits()
1954 except VerifyException as e:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001955 PrintErrorForProject(project.name, HookFailure(str(e)))
Doug Anderson14749562013-06-26 13:38:29 -07001956 os.chdir(pwd)
1957 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001958
Alex Deymo643ac4c2015-09-03 10:40:50 -07001959 hooks = _get_project_hooks(project.name, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001960 error_found = False
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001961 commit_count = len(commit_list)
Mike Frysingerb99b3772019-08-17 14:19:44 -04001962 hook_count = len(hooks)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001963 for i, commit in enumerate(commit_list):
Mike Frysingerb2496652019-09-12 23:35:46 -04001964 CACHE.clear()
1965
Ryan Cui1562fb82011-05-09 11:01:31 -07001966 error_list = []
Mike Frysingerb99b3772019-08-17 14:19:44 -04001967 for h, hook in enumerate(hooks):
1968 output = ('PRESUBMIT.cfg: [%i/%i]: %s: Running [%i/%i] %s' %
Ben Chaneb806d82019-09-16 11:52:52 -07001969 (i + 1, commit_count, commit, h + 1, hook_count, hook.__name__))
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001970 print(output, end='\r')
1971 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001972 hook_error = hook(project, commit)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001973 print(' ' * len(output), end='\r')
1974 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001975 if hook_error:
Keigo Oka7e880ac2019-07-03 15:03:43 +09001976 if isinstance(hook_error, list):
1977 error_list.extend(hook_error)
1978 else:
1979 error_list.append(hook_error)
Ryan Cui1562fb82011-05-09 11:01:31 -07001980 error_found = True
1981 if error_list:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001982 PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
Ryan Cui1562fb82011-05-09 11:01:31 -07001983 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001984
Ryan Cuiec4d6332011-05-02 14:15:25 -07001985 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001986 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001987
Mike Frysingerae409522014-02-01 03:16:11 -05001988
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001989# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001990
Ryan Cui1562fb82011-05-09 11:01:31 -07001991
Mike Frysingerae409522014-02-01 03:16:11 -05001992def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001993 """Main function invoked directly by repo.
1994
1995 This function will exit directly upon error so that repo doesn't print some
1996 obscure error message.
1997
1998 Args:
1999 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07002000 worktree_list: A list of directories. It should be the same length as
2001 project_list, so that each entry in project_list matches with a directory
2002 in worktree_list. If None, we will attempt to calculate the directories
2003 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08002004 kwargs: Leave this here for forward-compatibility.
2005 """
Ryan Cui1562fb82011-05-09 11:01:31 -07002006 found_error = False
David James2edd9002013-10-11 14:09:19 -07002007 if not worktree_list:
2008 worktree_list = [None] * len(project_list)
2009 for project, worktree in zip(project_list, worktree_list):
2010 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07002011 found_error = True
2012
Mike Frysingerae409522014-02-01 03:16:11 -05002013 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07002014 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07002015 '- To disable some source style checks, and for other hints, see '
2016 '<checkout_dir>/src/repohooks/README\n'
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002017 "- To upload only current project, run 'repo upload .'")
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04002018 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07002019 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07002020
Ryan Cui1562fb82011-05-09 11:01:31 -07002021
Doug Anderson44a644f2011-11-02 10:37:37 -07002022def _identify_project(path):
2023 """Identify the repo project associated with the given path.
2024
2025 Returns:
2026 A string indicating what project is associated with the path passed in or
2027 a blank string upon failure.
2028 """
2029 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002030 redirect_stderr=True, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002031
2032
Mike Frysinger55f85b52014-12-18 14:45:21 -05002033def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07002034 """Run hooks directly (outside of the context of repo).
2035
Doug Anderson44a644f2011-11-02 10:37:37 -07002036 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05002037 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07002038
2039 Returns:
2040 0 if no pre-upload failures, 1 if failures.
2041
2042 Raises:
2043 BadInvocation: On some types of invocation errors.
2044 """
Mike Frysinger66142932014-12-18 14:55:57 -05002045 parser = commandline.ArgumentParser(description=__doc__)
2046 parser.add_argument('--dir', default=None,
2047 help='The directory that the project lives in. If not '
2048 'specified, use the git project root based on the cwd.')
2049 parser.add_argument('--project', default=None,
2050 help='The project repo path; this can affect how the '
2051 'hooks get run, since some hooks are project-specific. '
2052 'For chromite this is chromiumos/chromite. If not '
2053 'specified, the repo tool will be used to figure this '
2054 'out based on the dir.')
2055 parser.add_argument('--rerun-since', default=None,
Vadim Bendebury75447b92018-01-10 12:06:01 -08002056 help='Rerun hooks on old commits since some point '
2057 'in the past. The argument could be a date (should '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002058 "match git log's concept of a date, e.g. 2012-06-20), "
Vadim Bendebury75447b92018-01-10 12:06:01 -08002059 'or a SHA1, or just a number of commits to check (from 1 '
2060 'to 99). This option is mutually exclusive with '
2061 '--pre-submit.')
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002062 parser.add_argument('--pre-submit', action='store_true',
Mike Frysinger66142932014-12-18 14:55:57 -05002063 help='Run the check against the pending commit. '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002064 "This option should be used at the 'git commit' "
2065 "phase as opposed to 'repo upload'. This option "
Mike Frysinger66142932014-12-18 14:55:57 -05002066 'is mutually exclusive with --rerun-since.')
2067 parser.add_argument('commits', nargs='*',
2068 help='Check specific commits')
2069 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07002070
Doug Anderson14749562013-06-26 13:38:29 -07002071 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05002072 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002073 raise BadInvocation("Can't pass commits and use rerun-since: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002074 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07002075
Vadim Bendebury75447b92018-01-10 12:06:01 -08002076 if len(opts.rerun_since) < 3 and opts.rerun_since.isdigit():
2077 # This must be the number of commits to check. We don't expect the user
2078 # to want to check more than 99 commits.
2079 limit = '-n%s' % opts.rerun_since
2080 elif git.IsSHA1(opts.rerun_since, False):
2081 limit = '%s..' % opts.rerun_since
2082 else:
2083 # This better be a date.
2084 limit = '--since=%s' % opts.rerun_since
2085 cmd = ['git', 'log', limit, '--pretty=%H']
Doug Anderson14749562013-06-26 13:38:29 -07002086 all_commits = _run_command(cmd).splitlines()
2087 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
2088
2089 # Eliminate chrome-bot commits but keep ordering the same...
2090 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05002091 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07002092
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002093 if opts.pre_submit:
2094 raise BadInvocation('rerun-since and pre-submit can not be '
2095 'used together')
2096 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05002097 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002098 raise BadInvocation("Can't pass commits and use pre-submit: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002099 ' '.join(opts.commits))
2100 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07002101
2102 # Check/normlaize git dir; if unspecified, we'll use the root of the git
2103 # project from CWD
2104 if opts.dir is None:
2105 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002106 redirect_stderr=True).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002107 if not git_dir:
2108 raise BadInvocation('The current directory is not part of a git project.')
2109 opts.dir = os.path.dirname(os.path.abspath(git_dir))
2110 elif not os.path.isdir(opts.dir):
2111 raise BadInvocation('Invalid dir: %s' % opts.dir)
2112 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
2113 raise BadInvocation('Not a git directory: %s' % opts.dir)
2114
2115 # Identify the project if it wasn't specified; this _requires_ the repo
2116 # tool to be installed and for the project to be part of a repo checkout.
2117 if not opts.project:
2118 opts.project = _identify_project(opts.dir)
2119 if not opts.project:
2120 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
2121
Doug Anderson14749562013-06-26 13:38:29 -07002122 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05002123 commit_list=opts.commits,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002124 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07002125 if found_error:
2126 return 1
2127 return 0
2128
2129
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002130if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05002131 sys.exit(direct_main(sys.argv[1:]))