blob: d20381caebb4ef7505776e05b5de5d75bb80cf3d [file] [log] [blame]
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001#!/usr/bin/env python2
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002# -*- coding: utf-8 -*-
Jon Salz98255932012-08-18 14:48:02 +08003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Mike Frysingerae409522014-02-01 03:16:11 -05007"""Presubmit checks to run when doing `repo upload`.
8
9You can add new checks by adding a functions to the HOOKS constants.
10"""
11
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040012from __future__ import print_function
13
Keigo Okadd908822019-06-04 11:30:25 +090014import argparse
Alex Deymo643ac4c2015-09-03 10:40:50 -070015import collections
Ryan Cui9b651632011-05-11 11:38:58 -070016import ConfigParser
Keigo Oka7e880ac2019-07-03 15:03:43 +090017import datetime
Daniel Erate3ea3fc2015-02-13 15:27:52 -070018import fnmatch
Jon Salz3ee59de2012-08-18 13:54:22 +080019import functools
Dale Curtis2975c432011-05-03 17:25:20 -070020import json
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070021import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070022import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070023import sys
Peter Ammon811f6702014-06-12 15:45:38 -070024import stat
Aviv Keshet5ac59522017-01-31 14:28:27 -080025import StringIO
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070026
Ryan Cui1562fb82011-05-09 11:01:31 -070027from errors import (VerifyException, HookFailure, PrintErrorForProject,
28 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070029
Mike Frysinger6850d512018-05-21 12:12:14 -040030# If repo imports us, the __name__ will be __builtin__, and the cwd will be in
31# the top level of the checkout (i.e. $CHROMEOS_CHECKOUT). chromite will be in
32# that directory, so add it to our path. This works whether we're running the
33# repo in $CHROMEOS_CHECKOUT/.repo/repo/ or a custom version in a completely
34# different tree.
35if __name__ == '__builtin__':
36 sys.path.insert(0, os.getcwd())
37
38# If we're run directly, we'll find chromite relative to the repohooks dir in
39# $CHROMEOS_CHECKOUT/src/repohooks, so go up two dirs.
40if __name__ == '__main__':
David Jamesc3b68b32013-04-03 09:17:03 -070041 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
42
Mike Frysinger66142932014-12-18 14:55:57 -050043from chromite.lib import commandline
Aviv Keshet5ac59522017-01-31 14:28:27 -080044from chromite.lib import constants
Rahul Chaudhry0e515342015-08-07 12:00:43 -070045from chromite.lib import cros_build_lib
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050046from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070047from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070048from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040049from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070050
Vadim Bendebury2b62d742014-06-22 13:14:51 -070051PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070052
53COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050054 # C++ and friends
Mike Frysinger24dd3c52019-08-17 14:22:48 -040055 r'.*\.c$', r'.*\.cc$', r'.*\.cpp$', r'.*\.h$', r'.*\.m$', r'.*\.mm$',
56 r'.*\.inl$', r'.*\.asm$', r'.*\.hxx$', r'.*\.hpp$', r'.*\.s$', r'.*\.S$',
Mike Frysingerae409522014-02-01 03:16:11 -050057 # Scripts
Mike Frysinger24dd3c52019-08-17 14:22:48 -040058 r'.*\.js$', r'.*\.py$', r'.*\.sh$', r'.*\.rb$', r'.*\.pl$', r'.*\.pm$',
Mike Frysingerae409522014-02-01 03:16:11 -050059 # No extension at all, note that ALL CAPS files are black listed in
60 # COMMON_EXCLUDED_LIST below.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040061 r'(^|.*[\\\/])[^.]+$',
Mike Frysingerae409522014-02-01 03:16:11 -050062 # Other
Mike Frysinger24dd3c52019-08-17 14:22:48 -040063 r'.*\.java$', r'.*\.mk$', r'.*\.am$',
64 r'.*\.policy$', r'.*\.conf$', r'.*\.go$',
65 r'(^OWNERS|/OWNERS)',
Ryan Cuiec4d6332011-05-02 14:15:25 -070066]
67
Ryan Cui1562fb82011-05-09 11:01:31 -070068
Ryan Cuiec4d6332011-05-02 14:15:25 -070069COMMON_EXCLUDED_PATHS = [
Daniel Erate3ea3fc2015-02-13 15:27:52 -070070 # For ebuild trees, ignore any caches and manifest data.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040071 r'.*/Manifest$',
72 r'.*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$',
Doug Anderson5bfb6792011-10-25 16:45:41 -070073
Daniel Erate3ea3fc2015-02-13 15:27:52 -070074 # Ignore profiles data (like overlay-tegra2/profiles).
Mike Frysinger24dd3c52019-08-17 14:22:48 -040075 r'(^|.*/)overlay-.*/profiles/.*',
76 r'^profiles/.*$',
Mike Frysinger98638102014-08-28 00:15:08 -040077
C Shapiro8f90e9b2017-06-28 09:54:50 -060078 # Ignore config files in ebuild setup.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040079 r'(^|.*/)overlay-.*/chromeos-base/chromeos-bsp.*/files/.*',
80 r'^chromeos-base/chromeos-bsp.*/files/.*',
C Shapiro8f90e9b2017-06-28 09:54:50 -060081
Daniel Erate3ea3fc2015-02-13 15:27:52 -070082 # Ignore minified js and jquery.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040083 r'.*\.min\.js',
84 r'.*jquery.*\.js',
Mike Frysinger33a458d2014-03-03 17:00:51 -050085
86 # Ignore license files as the content is often taken verbatim.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040087 r'.*/licenses/.*',
Alex Klein619c0912019-01-30 17:13:23 -070088
Mike Frysinger13650402019-07-31 14:31:46 -040089 # Exclude generated protobuf bindings.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040090 r'.*_pb2\.py$',
91 r'.*\.pb\.go$',
Ryan Cuiec4d6332011-05-02 14:15:25 -070092]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070093
Ken Turnerd07564b2018-02-08 17:57:59 +110094LICENSE_EXCLUDED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -040095 r'^(.*/)?OWNERS$',
Ken Turnerd07564b2018-02-08 17:57:59 +110096]
Ryan Cui1562fb82011-05-09 11:01:31 -070097
Ryan Cui9b651632011-05-11 11:38:58 -070098_CONFIG_FILE = 'PRESUBMIT.cfg'
99
100
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700101# File containing wildcards, one per line, matching files that should be
102# excluded from presubmit checks. Lines beginning with '#' are ignored.
103_IGNORE_FILE = '.presubmitignore'
104
Doug Anderson44a644f2011-11-02 10:37:37 -0700105# Exceptions
106
107
108class BadInvocation(Exception):
109 """An Exception indicating a bad invocation of the program."""
110 pass
111
112
Ryan Cui1562fb82011-05-09 11:01:31 -0700113# General Helpers
114
Sean Paulba01d402011-05-05 11:36:23 -0400115
Mike Frysingerb2496652019-09-12 23:35:46 -0400116class Cache(object):
117 """General helper for caching git content."""
118
119 def __init__(self):
120 self._cache = {}
121
122 def get_subcache(self, scope):
123 return self._cache.setdefault(scope, {})
124
125 def clear(self):
126 self._cache.clear()
127
128CACHE = Cache()
129
130
Alex Deymo643ac4c2015-09-03 10:40:50 -0700131Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
132
133
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700134# pylint: disable=redefined-builtin
135def _run_command(cmd, cwd=None, input=None,
136 redirect_stderr=False, combine_stdout_stderr=False):
Doug Anderson44a644f2011-11-02 10:37:37 -0700137 """Executes the passed in command and returns raw stdout output.
138
139 Args:
140 cmd: The command to run; should be a list of strings.
141 cwd: The directory to switch to for running the command.
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700142 input: The data to pipe into this command through stdin. If a file object
143 or file descriptor, stdin will be connected directly to that.
144 redirect_stderr: Redirect stderr away from console.
145 combine_stdout_stderr: Combines stdout and stderr streams into stdout.
Doug Anderson44a644f2011-11-02 10:37:37 -0700146
147 Returns:
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700148 The stdout from the process (discards stderr and returncode).
Doug Anderson44a644f2011-11-02 10:37:37 -0700149 """
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700150 return cros_build_lib.RunCommand(cmd=cmd,
151 cwd=cwd,
152 print_cmd=False,
153 input=input,
154 stdout_to_pipe=True,
155 redirect_stderr=redirect_stderr,
156 combine_stdout_stderr=combine_stdout_stderr,
157 error_code_ok=True).output
158# pylint: enable=redefined-builtin
Ryan Cui72834d12011-05-05 14:51:33 -0700159
Ryan Cui1562fb82011-05-09 11:01:31 -0700160
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700161def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700162 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700163 if __name__ == '__main__':
164 # Works when file is run on its own (__file__ is defined)...
165 return os.path.abspath(os.path.dirname(__file__))
166 else:
167 # We need to do this when we're run through repo. Since repo executes
168 # us with execfile(), we don't get __file__ defined.
169 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
170 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700171
Ryan Cui1562fb82011-05-09 11:01:31 -0700172
Ryan Cuiec4d6332011-05-02 14:15:25 -0700173def _match_regex_list(subject, expressions):
174 """Try to match a list of regular expressions to a string.
175
176 Args:
177 subject: The string to match regexes on
178 expressions: A list of regular expressions to check for matches with.
179
180 Returns:
181 Whether the passed in subject matches any of the passed in regexes.
182 """
183 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500184 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700185 return True
186 return False
187
Ryan Cui1562fb82011-05-09 11:01:31 -0700188
Mike Frysingerae409522014-02-01 03:16:11 -0500189def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700190 """Filter out files based on the conditions passed in.
191
192 Args:
193 files: list of filepaths to filter
194 include_list: list of regex that when matched with a file path will cause it
195 to be added to the output list unless the file is also matched with a
196 regex in the exclude_list.
197 exclude_list: list of regex that when matched with a file will prevent it
198 from being added to the output list, even if it is also matched with a
199 regex in the include_list.
200
201 Returns:
202 A list of filepaths that contain files matched in the include_list and not
203 in the exclude_list.
204 """
205 filtered = []
206 for f in files:
207 if (_match_regex_list(f, include_list) and
208 not _match_regex_list(f, exclude_list)):
209 filtered.append(f)
210 return filtered
211
Ryan Cuiec4d6332011-05-02 14:15:25 -0700212
213# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700214
215
Ryan Cui4725d952011-05-05 15:41:19 -0700216def _get_upstream_branch():
217 """Returns the upstream tracking branch of the current branch.
218
219 Raises:
220 Error if there is no tracking branch
221 """
222 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
223 current_branch = current_branch.replace('refs/heads/', '')
224 if not current_branch:
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 cfg_option = 'branch.' + current_branch + '.%s'
228 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
229 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
230 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700231 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700232
233 return full_upstream.replace('heads', 'remotes/' + remote)
234
Ryan Cui1562fb82011-05-09 11:01:31 -0700235
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700236def _get_patch(commit):
237 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700238 if commit == PRE_SUBMIT:
239 return _run_command(['git', 'diff', '--cached', 'HEAD'])
240 else:
241 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700242
Ryan Cui1562fb82011-05-09 11:01:31 -0700243
Jon Salz98255932012-08-18 14:48:02 +0800244def _try_utf8_decode(data):
245 """Attempts to decode a string as UTF-8.
246
247 Returns:
248 The decoded Unicode object, or the original string if parsing fails.
249 """
250 try:
251 return unicode(data, 'utf-8', 'strict')
252 except UnicodeDecodeError:
253 return data
254
255
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500256def _get_file_content(path, commit):
257 """Returns the content of a file at a specific commit.
258
259 We can't rely on the file as it exists in the filesystem as people might be
260 uploading a series of changes which modifies the file multiple times.
261
262 Note: The "content" of a symlink is just the target. So if you're expecting
263 a full file, you should check that first. One way to detect is that the
264 content will not have any newlines.
265 """
Mike Frysingerb2496652019-09-12 23:35:46 -0400266 # Make sure people don't accidentally pass in full paths which will never
267 # work. You need to use relative=True with _get_affected_files.
268 if path.startswith('/'):
269 raise ValueError('_get_file_content must be called with relative paths: %s'
270 % (path,))
271
272 # {<commit>: {<path1>: <content>, <path2>: <content>}}
273 cache = CACHE.get_subcache('get_file_content')
274 if path in cache:
275 return cache[path]
276
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700277 if commit == PRE_SUBMIT:
Mike Frysingerb2496652019-09-12 23:35:46 -0400278 content = _run_command(['git', 'diff', 'HEAD', path])
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700279 else:
Mike Frysingerb2496652019-09-12 23:35:46 -0400280 content = _run_command(['git', 'show', '%s:%s' % (commit, path)])
281 cache[path] = content
282 return content
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500283
284
Mike Frysingerae409522014-02-01 03:16:11 -0500285def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700286 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysingerb2496652019-09-12 23:35:46 -0400287 # {<commit>: {<path1>: <content>, <path2>: <content>}}
288 cache = CACHE.get_subcache('get_file_diff')
289 if path in cache:
290 return cache[path]
291
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700292 if commit == PRE_SUBMIT:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800293 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff', 'HEAD',
294 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700295 else:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800296 command = ['git', 'show', '-p', '--pretty=format:', '--no-ext-diff', commit,
297 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700298 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700299
300 new_lines = []
301 line_num = 0
302 for line in output.splitlines():
303 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
304 if m:
305 line_num = int(m.groups(1)[0])
306 continue
307 if line.startswith('+') and not line.startswith('++'):
Jon Salz98255932012-08-18 14:48:02 +0800308 new_lines.append((line_num, _try_utf8_decode(line[1:])))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700309 if not line.startswith('-'):
310 line_num += 1
Mike Frysingerb2496652019-09-12 23:35:46 -0400311 cache[path] = new_lines
Ryan Cuiec4d6332011-05-02 14:15:25 -0700312 return new_lines
313
Ryan Cui1562fb82011-05-09 11:01:31 -0700314
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700315def _get_ignore_wildcards(directory, cache):
316 """Get wildcards listed in a directory's _IGNORE_FILE.
317
318 Args:
319 directory: A string containing a directory path.
320 cache: A dictionary (opaque to caller) caching previously-read wildcards.
321
322 Returns:
323 A list of wildcards from _IGNORE_FILE or an empty list if _IGNORE_FILE
324 wasn't present.
325 """
326 # In the cache, keys are directories and values are lists of wildcards from
327 # _IGNORE_FILE within those directories (and empty if no file was present).
328 if directory not in cache:
329 wildcards = []
330 dotfile_path = os.path.join(directory, _IGNORE_FILE)
331 if os.path.exists(dotfile_path):
332 # TODO(derat): Consider using _get_file_content() to get the file as of
333 # this commit instead of the on-disk version. This may have a noticeable
334 # performance impact, as each call to _get_file_content() runs git.
335 with open(dotfile_path, 'r') as dotfile:
336 for line in dotfile.readlines():
337 line = line.strip()
338 if line.startswith('#'):
339 continue
340 if line.endswith('/'):
341 line += '*'
342 wildcards.append(line)
343 cache[directory] = wildcards
344
345 return cache[directory]
346
347
348def _path_is_ignored(path, cache):
349 """Check whether a path is ignored by _IGNORE_FILE.
350
351 Args:
352 path: A string containing a path.
353 cache: A dictionary (opaque to caller) caching previously-read wildcards.
354
355 Returns:
356 True if a file named _IGNORE_FILE in one of the passed-in path's parent
357 directories contains a wildcard matching the path.
358 """
359 # Skip ignore files.
360 if os.path.basename(path) == _IGNORE_FILE:
361 return True
362
363 path = os.path.abspath(path)
364 base = os.getcwd()
365
366 prefix = os.path.dirname(path)
367 while prefix.startswith(base):
368 rel_path = path[len(prefix) + 1:]
369 for wildcard in _get_ignore_wildcards(prefix, cache):
370 if fnmatch.fnmatch(rel_path, wildcard):
371 return True
372 prefix = os.path.dirname(prefix)
373
374 return False
375
376
Mike Frysinger292b45d2014-11-25 01:17:10 -0500377def _get_affected_files(commit, include_deletes=False, relative=False,
378 include_symlinks=False, include_adds=True,
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700379 full_details=False, use_ignore_files=True):
Peter Ammon811f6702014-06-12 15:45:38 -0700380 """Returns list of file paths that were modified/added, excluding symlinks.
381
382 Args:
383 commit: The commit
384 include_deletes: If true, we'll include deleted files in the result
385 relative: Whether to return relative or full paths to files
Mike Frysinger292b45d2014-11-25 01:17:10 -0500386 include_symlinks: If true, we'll include symlinks in the result
387 include_adds: If true, we'll include new files in the result
388 full_details: If False, return filenames, else return structured results.
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700389 use_ignore_files: Whether we ignore files matched by _IGNORE_FILE files.
Peter Ammon811f6702014-06-12 15:45:38 -0700390
391 Returns:
392 A list of modified/added (and perhaps deleted) files
393 """
Mike Frysinger292b45d2014-11-25 01:17:10 -0500394 if not relative and full_details:
395 raise ValueError('full_details only supports relative paths currently')
396
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700397 if commit == PRE_SUBMIT:
398 return _run_command(['git', 'diff-index', '--cached',
399 '--name-only', 'HEAD']).split()
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500400
401 path = os.getcwd()
Mike Frysingerb2496652019-09-12 23:35:46 -0400402 # {<commit>: {<path1>: <content>, <path2>: <content>}}
403 cache = CACHE.get_subcache('get_affected_files')
404 if path not in cache:
405 cache[path] = git.RawDiff(path, '%s^!' % commit)
406 files = cache[path]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500407
408 # Filter out symlinks.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500409 if not include_symlinks:
410 files = [x for x in files if not stat.S_ISLNK(int(x.dst_mode, 8))]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500411
412 if not include_deletes:
413 files = [x for x in files if x.status != 'D']
414
Mike Frysinger292b45d2014-11-25 01:17:10 -0500415 if not include_adds:
416 files = [x for x in files if x.status != 'A']
417
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700418 if use_ignore_files:
419 cache = {}
420 is_ignored = lambda x: _path_is_ignored(x.dst_file or x.src_file, cache)
421 files = [x for x in files if not is_ignored(x)]
422
Mike Frysinger292b45d2014-11-25 01:17:10 -0500423 if full_details:
424 # Caller wants the raw objects to parse status/etc... themselves.
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500425 return files
426 else:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500427 # Caller only cares about filenames.
428 files = [x.dst_file if x.dst_file else x.src_file for x in files]
429 if relative:
430 return files
431 else:
432 return [os.path.join(path, x) for x in files]
Peter Ammon811f6702014-06-12 15:45:38 -0700433
434
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700435def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700436 """Returns a list of commits for this review."""
Mike Frysingere300c7d2019-09-12 23:33:52 -0400437 cmd = ['git', 'log', '--no-merges', '--format=%H',
438 '%s..' % _get_upstream_branch()]
Ryan Cui72834d12011-05-05 14:51:33 -0700439 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700440
Ryan Cui1562fb82011-05-09 11:01:31 -0700441
Ryan Cuiec4d6332011-05-02 14:15:25 -0700442def _get_commit_desc(commit):
443 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700444 if commit == PRE_SUBMIT:
445 return ''
Mike Frysingerb2496652019-09-12 23:35:46 -0400446
447 # {<commit>: <content>}
448 cache = CACHE.get_subcache('get_commit_desc')
449 if commit not in cache:
450 cache[commit] = _run_command(['git', 'log', '--format=%s%n%n%b',
451 commit + '^!'])
452 return cache[commit]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700453
454
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800455def _check_lines_in_diff(commit, files, check_callable, error_description):
456 """Checks given file for errors via the given check.
457
458 This is a convenience function for common per-line checks. It goes through all
459 files and returns a HookFailure with the error description listing all the
460 failures.
461
462 Args:
463 commit: The commit we're working on.
464 files: The files to check.
465 check_callable: A callable that takes a line and returns True if this line
466 _fails_ the check.
467 error_description: A string describing the error.
468 """
469 errors = []
470 for afile in files:
471 for line_num, line in _get_file_diff(afile, commit):
472 if check_callable(line):
473 errors.append('%s, line %s' % (afile, line_num))
474 if errors:
475 return HookFailure(error_description, errors)
476
477
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900478def _parse_common_inclusion_options(options):
479 """Parses common hook options for including/excluding files.
480
481 Args:
482 options: Option string list.
483
484 Returns:
485 (included, excluded) where each one is a list of regex strings.
486 """
487 parser = argparse.ArgumentParser()
488 parser.add_argument('--exclude_regex', action='append')
489 parser.add_argument('--include_regex', action='append')
490 opts = parser.parse_args(options)
491 included = opts.include_regex or []
492 excluded = opts.exclude_regex or []
493 return included, excluded
494
495
Ryan Cuiec4d6332011-05-02 14:15:25 -0700496# Common Hooks
497
Ryan Cui1562fb82011-05-09 11:01:31 -0700498
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900499def _check_no_long_lines(_project, commit, options=()):
Mike Frysinger55f85b52014-12-18 14:45:21 -0500500 """Checks there are no lines longer than MAX_LEN in any of the text files."""
Keigo Oka9732e382019-06-28 17:44:59 +0900501 LONG_LINE_OK_PATHS = [
502 # Go has no line length limit.
503 # https://golang.org/doc/effective_go.html#formatting
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400504 r'.*\.go$',
Keigo Oka9732e382019-06-28 17:44:59 +0900505 ]
Mike Frysinger55f85b52014-12-18 14:45:21 -0500506
Ryan Cuiec4d6332011-05-02 14:15:25 -0700507 MAX_LEN = 80
508
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900509 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700510 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900511 included + COMMON_INCLUDED_PATHS,
Keigo Oka9732e382019-06-28 17:44:59 +0900512 excluded + COMMON_EXCLUDED_PATHS + LONG_LINE_OK_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700513
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900514 errors = []
Ryan Cuiec4d6332011-05-02 14:15:25 -0700515 for afile in files:
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700516 skip_regexps = (
517 r'https?://',
518 r'^#\s*(define|include|import|pragma|if|ifndef|endif)\b',
519 )
520
521 if os.path.basename(afile).startswith('OWNERS'):
522 # File paths can get long, and there's no way to break them up into
523 # multiple lines.
524 skip_regexps += (
525 r'^include\b',
526 r'file:',
527 )
528
529 skip_regexps = [re.compile(x) for x in skip_regexps]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700530 for line_num, line in _get_file_diff(afile, commit):
531 # Allow certain lines to exceed the maxlen rule.
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700532 if len(line) <= MAX_LEN or any(x.search(line) for x in skip_regexps):
Jon Salz98255932012-08-18 14:48:02 +0800533 continue
534
535 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
536 if len(errors) == 5: # Just show the first 5 errors.
537 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700538
539 if errors:
540 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700541 return HookFailure(msg, errors)
542
Ryan Cuiec4d6332011-05-02 14:15:25 -0700543
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900544def _check_no_stray_whitespace(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700545 """Checks that there is no stray whitespace at source lines end."""
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900546 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700547 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900548 included + COMMON_INCLUDED_PATHS,
549 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800550 return _check_lines_in_diff(commit, files,
551 lambda line: line.rstrip() != line,
552 'Found line ending with white space in:')
Ryan Cui1562fb82011-05-09 11:01:31 -0700553
Ryan Cuiec4d6332011-05-02 14:15:25 -0700554
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900555def _check_no_tabs(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700556 """Checks there are no unexpanded tabs."""
Mike Frysingercd134512017-10-26 04:36:33 -0400557 # Don't add entire repos here. Update the PRESUBMIT.cfg in each repo instead.
558 # We only whitelist known specific filetypes here that show up in all repos.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700559 TAB_OK_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400560 r'.*\.ebuild$',
561 r'.*\.eclass$',
562 r'.*\.go$',
563 r'.*/[M|m]akefile$',
564 r'.*\.mk$',
Ryan Cuiec4d6332011-05-02 14:15:25 -0700565 ]
566
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900567 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700568 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900569 included + COMMON_INCLUDED_PATHS,
570 excluded + COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800571 return _check_lines_in_diff(commit, files,
572 lambda line: '\t' in line,
573 'Found a tab character in:')
Ryan Cuiec4d6332011-05-02 14:15:25 -0700574
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800575
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900576def _check_tabbed_indents(_project, commit, options=()):
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800577 """Checks that indents use tabs only."""
578 TABS_REQUIRED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400579 r'.*\.ebuild$',
580 r'.*\.eclass$',
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800581 ]
582 LEADING_SPACE_RE = re.compile('[\t]* ')
583
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900584 included, excluded = _parse_common_inclusion_options(options)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800585 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900586 included + TABS_REQUIRED_PATHS,
587 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800588 return _check_lines_in_diff(
589 commit, files,
590 lambda line: LEADING_SPACE_RE.match(line) is not None,
591 'Found a space in indentation (must be all tabs):')
Ryan Cui1562fb82011-05-09 11:01:31 -0700592
Ryan Cuiec4d6332011-05-02 14:15:25 -0700593
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700594def _check_gofmt(_project, commit):
595 """Checks that Go files are formatted with gofmt."""
596 errors = []
597 files = _filter_files(_get_affected_files(commit, relative=True),
598 [r'\.go$'])
599
600 for gofile in files:
601 contents = _get_file_content(gofile, commit)
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700602 output = _run_command(cmd=['gofmt', '-l'], input=contents,
603 combine_stdout_stderr=True)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700604 if output:
605 errors.append(gofile)
606 if errors:
607 return HookFailure('Files not formatted with gofmt:', errors)
608
609
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600610def _check_rustfmt(_project, commit):
611 """Checks that Rust files are formatted with rustfmt."""
612 errors = []
613 files = _filter_files(_get_affected_files(commit, relative=True),
614 [r'\.rs$'])
615
616 for rustfile in files:
617 contents = _get_file_content(rustfile, commit)
618 output = _run_command(cmd=['rustfmt'], input=contents,
619 combine_stdout_stderr=True)
620 if output != contents:
621 errors.append(rustfile)
622 if errors:
623 return HookFailure('Files not formatted with rustfmt: '
624 "(run 'cargo fmt' to fix)", errors)
625
626
Mike Frysingerae409522014-02-01 03:16:11 -0500627def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700628 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700629 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700630
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700631 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700632 msg = 'Changelist description needs TEST field (after first line)'
633 return HookFailure(msg)
634
Ryan Cuiec4d6332011-05-02 14:15:25 -0700635
Mike Frysingerae409522014-02-01 03:16:11 -0500636def _check_change_has_valid_cq_depend(_project, commit):
Jason D. Clinton299e3222019-05-23 09:42:03 -0600637 """Check for a correctly formatted Cq-Depend field in the commit message."""
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700638 desc = _get_commit_desc(commit)
Jason D. Clinton299e3222019-05-23 09:42:03 -0600639 msg = 'Changelist has invalid Cq-Depend target.'
640 example = 'Example: Cq-Depend: chromium:1234, chrome-internal:2345'
David Jamesc3b68b32013-04-03 09:17:03 -0700641 try:
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700642 patch.GetPaladinDeps(desc)
David Jamesc3b68b32013-04-03 09:17:03 -0700643 except ValueError as ex:
644 return HookFailure(msg, [example, str(ex)])
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700645 # Check that Cq-Depend is in the same paragraph as Change-Id.
646 msg = 'Cq-Depend (or CQ-DEPEND) is not in the same paragraph as Change-Id.'
647 paragraphs = desc.split('\n\n')
648 for paragraph in paragraphs:
649 if (re.search(r'^Cq-Depend:', paragraph, re.M) or
650 re.search(r'^CQ-DEPEND=', paragraph, re.M)) \
651 and not re.search('^Change-Id:', paragraph, re.M):
652 return HookFailure(msg)
David Jamesc3b68b32013-04-03 09:17:03 -0700653
654
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800655def _check_change_is_contribution(_project, commit):
656 """Check that the change is a contribution."""
657 NO_CONTRIB = 'not a contribution'
658 if NO_CONTRIB in _get_commit_desc(commit).lower():
659 msg = ('Changelist is not a contribution, this cannot be accepted.\n'
660 'Please remove the "%s" text from the commit message.') % NO_CONTRIB
661 return HookFailure(msg)
662
663
Alex Deymo643ac4c2015-09-03 10:40:50 -0700664def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700665 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700666 OLD_BUG_RE = r'\nBUG=.*chromium-os'
667 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
668 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
669 'the chromium tracker in your BUG= line now.')
670 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700671
Alex Deymo643ac4c2015-09-03 10:40:50 -0700672 # Android internal and external projects use "Bug: " to track bugs in
673 # buganizer.
674 BUG_COLON_REMOTES = (
675 'aosp',
676 'goog',
677 )
678 if project.remote in BUG_COLON_REMOTES:
679 BUG_RE = r'\nBug: ?([Nn]one|\d+)'
680 if not re.search(BUG_RE, _get_commit_desc(commit)):
681 msg = ('Changelist description needs BUG field (after first line):\n'
682 'Bug: 9999 (for buganizer)\n'
683 'BUG=None')
684 return HookFailure(msg)
685 else:
Jorge Lucangeli Obesdce214e2017-10-25 15:04:39 -0400686 BUG_RE = r'\nBUG=([Nn]one|(chromium|b):\d+)'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700687 if not re.search(BUG_RE, _get_commit_desc(commit)):
688 msg = ('Changelist description needs BUG field (after first line):\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700689 'BUG=chromium:9999 (for public tracker)\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700690 'BUG=b:9999 (for buganizer)\n'
691 'BUG=None')
692 return HookFailure(msg)
Ryan Cui1562fb82011-05-09 11:01:31 -0700693
Ryan Cuiec4d6332011-05-02 14:15:25 -0700694
Jack Neus8edbf642019-07-10 16:08:31 -0600695def _check_change_no_include_oem(project, commit):
696 """Check that the change does not reference OEMs."""
697 ALLOWLIST = {
698 'chromiumos/platform/ec',
699 # Used by unit tests.
700 'project',
701 }
702 if project.name not in ALLOWLIST:
703 return None
704
Mike Frysingerbb34a222019-07-31 14:40:46 -0400705 TAGS = {
Jack Neus8edbf642019-07-10 16:08:31 -0600706 'Reviewed-on',
707 'Reviewed-by',
708 'Signed-off-by',
709 'Commit-Ready',
710 'Tested-by',
711 'Commit-Queue',
712 'Legacy-Commit-Queue',
713 'Acked-by',
714 'Modified-by',
715 'CC',
716 'Suggested-by',
717 'Reported-by',
718 'Acked-for-chrome-by',
Mike Frysingerbb34a222019-07-31 14:40:46 -0400719 }
Jack Neus8edbf642019-07-10 16:08:31 -0600720
721 # Ignore tags, which could reasonably contain OEM names
722 # (e.g. Reviewed-by: foo@oem.corp-partner.google.com).
Jack Neus8edbf642019-07-10 16:08:31 -0600723 commit_message = ' '.join(
Mike Frysingerbb34a222019-07-31 14:40:46 -0400724 x for x in _get_commit_desc(commit).splitlines()
725 if ':' not in x or x.split(':', 1)[0] not in TAGS)
726
Jack Neus8edbf642019-07-10 16:08:31 -0600727 commit_message = re.sub(r'[\s_-]+', ' ', commit_message)
728
729 # Exercise caution when expanding these lists. Adding a name
730 # could indicate a new relationship with a company!
731 OEMS = ['hp', 'hewlett packard', 'dell', 'lenovo', 'acer', 'asus', 'samsung']
732 ODMS = [
733 'bitland', 'compal', 'haier', 'huaqin', 'inventec', 'lg', 'pegatron',
734 'pegatron(ems)', 'quanta', 'samsung', 'wistron'
735 ]
736
737 for name_type, name_list in [('OEM', OEMS), ('ODM', ODMS)]:
738 # Construct regex
739 name_re = r'\b(%s)\b' % '|'.join([re.escape(x) for x in name_list])
740 matches = [x[0] for x in re.findall(name_re, commit_message, re.IGNORECASE)]
741 if len(matches):
742 # If there's a match, throw an error.
743 error_msg = ('Changelist description contains the name of an'
744 ' %s: "%s".' % (name_type, '","'.join(matches)))
745 return HookFailure(error_msg)
746
747
Mike Frysinger292b45d2014-11-25 01:17:10 -0500748def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -0700749 """Check that we're not missing a revbump of an ebuild in the given commit.
750
751 If the given commit touches files in a directory that has ebuilds somewhere
752 up the directory hierarchy, it's very likely that we need an ebuild revbump
753 in order for those changes to take effect.
754
755 It's not totally trivial to detect a revbump, so at least detect that an
756 ebuild with a revision number in it was touched. This should handle the
757 common case where we use a symlink to do the revbump.
758
759 TODO: it would be nice to enhance this hook to:
760 * Handle cases where people revbump with a slightly different syntax. I see
761 one ebuild (puppy) that revbumps with _pN. This is a false positive.
762 * Catches cases where people aren't using symlinks for revbumps. If they
763 edit a revisioned file directly (and are expected to rename it for revbump)
764 we'll miss that. Perhaps we could detect that the file touched is a
765 symlink?
766
767 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
768 still better off than without this check.
769
770 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700771 project: The Project to look at
Doug Anderson42b8a052013-06-26 10:45:36 -0700772 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -0500773 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -0700774
775 Returns:
776 A HookFailure or None.
777 """
Mike Frysinger011af942014-01-17 16:12:22 -0500778 # If this is the portage-stable overlay, then ignore the check. It's rare
779 # that we're doing anything other than importing files from upstream, so
780 # forcing a rev bump makes no sense.
781 whitelist = (
782 'chromiumos/overlays/portage-stable',
783 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700784 if project.name in whitelist:
Mike Frysinger011af942014-01-17 16:12:22 -0500785 return None
786
Mike Frysinger292b45d2014-11-25 01:17:10 -0500787 def FinalName(obj):
788 # If the file is being deleted, then the dst_file is not set.
789 if obj.dst_file is None:
790 return obj.src_file
791 else:
792 return obj.dst_file
793
794 affected_path_objs = _get_affected_files(
795 commit, include_deletes=True, include_symlinks=True, relative=True,
796 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -0700797
798 # Don't yell about changes to whitelisted files...
Aviv Keshet272f2e52016-04-25 14:49:44 -0700799 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml', 'COMMIT-QUEUE.ini')
Mike Frysinger292b45d2014-11-25 01:17:10 -0500800 affected_path_objs = [x for x in affected_path_objs
801 if os.path.basename(FinalName(x)) not in whitelist]
802 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -0700803 return None
804
805 # If we've touched any file named with a -rN.ebuild then we'll say we're
806 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500807 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
808 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700809 if touched_revved_ebuild:
810 return None
811
Mike Frysinger292b45d2014-11-25 01:17:10 -0500812 # If we're creating new ebuilds from scratch, then we don't need an uprev.
813 # Find all the dirs that new ebuilds and ignore their files/.
814 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
815 if FinalName(x).endswith('.ebuild') and x.status == 'A']
816 affected_path_objs = [obj for obj in affected_path_objs
817 if not any(FinalName(obj).startswith(x)
818 for x in ebuild_dirs)]
819 if not affected_path_objs:
820 return
821
Doug Anderson42b8a052013-06-26 10:45:36 -0700822 # We want to examine the current contents of all directories that are parents
823 # of files that were touched (up to the top of the project).
824 #
825 # ...note: we use the current directory contents even though it may have
826 # changed since the commit we're looking at. This is just a heuristic after
827 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500828 if project_top is None:
829 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -0700830 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -0500831 for obj in affected_path_objs:
832 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -0700833 while os.path.exists(path) and not os.path.samefile(path, project_top):
834 dirs_to_check.add(path)
835 path = os.path.dirname(path)
836
837 # Look through each directory. If it's got an ebuild in it then we'll
838 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -0800839 affected_paths = set(os.path.join(project_top, FinalName(x))
840 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700841 for dir_path in dirs_to_check:
842 contents = os.listdir(dir_path)
843 ebuilds = [os.path.join(dir_path, path)
844 for path in contents if path.endswith('.ebuild')]
845 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
846
C Shapiroae157ae2017-09-18 16:24:03 -0600847 affected_paths_under_9999_ebuilds = set()
848 for affected_path in affected_paths:
849 for ebuild_9999 in ebuilds_9999:
850 ebuild_dir = os.path.dirname(ebuild_9999)
851 if affected_path.startswith(ebuild_dir):
852 affected_paths_under_9999_ebuilds.add(affected_path)
853
854 # If every file changed exists under a 9999 ebuild, then skip
855 if len(affected_paths_under_9999_ebuilds) == len(affected_paths):
856 continue
857
Doug Anderson42b8a052013-06-26 10:45:36 -0700858 # If the -9999.ebuild file was touched the bot will uprev for us.
859 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -0500860 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -0700861 continue
862
863 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500864 return HookFailure('Changelist probably needs a revbump of an ebuild, '
865 'or a -r1.ebuild symlink if this is a new ebuild:\n'
866 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -0700867
868 return None
869
870
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500871def _check_ebuild_eapi(project, commit):
Mike Frysinger948284a2018-02-01 15:22:56 -0500872 """Make sure we have people use EAPI=5 or newer with custom ebuilds.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500873
874 We want to get away from older EAPI's as it makes life confusing and they
875 have less builtin error checking.
876
877 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700878 project: The Project to look at
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500879 commit: The commit to look at
880
881 Returns:
882 A HookFailure or None.
883 """
884 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500885 # that we're doing anything other than importing files from upstream, and
886 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500887 whitelist = (
888 'chromiumos/overlays/portage-stable',
889 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700890 if project.name in whitelist:
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500891 return None
892
Mike Frysinger948284a2018-02-01 15:22:56 -0500893 BAD_EAPIS = ('0', '1', '2', '3', '4')
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500894
895 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
896
897 ebuilds_re = [r'\.ebuild$']
898 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
899 ebuilds_re)
900 bad_ebuilds = []
901
902 for ebuild in ebuilds:
903 # If the ebuild does not specify an EAPI, it defaults to 0.
904 eapi = '0'
905
906 lines = _get_file_content(ebuild, commit).splitlines()
907 if len(lines) == 1:
908 # This is most likely a symlink, so skip it entirely.
909 continue
910
911 for line in lines:
912 m = get_eapi.match(line)
913 if m:
914 # Once we hit the first EAPI line in this ebuild, stop processing.
915 # The spec requires that there only be one and it be first, so
916 # checking all possible values is pointless. We also assume that
917 # it's "the" EAPI line and not something in the middle of a heredoc.
918 eapi = m.group(1)
919 break
920
921 if eapi in BAD_EAPIS:
922 bad_ebuilds.append((ebuild, eapi))
923
924 if bad_ebuilds:
925 # pylint: disable=C0301
926 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
927 # pylint: enable=C0301
928 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500929 'These ebuilds are using old EAPIs. If these are imported from\n'
930 'Gentoo, then you may ignore and upload once with the --no-verify\n'
Mike Frysinger948284a2018-02-01 15:22:56 -0500931 'flag. Otherwise, please update to 5 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500932 '\t%s\n'
933 'See this guide for more details:\n%s\n' %
934 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
935
936
Mike Frysinger89bdb852014-02-01 05:26:26 -0500937def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500938 """Make sure we use the new style KEYWORDS when possible in ebuilds.
939
940 If an ebuild generally does not care about the arch it is running on, then
941 ebuilds should flag it with one of:
942 KEYWORDS="*" # A stable ebuild.
943 KEYWORDS="~*" # An unstable ebuild.
944 KEYWORDS="-* ..." # Is known to only work on specific arches.
945
946 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700947 project: The Project to look at
Mike Frysingerc51ece72014-01-17 16:23:40 -0500948 commit: The commit to look at
949
950 Returns:
951 A HookFailure or None.
952 """
953 WHITELIST = set(('*', '-*', '~*'))
954
955 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
956
Mike Frysinger89bdb852014-02-01 05:26:26 -0500957 ebuilds_re = [r'\.ebuild$']
958 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
959 ebuilds_re)
960
Mike Frysinger8d42d742014-09-22 15:50:21 -0400961 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500962 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400963 # We get the full content rather than a diff as the latter does not work
964 # on new files (like when adding new ebuilds).
965 lines = _get_file_content(ebuild, commit).splitlines()
966 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500967 m = get_keywords.match(line)
968 if m:
969 keywords = set(m.group(1).split())
970 if not keywords or WHITELIST - keywords != WHITELIST:
971 continue
972
Mike Frysinger8d42d742014-09-22 15:50:21 -0400973 bad_ebuilds.append(ebuild)
974
975 if bad_ebuilds:
976 return HookFailure(
977 '%s\n'
978 'Please update KEYWORDS to use a glob:\n'
979 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
980 ' KEYWORDS="*"\n'
981 'If the ebuild should be marked unstable (normal for '
982 'cros-workon / 9999 ebuilds):\n'
983 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -0400984 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -0400985 'then use -* like so:\n'
986 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500987
988
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800989def _check_ebuild_licenses(_project, commit):
990 """Check if the LICENSE field in the ebuild is correct."""
Brian Norris7a610e82016-02-17 12:24:54 -0800991 affected_paths = _get_affected_files(commit, relative=True)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800992 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
993
994 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800995 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800996
997 for ebuild in touched_ebuilds:
998 # Skip virutal packages.
999 if ebuild.split('/')[-3] == 'virtual':
1000 continue
1001
Alex Kleinb5953522018-08-03 11:44:21 -06001002 # e.g. path/to/overlay/category/package/package.ebuild -> path/to/overlay
1003 overlay_path = os.sep.join(ebuild.split(os.sep)[:-3])
1004
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001005 try:
Brian Norris7a610e82016-02-17 12:24:54 -08001006 ebuild_content = _get_file_content(ebuild, commit)
Alex Kleinb5953522018-08-03 11:44:21 -06001007 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
1008 overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001009 except ValueError as e:
1010 return HookFailure(e.message, [ebuild])
1011
1012 # Also ignore licenses ending with '?'
1013 for license_type in [x for x in license_types
1014 if x not in LICENSES_IGNORE and not x.endswith('?')]:
1015 try:
Alex Kleinb5953522018-08-03 11:44:21 -06001016 licenses_lib.Licensing.FindLicenseType(license_type,
1017 overlay_path=overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001018 except AssertionError as e:
1019 return HookFailure(e.message, [ebuild])
1020
1021
Mike Frysingercd363c82014-02-01 05:20:18 -05001022def _check_ebuild_virtual_pv(project, commit):
1023 """Enforce the virtual PV policies."""
1024 # If this is the portage-stable overlay, then ignore the check.
1025 # We want to import virtuals as-is from upstream Gentoo.
1026 whitelist = (
1027 'chromiumos/overlays/portage-stable',
1028 )
Alex Deymo643ac4c2015-09-03 10:40:50 -07001029 if project.name in whitelist:
Mike Frysingercd363c82014-02-01 05:20:18 -05001030 return None
1031
1032 # We assume the repo name is the same as the dir name on disk.
1033 # It would be dumb to not have them match though.
Alex Deymo643ac4c2015-09-03 10:40:50 -07001034 project_base = os.path.basename(project.name)
Mike Frysingercd363c82014-02-01 05:20:18 -05001035
1036 is_variant = lambda x: x.startswith('overlay-variant-')
1037 is_board = lambda x: x.startswith('overlay-')
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001038 is_baseboard = lambda x: x.startswith('baseboard-')
1039 is_chipset = lambda x: x.startswith('chipset-')
1040 is_project = lambda x: x.startswith('project-')
Mike Frysingercd363c82014-02-01 05:20:18 -05001041 is_private = lambda x: x.endswith('-private')
1042
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001043 is_special_overlay = lambda x: (is_board(x) or is_chipset(x) or
1044 is_baseboard(x) or is_project(x))
1045
Mike Frysingercd363c82014-02-01 05:20:18 -05001046 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
1047
1048 ebuilds_re = [r'\.ebuild$']
1049 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1050 ebuilds_re)
1051 bad_ebuilds = []
1052
1053 for ebuild in ebuilds:
1054 m = get_pv.match(ebuild)
1055 if m:
1056 overlay = m.group(1)
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001057 if not overlay or not is_special_overlay(overlay):
Alex Deymo643ac4c2015-09-03 10:40:50 -07001058 overlay = project_base
Mike Frysingercd363c82014-02-01 05:20:18 -05001059
1060 pv = m.group(3).split('-', 1)[0]
1061
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001062 # Virtual versions >= 4 are special cases used above the standard
1063 # versioning structure, e.g. if one has a board inheriting a board.
1064 if float(pv) >= 4:
1065 want_pv = pv
Mike Frysingercd363c82014-02-01 05:20:18 -05001066 elif is_board(overlay):
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001067 if is_private(overlay):
1068 want_pv = '3.5' if is_variant(overlay) else '3'
1069 elif is_board(overlay):
1070 want_pv = '2.5' if is_variant(overlay) else '2'
1071 elif is_baseboard(overlay):
1072 want_pv = '1.9'
1073 elif is_chipset(overlay):
1074 want_pv = '1.8'
1075 elif is_project(overlay):
1076 want_pv = '1.7' if is_private(overlay) else '1.5'
Mike Frysingercd363c82014-02-01 05:20:18 -05001077 else:
1078 want_pv = '1'
1079
1080 if pv != want_pv:
1081 bad_ebuilds.append((ebuild, pv, want_pv))
1082
1083 if bad_ebuilds:
1084 # pylint: disable=C0301
1085 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
1086 # pylint: enable=C0301
1087 return HookFailure(
1088 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
1089 '\t%s\n'
1090 'If this is an upstream Gentoo virtual, then you may ignore this\n'
1091 'check (and re-run w/--no-verify). Otherwise, please see this\n'
1092 'page for more details:\n%s\n' %
1093 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
1094 for x in bad_ebuilds]), url))
1095
1096
Daniel Erat9d203ff2015-02-17 10:12:21 -07001097def _check_portage_make_use_var(_project, commit):
1098 """Verify that $USE is set correctly in make.conf and make.defaults."""
1099 files = _filter_files(_get_affected_files(commit, relative=True),
1100 [r'(^|/)make.(conf|defaults)$'])
1101
1102 errors = []
1103 for path in files:
1104 basename = os.path.basename(path)
1105
1106 # Has a USE= line already been encountered in this file?
1107 saw_use = False
1108
1109 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
1110 if not line.startswith('USE='):
1111 continue
1112
1113 preserves_use = '${USE}' in line or '$USE' in line
1114
1115 if (basename == 'make.conf' or
1116 (basename == 'make.defaults' and saw_use)) and not preserves_use:
1117 errors.append('%s:%d: missing ${USE}' % (path, i))
1118 elif basename == 'make.defaults' and not saw_use and preserves_use:
1119 errors.append('%s:%d: ${USE} referenced in initial declaration' %
1120 (path, i))
1121
1122 saw_use = True
1123
1124 if errors:
1125 return HookFailure(
1126 'One or more Portage make files appear to set USE incorrectly.\n'
1127 '\n'
1128 'All USE assignments in make.conf and all assignments after the\n'
1129 'initial declaration in make.defaults should contain "${USE}" to\n'
1130 'preserve previously-set flags.\n'
1131 '\n'
1132 'The initial USE declaration in make.defaults should not contain\n'
1133 '"${USE}".\n',
1134 errors)
1135
1136
Mike Frysingerae409522014-02-01 03:16:11 -05001137def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001138 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001139 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001140 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001141 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001142 if not m:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001143 return HookFailure('Last paragraph of description must include Change-Id.')
Ryan Cui1562fb82011-05-09 11:01:31 -07001144
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001145 # S-o-b tags always allowed to follow Change-ID.
1146 allowed_tags = ['Signed-off-by']
1147
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001148 end = desc[m.end():].strip().splitlines()
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001149 cherry_pick_marker = 'cherry picked from commit'
1150
1151 if end and cherry_pick_marker in end[-1]:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001152 # Cherry picked patches allow more tags in the last paragraph.
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001153 allowed_tags += ['Commit-Queue', 'Commit-Ready', 'Reviewed-by',
1154 'Reviewed-on', 'Tested-by']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001155 end = end[:-1]
1156
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001157 # Note that descriptions could have multiple cherry pick markers.
1158 tag_search = r'^(%s:|\(%s) ' % (':|'.join(allowed_tags), cherry_pick_marker)
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001159
1160 if [x for x in end if not re.search(tag_search, x)]:
1161 return HookFailure('Only "%s:" tag(s) may follow the Change-Id.' %
1162 ':", "'.join(allowed_tags))
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001163
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001164
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001165def _check_commit_message_style(_project, commit):
1166 """Verify that the commit message matches our style.
1167
1168 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
1169 commit hooks.
1170 """
1171 desc = _get_commit_desc(commit)
1172
1173 # The first line should be by itself.
1174 lines = desc.splitlines()
1175 if len(lines) > 1 and lines[1]:
1176 return HookFailure('The second line of the commit message must be blank.')
1177
1178 # The first line should be one sentence.
1179 if '. ' in lines[0]:
1180 return HookFailure('The first line cannot be more than one sentence.')
1181
1182 # The first line cannot be too long.
1183 MAX_FIRST_LINE_LEN = 100
1184 if len(lines[0]) > MAX_FIRST_LINE_LEN:
1185 return HookFailure('The first line must be less than %i chars.' %
1186 MAX_FIRST_LINE_LEN)
1187
1188
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001189def _check_cros_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001190 """Verifies the Chromium OS license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -07001191
Mike Frysinger98638102014-08-28 00:15:08 -04001192 Should be following the spec:
1193 http://dev.chromium.org/developers/coding-style#TOC-File-headers
1194 """
1195 # For older years, be a bit more flexible as our policy says leave them be.
1196 LICENSE_HEADER = (
Keigo Oka7e880ac2019-07-03 15:03:43 +09001197 r'.*Copyright(?: \(c\))? (20[0-9]{2})(?:-20[0-9]{2})? The Chromium OS '
1198 r'Authors\. All rights reserved\.\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001199 r'.*Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001200 r'be\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001201 r'.*found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -05001202 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001203 )
1204 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1205
1206 # For newer years, be stricter.
Keigo Oka7e880ac2019-07-03 15:03:43 +09001207 BAD_COPYRIGHT_LINE = (
Brian Norris68838dd2018-09-26 18:30:24 -07001208 r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001209 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001210 )
Keigo Oka7e880ac2019-07-03 15:03:43 +09001211 bad_copyright_re = re.compile(BAD_COPYRIGHT_LINE)
Mike Frysinger98638102014-08-28 00:15:08 -04001212
Shuhei Takahashiabc20f32017-07-10 19:35:45 +09001213 included, excluded = _parse_common_inclusion_options(options)
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001214
Mike Frysinger98638102014-08-28 00:15:08 -04001215 bad_files = []
1216 bad_copyright_files = []
Keigo Oka7e880ac2019-07-03 15:03:43 +09001217 bad_year_files = []
1218
Ken Turnerd07564b2018-02-08 17:57:59 +11001219 files = _filter_files(
1220 _get_affected_files(commit, relative=True),
1221 included + COMMON_INCLUDED_PATHS,
1222 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001223 existing_files = set(_get_affected_files(commit, relative=True,
1224 include_adds=False))
Mike Frysinger98638102014-08-28 00:15:08 -04001225
Keigo Oka7e880ac2019-07-03 15:03:43 +09001226 current_year = str(datetime.datetime.now().year)
Mike Frysinger98638102014-08-28 00:15:08 -04001227 for f in files:
1228 contents = _get_file_content(f, commit)
1229 if not contents:
1230 # Ignore empty files.
1231 continue
1232
Keigo Oka7e880ac2019-07-03 15:03:43 +09001233 m = license_re.search(contents)
1234 if not m:
Mike Frysinger98638102014-08-28 00:15:08 -04001235 bad_files.append(f)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001236 elif bad_copyright_re.search(contents):
Mike Frysinger98638102014-08-28 00:15:08 -04001237 bad_copyright_files.append(f)
1238
Keigo Oka7e880ac2019-07-03 15:03:43 +09001239 if m and f not in existing_files:
1240 year = m.group(1)
1241 if year != current_year:
1242 bad_year_files.append(f)
1243
1244 errors = []
Mike Frysinger98638102014-08-28 00:15:08 -04001245 if bad_files:
1246 msg = '%s:\n%s\n%s' % (
1247 'License must match', license_re.pattern,
1248 'Found a bad header in these files:')
Keigo Oka7e880ac2019-07-03 15:03:43 +09001249 errors.append(HookFailure(msg, bad_files))
Mike Frysinger98638102014-08-28 00:15:08 -04001250 if bad_copyright_files:
1251 msg = 'Do not use (c) in copyright headers in new files:'
Keigo Oka7e880ac2019-07-03 15:03:43 +09001252 errors.append(HookFailure(msg, bad_copyright_files))
1253 if bad_year_files:
1254 msg = 'Use current year (%s) in copyright headers in new files:' % (
1255 current_year)
1256 errors.append(HookFailure(msg, bad_year_files))
Ryan Cuiec4d6332011-05-02 14:15:25 -07001257
Keigo Oka7e880ac2019-07-03 15:03:43 +09001258 return errors
Ryan Cuiec4d6332011-05-02 14:15:25 -07001259
Amin Hassani391efa92018-01-26 17:58:05 -08001260def _check_aosp_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001261 """Verifies the AOSP license/copyright header.
1262
1263 AOSP uses the Apache2 License:
1264 https://source.android.com/source/licenses.html
1265 """
1266 LICENSE_HEADER = (
1267 r"""^[#/\*]*
1268[#/\*]* ?Copyright( \([cC]\))? 20[-0-9]{2,7} The Android Open Source Project
1269[#/\*]* ?
1270[#/\*]* ?Licensed under the Apache License, Version 2.0 \(the "License"\);
1271[#/\*]* ?you may not use this file except in compliance with the License\.
1272[#/\*]* ?You may obtain a copy of the License at
1273[#/\*]* ?
1274[#/\*]* ? http://www\.apache\.org/licenses/LICENSE-2\.0
1275[#/\*]* ?
1276[#/\*]* ?Unless required by applicable law or agreed to in writing, software
1277[#/\*]* ?distributed under the License is distributed on an "AS IS" BASIS,
1278[#/\*]* ?WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or """
1279 r"""implied\.
1280[#/\*]* ?See the License for the specific language governing permissions and
1281[#/\*]* ?limitations under the License\.
1282[#/\*]*$
1283"""
1284 )
1285 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1286
Amin Hassani391efa92018-01-26 17:58:05 -08001287 included, excluded = _parse_common_inclusion_options(options)
1288
Ken Turnerd07564b2018-02-08 17:57:59 +11001289 files = _filter_files(
1290 _get_affected_files(commit, relative=True),
1291 included + COMMON_INCLUDED_PATHS,
1292 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Alex Deymof5792ce2015-08-24 22:50:08 -07001293
1294 bad_files = []
1295 for f in files:
1296 contents = _get_file_content(f, commit)
1297 if not contents:
1298 # Ignore empty files.
1299 continue
1300
1301 if not license_re.search(contents):
1302 bad_files.append(f)
1303
1304 if bad_files:
1305 msg = ('License must match:\n%s\nFound a bad header in these files:' %
1306 license_re.pattern)
1307 return HookFailure(msg, bad_files)
1308
1309
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001310def _check_layout_conf(_project, commit):
1311 """Verifies the metadata/layout.conf file."""
1312 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001313 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001314 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001315 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001316
Mike Frysinger94a670c2014-09-19 12:46:26 -04001317 # Handle multiple overlays in a single commit (like the public tree).
1318 for f in _get_affected_files(commit, relative=True):
1319 if f.endswith(repo_name):
1320 repo_names.append(f)
1321 elif f.endswith(layout_path):
1322 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001323
1324 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -04001325 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001326 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -04001327 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001328
Mike Frysinger94a670c2014-09-19 12:46:26 -04001329 # Gather all the errors in one pass so we show one full message.
1330 all_errors = {}
1331 for layout_path in layout_paths:
1332 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001333
Mike Frysinger94a670c2014-09-19 12:46:26 -04001334 # Make sure the config file is sorted.
1335 data = [x for x in _get_file_content(layout_path, commit).splitlines()
1336 if x and x[0] != '#']
1337 if sorted(data) != data:
1338 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001339
Mike Frysinger94a670c2014-09-19 12:46:26 -04001340 # Require people to set specific values all the time.
1341 settings = (
1342 # TODO: Enable this for everyone. http://crbug.com/408038
1343 #('fast caching', 'cache-format = md5-dict'),
1344 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -05001345 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
1346 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001347 )
1348 for reason, line in settings:
1349 if line not in data:
1350 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001351
Mike Frysinger94a670c2014-09-19 12:46:26 -04001352 # Require one of these settings.
Mike Frysinger5fae62d2015-11-11 20:12:15 -05001353 if 'use-manifests = strict' not in data:
1354 errors += ['enable file checking with: use-manifests = strict']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001355
Mike Frysinger94a670c2014-09-19 12:46:26 -04001356 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -04001357 for line in data:
1358 if line.startswith('repo-name = '):
1359 break
1360 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001361 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001362
Mike Frysinger94a670c2014-09-19 12:46:26 -04001363 # Summarize all the errors we saw (if any).
1364 lines = ''
1365 for layout_path, errors in all_errors.items():
1366 if errors:
1367 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1368 if lines:
1369 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1370 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001371
1372
Keigo Oka4a09bd92019-05-07 14:01:00 +09001373def _check_no_new_gyp(_project, commit):
1374 """Verifies no project starts to use GYP."""
1375 whitelist = [
Keigo Oka4a09bd92019-05-07 14:01:00 +09001376 'chromeos/ap',
1377 'chromeos/ap-daemons',
Keigo Oka150a6fd2019-06-04 11:30:25 +09001378 'chromeos/ap/security',
1379 'chromeos/ap/wireless',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001380 'chromeos/platform/actions',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001381 'chromeos/platform/drivefs-google3',
1382 'chromeos/platform/experimental-touch-fw',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001383 'chromeos/thermald',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001384 'chromiumos/platform2',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001385 'weave/libweave',
1386 ]
1387 if _project.name in whitelist:
1388 return None
1389
1390 gypfiles = _filter_files(
1391 _get_affected_files(commit, include_symlinks=True, relative=True),
1392 [r'\.gyp$'])
1393
1394 if gypfiles:
1395 return HookFailure('GYP is deprecated and not allowed in a new project:',
1396 gypfiles)
1397
1398
Ryan Cuiec4d6332011-05-02 14:15:25 -07001399# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001400
Ryan Cui1562fb82011-05-09 11:01:31 -07001401
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001402def _check_clang_format(_project, commit, options=()):
1403 """Runs clang-format on the given project"""
1404 hooks_dir = _get_hooks_dir()
1405 options = list(options)
1406 if commit == PRE_SUBMIT:
1407 options.append('--commit=HEAD')
1408 else:
1409 options.extend(['--commit', commit])
Brian Norris0c62a142018-12-11 13:24:29 -08001410 cmd = [os.path.join(hooks_dir, 'clang-format.py')] + options
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001411 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1412 print_cmd=False,
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001413 stdout_to_pipe=True,
1414 combine_stdout_stderr=True,
1415 error_code_ok=True)
1416 if cmd_result.returncode:
1417 return HookFailure('clang-format.py errors/warnings\n\n' +
1418 cmd_result.output)
1419
1420
Mike Frysingerae409522014-02-01 03:16:11 -05001421def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001422 """Runs checkpatch.pl on the given project"""
1423 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001424 options = list(options)
1425 if commit == PRE_SUBMIT:
1426 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1427 # this case.
1428 options.append('--ignore=MISSING_SIGN_OFF')
Filipe Brandenburger28d48d62015-10-07 09:48:54 -07001429 # Always ignore the check for the MAINTAINERS file. We do not track that
1430 # information on that file in our source trees, so let's suppress the
1431 # warning.
1432 options.append('--ignore=FILE_PATH_CHANGES')
Filipe Brandenburgerce978322015-10-09 10:04:15 -07001433 # Do not complain about the Change-Id: fields, since we use Gerrit.
1434 # Upstream does not want those lines (since they do not use Gerrit), but
1435 # we always do, so disable the check globally.
Daisuke Nojiri4844f892015-10-08 09:54:33 -07001436 options.append('--ignore=GERRIT_CHANGE_ID')
Brian Norris0c62a142018-12-11 13:24:29 -08001437 cmd = [os.path.join(hooks_dir, 'checkpatch.pl')] + options + ['-']
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001438 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1439 print_cmd=False,
1440 input=_get_patch(commit),
1441 stdout_to_pipe=True,
1442 combine_stdout_stderr=True,
1443 error_code_ok=True)
1444 if cmd_result.returncode:
1445 return HookFailure('checkpatch.pl errors/warnings\n\n' + cmd_result.output)
Ryan Cuiec4d6332011-05-02 14:15:25 -07001446
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001447
Brian Norris23c62e92018-11-14 12:25:51 -08001448def _run_kerneldoc(_project, commit, options=()):
1449 """Runs kernel-doc validator on the given project"""
1450 included, excluded = _parse_common_inclusion_options(options)
1451 files = _filter_files(_get_affected_files(commit, relative=True),
1452 included, excluded)
1453 if files:
1454 hooks_dir = _get_hooks_dir()
Brian Norris0c62a142018-12-11 13:24:29 -08001455 cmd = [os.path.join(hooks_dir, 'kernel-doc'), '-none'] + files
Brian Norris23c62e92018-11-14 12:25:51 -08001456 output = _run_command(cmd=cmd, combine_stdout_stderr=True)
1457 if output:
Brian Norris0c62a142018-12-11 13:24:29 -08001458 return HookFailure('kernel-doc errors/warnings:',
1459 items=output.splitlines())
Brian Norris23c62e92018-11-14 12:25:51 -08001460
1461
Mike Frysingerae409522014-02-01 03:16:11 -05001462def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001463 """Makes sure kernel config changes are not mixed with code changes"""
1464 files = _get_affected_files(commit)
1465 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
1466 return HookFailure('Changes to chromeos/config/ and regular files must '
1467 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -07001468
Mike Frysingerae409522014-02-01 03:16:11 -05001469
1470def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001471 """Checks that all JSON files are syntactically valid."""
Mike Frysinger908be682018-01-04 02:21:50 -05001472 ret = []
1473
1474 files = _filter_files(_get_affected_files(commit, relative=True),
1475 [r'.*\.json$'])
1476 for f in files:
1477 data = _get_file_content(f, commit)
Dale Curtis2975c432011-05-03 17:25:20 -07001478 try:
Mike Frysinger908be682018-01-04 02:21:50 -05001479 json.loads(data)
1480 except Exception as e:
1481 ret.append('%s: Invalid JSON: %s' % (f, e))
1482
1483 if ret:
1484 return HookFailure('\n'.join(ret))
Dale Curtis2975c432011-05-03 17:25:20 -07001485
1486
Mike Frysingerae409522014-02-01 03:16:11 -05001487def _check_manifests(_project, commit):
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001488 """Make sure Manifest files only have comments & DIST lines."""
1489 ret = []
Mike Frysinger52b537e2013-08-22 22:59:53 -04001490
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001491 manifests = _filter_files(_get_affected_files(commit, relative=True),
1492 [r'.*/Manifest$'])
1493 for path in manifests:
1494 data = _get_file_content(path, commit)
1495
1496 # Disallow blank files.
1497 if not data.strip():
1498 ret.append('%s: delete empty file' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001499 continue
1500
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001501 # Make sure the last newline isn't omitted.
1502 if data[-1] != '\n':
1503 ret.append('%s: missing trailing newline' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001504
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001505 # Do not allow leading or trailing blank lines.
1506 lines = data.splitlines()
1507 if not lines[0]:
1508 ret.append('%s: delete leading blank lines' % (path,))
1509 if not lines[-1]:
1510 ret.append('%s: delete trailing blank lines' % (path,))
1511
1512 for line in lines:
1513 # Disallow leading/trailing whitespace.
1514 if line != line.strip():
1515 ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
1516
1517 # Allow blank lines & comments.
1518 line = line.split('#', 1)[0]
1519 if not line:
1520 continue
1521
1522 # All other linse should start with DIST.
1523 if not line.startswith('DIST '):
1524 ret.append('%s: remove non-DIST lines: %s' % (path, line))
1525 break
1526
1527 if ret:
1528 return HookFailure('\n'.join(ret))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001529
1530
Mike Frysingerae409522014-02-01 03:16:11 -05001531def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001532 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001533 if commit == PRE_SUBMIT:
1534 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001535 BRANCH_RE = r'\nBRANCH=\S+'
1536
1537 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1538 msg = ('Changelist description needs BRANCH field (after first line)\n'
1539 'E.g. BRANCH=none or BRANCH=link,snow')
1540 return HookFailure(msg)
1541
1542
Mike Frysingerae409522014-02-01 03:16:11 -05001543def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001544 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001545 if commit == PRE_SUBMIT:
1546 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001547 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1548
1549 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1550 msg = ('Changelist description needs Signed-off-by: field\n'
1551 'E.g. Signed-off-by: My Name <me@chromium.org>')
1552 return HookFailure(msg)
1553
1554
Aviv Keshet5ac59522017-01-31 14:28:27 -08001555def _check_cq_ini_well_formed(_project, commit):
1556 """Check that any modified COMMIT-QUEUE.ini files are well formed."""
1557 pattern = '.*' + constants.CQ_CONFIG_FILENAME
Mike Frysingerd0523442018-01-03 17:05:29 -05001558 files = _filter_files(_get_affected_files(commit, relative=True), (pattern,))
Aviv Keshet5ac59522017-01-31 14:28:27 -08001559
1560 # TODO(akeshet): Check not only that the file is parseable, but that all the
1561 # pre-cq configs it requests are existing ones.
1562 for f in files:
1563 try:
1564 parser = ConfigParser.SafeConfigParser()
1565 # Prior to python3, ConfigParser has no read_string method, so we must
1566 # pass it either a file path or file like object. And we must use
1567 # _get_file_content to fetch file contents to ensure we are examining the
1568 # commit diff, rather than whatever's on disk.
1569 contents = _get_file_content(f, commit)
1570 parser.readfp(StringIO.StringIO(contents))
1571 except ConfigParser.Error as e:
1572 msg = ('Unable to parse COMMIT-QUEUE.ini file at %s due to %s.' %
1573 (f, e))
1574 return HookFailure(msg)
1575
1576
Jon Salz3ee59de2012-08-18 13:54:22 +08001577def _run_project_hook_script(script, project, commit):
1578 """Runs a project hook script.
1579
1580 The script is run with the following environment variables set:
1581 PRESUBMIT_PROJECT: The affected project
1582 PRESUBMIT_COMMIT: The affected commit
1583 PRESUBMIT_FILES: A newline-separated list of affected files
1584
1585 The script is considered to fail if the exit code is non-zero. It should
1586 write an error message to stdout.
1587 """
1588 env = dict(os.environ)
Alex Deymo643ac4c2015-09-03 10:40:50 -07001589 env['PRESUBMIT_PROJECT'] = project.name
Jon Salz3ee59de2012-08-18 13:54:22 +08001590 env['PRESUBMIT_COMMIT'] = commit
1591
1592 # Put affected files in an environment variable
1593 files = _get_affected_files(commit)
1594 env['PRESUBMIT_FILES'] = '\n'.join(files)
1595
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001596 cmd_result = cros_build_lib.RunCommand(cmd=script,
1597 env=env,
1598 shell=True,
1599 print_cmd=False,
1600 input=os.devnull,
1601 stdout_to_pipe=True,
1602 combine_stdout_stderr=True,
1603 error_code_ok=True)
1604 if cmd_result.returncode:
1605 stdout = cmd_result.output
Jon Salz7b618af2012-08-31 06:03:16 +08001606 if stdout:
1607 stdout = re.sub('(?m)^', ' ', stdout)
1608 return HookFailure('Hook script "%s" failed with code %d%s' %
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001609 (script, cmd_result.returncode,
Jon Salz3ee59de2012-08-18 13:54:22 +08001610 ':\n' + stdout if stdout else ''))
1611
1612
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001613def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05001614 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001615
Brian Norris77608e12018-04-06 10:38:43 -07001616 files = _get_affected_files(commit, include_deletes=True, relative=True)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001617 prefix = os.path.commonprefix(files)
1618 prefix = os.path.dirname(prefix)
1619
1620 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07001621 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001622 return
1623
1624 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07001625
1626 # The common files may all be within a subdirectory of the main project
1627 # directory, so walk up the tree until we find an alias file.
1628 # _get_affected_files() should return relative paths, but check against '/' to
1629 # ensure that this loop terminates even if it receives an absolute path.
1630 while prefix and prefix != '/':
1631 alias_file = os.path.join(prefix, '.project_alias')
1632
1633 # If an alias exists, use it.
1634 if os.path.isfile(alias_file):
1635 project_name = osutils.ReadFile(alias_file).strip()
1636
1637 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001638
1639 if not _get_commit_desc(commit).startswith(project_name + ': '):
1640 return HookFailure('The commit title for changes affecting only %s'
1641 ' should start with \"%s: \"'
1642 % (project_name, project_name))
1643
1644
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001645def _check_filepath_chartype(_project, commit):
1646 """Checks that FilePath::CharType stuff is not used."""
1647
1648 FILEPATH_REGEXP = re.compile('|'.join(
1649 [r'(?:base::)?FilePath::(?:Char|String|StringPiece)Type',
Satoru Takabayashi4ca37922018-08-08 10:16:38 +09001650 r'(?:base::)?FilePath::FromUTF8Unsafe',
1651 r'AsUTF8Unsafe',
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001652 r'FILE_PATH_LITERAL']))
1653 files = _filter_files(_get_affected_files(commit, relative=True),
1654 [r'.*\.(cc|h)$'])
1655
1656 errors = []
1657 for afile in files:
1658 for line_num, line in _get_file_diff(afile, commit):
1659 m = re.search(FILEPATH_REGEXP, line)
1660 if m:
1661 errors.append('%s, line %s has %s' % (afile, line_num, m.group(0)))
1662
1663 if errors:
1664 msg = 'Please assume FilePath::CharType is char (crbug.com/870621):'
1665 return HookFailure(msg, errors)
1666
1667
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001668def _check_exec_files(_project, commit):
1669 """Make +x bits on files."""
1670 # List of files that should never be +x.
1671 NO_EXEC = (
1672 'ChangeLog*',
1673 'COPYING',
1674 'make.conf',
1675 'make.defaults',
1676 'Manifest',
1677 'OWNERS',
1678 'package.use',
1679 'package.keywords',
1680 'package.mask',
1681 'parent',
1682 'README',
1683 'TODO',
1684 '.gitignore',
1685 '*.[achly]',
1686 '*.[ch]xx',
1687 '*.boto',
1688 '*.cc',
1689 '*.cfg',
1690 '*.conf',
1691 '*.config',
1692 '*.cpp',
1693 '*.css',
1694 '*.ebuild',
1695 '*.eclass',
Tatsuhisa Yamaguchi3b053632019-01-10 14:45:14 +09001696 '*.gn',
1697 '*.gni',
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001698 '*.gyp',
1699 '*.gypi',
1700 '*.htm',
1701 '*.html',
1702 '*.ini',
1703 '*.js',
1704 '*.json',
1705 '*.md',
1706 '*.mk',
1707 '*.patch',
1708 '*.policy',
1709 '*.proto',
1710 '*.raw',
1711 '*.rules',
1712 '*.service',
1713 '*.target',
1714 '*.txt',
1715 '*.xml',
1716 '*.yaml',
1717 )
1718
1719 def FinalName(obj):
1720 # If the file is being deleted, then the dst_file is not set.
1721 if obj.dst_file is None:
1722 return obj.src_file
1723 else:
1724 return obj.dst_file
1725
1726 bad_files = []
1727 files = _get_affected_files(commit, relative=True, full_details=True)
1728 for f in files:
1729 mode = int(f.dst_mode, 8)
1730 if not mode & 0o111:
1731 continue
1732 name = FinalName(f)
1733 for no_exec in NO_EXEC:
1734 if fnmatch.fnmatch(name, no_exec):
1735 bad_files.append(name)
1736 break
1737
1738 if bad_files:
1739 return HookFailure('These files should not be executable. '
1740 'Please `chmod -x` them.', bad_files)
1741
1742
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001743# Base
1744
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001745# A list of hooks which are not project specific and check patch description
1746# (as opposed to patch body).
1747_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001748 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001749 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001750 _check_change_has_test_field,
1751 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001752 _check_commit_message_style,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001753 _check_change_is_contribution,
Jack Neus8edbf642019-07-10 16:08:31 -06001754 _check_change_no_include_oem,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001755]
1756
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001757# A list of hooks that are not project-specific
1758_COMMON_HOOKS = [
Aviv Keshet5ac59522017-01-31 14:28:27 -08001759 _check_cq_ini_well_formed,
1760 _check_cros_license,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001761 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001762 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001763 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001764 _check_ebuild_virtual_pv,
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001765 _check_exec_files,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001766 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07001767 _check_gofmt,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001768 _check_layout_conf,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001769 _check_no_long_lines,
Keigo Oka4a09bd92019-05-07 14:01:00 +09001770 _check_no_new_gyp,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001771 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07001772 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001773 _check_portage_make_use_var,
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -06001774 _check_rustfmt,
Aviv Keshet5ac59522017-01-31 14:28:27 -08001775 _check_tabbed_indents,
Ryan Cui9b651632011-05-11 11:38:58 -07001776]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001777
Ryan Cui1562fb82011-05-09 11:01:31 -07001778
Ryan Cui9b651632011-05-11 11:38:58 -07001779# A dictionary of project-specific hooks(callbacks), indexed by project name.
1780# dict[project] = [callback1, callback2]
1781_PROJECT_SPECIFIC_HOOKS = {
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001782 'chromiumos/third_party/kernel': [_kernel_configcheck],
1783 'chromiumos/third_party/kernel-next': [_kernel_configcheck],
Ryan Cui9b651632011-05-11 11:38:58 -07001784}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001785
Ryan Cui1562fb82011-05-09 11:01:31 -07001786
Ryan Cui9b651632011-05-11 11:38:58 -07001787# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04001788# that the flag controls (value).
1789_HOOK_FLAGS = {
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001790 'clang_format_check': _check_clang_format,
Mike Frysingera7642f52015-03-25 18:31:42 -04001791 'checkpatch_check': _run_checkpatch,
Brian Norris23c62e92018-11-14 12:25:51 -08001792 'kerneldoc_check': _run_kerneldoc,
Ryan Cui9b651632011-05-11 11:38:58 -07001793 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04001794 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07001795 'long_line_check': _check_no_long_lines,
Alex Deymof5792ce2015-08-24 22:50:08 -07001796 'cros_license_check': _check_cros_license,
1797 'aosp_license_check': _check_aosp_license,
Ryan Cui9b651632011-05-11 11:38:58 -07001798 'tab_check': _check_no_tabs,
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001799 'tabbed_indent_required_check': _check_tabbed_indents,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001800 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001801 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001802 'bug_field_check': _check_change_has_bug_field,
1803 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07001804 'manifest_check': _check_manifests,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001805 'contribution_check': _check_change_is_contribution,
Mike Frysinger47bd1252018-06-11 12:12:20 -04001806 'project_prefix_check': _check_project_prefix,
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001807 'filepath_chartype_check': _check_filepath_chartype,
Ryan Cui9b651632011-05-11 11:38:58 -07001808}
1809
1810
Mike Frysinger3554bc92015-03-11 04:59:21 -04001811def _get_override_hooks(config):
1812 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001813
1814 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001815
1816 Args:
1817 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001818 """
1819 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001820 SECTION_OPTIONS = 'Hook Overrides Options'
Jon Salz3ee59de2012-08-18 13:54:22 +08001821 if not config.has_section(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001822 return set(), set()
Ryan Cui9b651632011-05-11 11:38:58 -07001823
Mike Frysinger56e8de02019-07-31 14:40:14 -04001824 valid_keys = set(_HOOK_FLAGS.keys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001825 hooks = _HOOK_FLAGS.copy()
Mike Frysinger3554bc92015-03-11 04:59:21 -04001826
1827 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07001828 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001829 for flag in config.options(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001830 if flag not in valid_keys:
1831 raise ValueError('Error: unknown key "%s" in hook section of "%s"' %
1832 (flag, _CONFIG_FILE))
1833
Ryan Cui9b651632011-05-11 11:38:58 -07001834 try:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001835 enabled = config.getboolean(SECTION, flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001836 except ValueError as e:
Mike Frysinger3554bc92015-03-11 04:59:21 -04001837 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
1838 (flag, _CONFIG_FILE, e))
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001839 if enabled:
1840 enable_flags.append(flag)
1841 else:
1842 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001843
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001844 # See if this hook has custom options.
1845 if enabled:
1846 try:
1847 options = config.get(SECTION_OPTIONS, flag)
1848 hooks[flag] = functools.partial(hooks[flag], options=options.split())
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001849 hooks[flag].__name__ = flag
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001850 except (ConfigParser.NoOptionError, ConfigParser.NoSectionError):
1851 pass
1852
1853 enabled_hooks = set(hooks[x] for x in enable_flags)
1854 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger3554bc92015-03-11 04:59:21 -04001855 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001856
1857
Jon Salz3ee59de2012-08-18 13:54:22 +08001858def _get_project_hook_scripts(config):
1859 """Returns a list of project-specific hook scripts.
1860
1861 Args:
1862 config: A ConfigParser for the project's config file.
1863 """
1864 SECTION = 'Hook Scripts'
1865 if not config.has_section(SECTION):
1866 return []
1867
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001868 return config.items(SECTION)
Jon Salz3ee59de2012-08-18 13:54:22 +08001869
1870
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001871def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001872 """Returns a list of hooks that need to be run for a project.
1873
1874 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001875
1876 Args:
1877 project: A string, name of the project.
1878 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001879 """
Jon Salz3ee59de2012-08-18 13:54:22 +08001880 config = ConfigParser.RawConfigParser()
1881 try:
1882 config.read(_CONFIG_FILE)
1883 except ConfigParser.Error:
1884 # Just use an empty config file
1885 config = ConfigParser.RawConfigParser()
1886
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001887 if presubmit:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001888 hooks = _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001889 else:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001890 hooks = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001891
Mike Frysinger3554bc92015-03-11 04:59:21 -04001892 enabled_hooks, disabled_hooks = _get_override_hooks(config)
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001893 hooks = [hook for hook in hooks if hook not in disabled_hooks]
1894
1895 # If a list is both in _COMMON_HOOKS and also enabled explicitly through an
1896 # override, keep the override only. Note that the override may end up being
1897 # a functools.partial, in which case we need to extract the .func to compare
1898 # it to the common hooks.
1899 unwrapped_hooks = [getattr(hook, 'func', hook) for hook in enabled_hooks]
1900 hooks = [hook for hook in hooks if hook not in unwrapped_hooks]
1901
1902 hooks = list(enabled_hooks) + hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001903
1904 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001905 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1906 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001907
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001908 for name, script in _get_project_hook_scripts(config):
1909 func = functools.partial(_run_project_hook_script, script)
1910 func.__name__ = name
1911 hooks.append(func)
Jon Salz3ee59de2012-08-18 13:54:22 +08001912
Ryan Cui9b651632011-05-11 11:38:58 -07001913 return hooks
1914
1915
Alex Deymo643ac4c2015-09-03 10:40:50 -07001916def _run_project_hooks(project_name, proj_dir=None,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001917 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001918 """For each project run its project specific hook from the hooks dictionary.
1919
1920 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001921 project_name: The name of project to run hooks for.
Doug Anderson44a644f2011-11-02 10:37:37 -07001922 proj_dir: If non-None, this is the directory the project is in. If None,
1923 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001924 commit_list: A list of commits to run hooks against. If None or empty list
1925 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001926 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001927
1928 Returns:
1929 Boolean value of whether any errors were ecountered while running the hooks.
1930 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001931 if proj_dir is None:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001932 proj_dirs = _run_command(
1933 ['repo', 'forall', project_name, '-c', 'pwd']).split()
David James2edd9002013-10-11 14:09:19 -07001934 if len(proj_dirs) == 0:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001935 print('%s cannot be found.' % project_name, file=sys.stderr)
David James2edd9002013-10-11 14:09:19 -07001936 print('Please specify a valid project.', file=sys.stderr)
1937 return True
1938 if len(proj_dirs) > 1:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001939 print('%s is associated with multiple directories.' % project_name,
David James2edd9002013-10-11 14:09:19 -07001940 file=sys.stderr)
1941 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1942 return True
1943 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001944
Ryan Cuiec4d6332011-05-02 14:15:25 -07001945 pwd = os.getcwd()
1946 # hooks assume they are run from the root of the project
1947 os.chdir(proj_dir)
1948
Alex Deymo643ac4c2015-09-03 10:40:50 -07001949 remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
1950 '--symbolic-full-name', '@{u}']).strip()
1951 if not remote_branch:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001952 print("Your project %s doesn't track any remote repo." % project_name,
Alex Deymo643ac4c2015-09-03 10:40:50 -07001953 file=sys.stderr)
1954 remote = None
1955 else:
Josh Pratt15d13ab2018-08-13 11:52:48 +10001956 branch_items = remote_branch.split('/', 1)
1957 if len(branch_items) != 2:
1958 PrintErrorForProject(
1959 project_name,
1960 HookFailure(
1961 'Cannot get remote and branch name (%s)' % remote_branch))
1962 os.chdir(pwd)
1963 return True
1964 remote, _branch = branch_items
Alex Deymo643ac4c2015-09-03 10:40:50 -07001965
1966 project = Project(name=project_name, dir=proj_dir, remote=remote)
1967
Doug Anderson14749562013-06-26 13:38:29 -07001968 if not commit_list:
1969 try:
1970 commit_list = _get_commits()
1971 except VerifyException as e:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001972 PrintErrorForProject(project.name, HookFailure(str(e)))
Doug Anderson14749562013-06-26 13:38:29 -07001973 os.chdir(pwd)
1974 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001975
Alex Deymo643ac4c2015-09-03 10:40:50 -07001976 hooks = _get_project_hooks(project.name, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001977 error_found = False
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001978 commit_count = len(commit_list)
Mike Frysingerb99b3772019-08-17 14:19:44 -04001979 hook_count = len(hooks)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001980 for i, commit in enumerate(commit_list):
Mike Frysingerb2496652019-09-12 23:35:46 -04001981 CACHE.clear()
1982
Ryan Cui1562fb82011-05-09 11:01:31 -07001983 error_list = []
Mike Frysingerb99b3772019-08-17 14:19:44 -04001984 for h, hook in enumerate(hooks):
1985 output = ('PRESUBMIT.cfg: [%i/%i]: %s: Running [%i/%i] %s' %
Ben Chaneb806d82019-09-16 11:52:52 -07001986 (i + 1, commit_count, commit, h + 1, hook_count, hook.__name__))
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001987 print(output, end='\r')
1988 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001989 hook_error = hook(project, commit)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001990 print(' ' * len(output), end='\r')
1991 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001992 if hook_error:
Keigo Oka7e880ac2019-07-03 15:03:43 +09001993 if isinstance(hook_error, list):
1994 error_list.extend(hook_error)
1995 else:
1996 error_list.append(hook_error)
Ryan Cui1562fb82011-05-09 11:01:31 -07001997 error_found = True
1998 if error_list:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001999 PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
Ryan Cui1562fb82011-05-09 11:01:31 -07002000 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07002001
Ryan Cuiec4d6332011-05-02 14:15:25 -07002002 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07002003 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002004
Mike Frysingerae409522014-02-01 03:16:11 -05002005
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07002006# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002007
Ryan Cui1562fb82011-05-09 11:01:31 -07002008
Mike Frysingerae409522014-02-01 03:16:11 -05002009def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08002010 """Main function invoked directly by repo.
2011
2012 This function will exit directly upon error so that repo doesn't print some
2013 obscure error message.
2014
2015 Args:
2016 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07002017 worktree_list: A list of directories. It should be the same length as
2018 project_list, so that each entry in project_list matches with a directory
2019 in worktree_list. If None, we will attempt to calculate the directories
2020 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08002021 kwargs: Leave this here for forward-compatibility.
2022 """
Ryan Cui1562fb82011-05-09 11:01:31 -07002023 found_error = False
David James2edd9002013-10-11 14:09:19 -07002024 if not worktree_list:
2025 worktree_list = [None] * len(project_list)
2026 for project, worktree in zip(project_list, worktree_list):
2027 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07002028 found_error = True
2029
Mike Frysingerae409522014-02-01 03:16:11 -05002030 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07002031 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07002032 '- To disable some source style checks, and for other hints, see '
2033 '<checkout_dir>/src/repohooks/README\n'
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002034 "- To upload only current project, run 'repo upload .'")
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04002035 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07002036 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07002037
Ryan Cui1562fb82011-05-09 11:01:31 -07002038
Doug Anderson44a644f2011-11-02 10:37:37 -07002039def _identify_project(path):
2040 """Identify the repo project associated with the given path.
2041
2042 Returns:
2043 A string indicating what project is associated with the path passed in or
2044 a blank string upon failure.
2045 """
2046 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002047 redirect_stderr=True, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002048
2049
Mike Frysinger55f85b52014-12-18 14:45:21 -05002050def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07002051 """Run hooks directly (outside of the context of repo).
2052
Doug Anderson44a644f2011-11-02 10:37:37 -07002053 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05002054 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07002055
2056 Returns:
2057 0 if no pre-upload failures, 1 if failures.
2058
2059 Raises:
2060 BadInvocation: On some types of invocation errors.
2061 """
Mike Frysinger66142932014-12-18 14:55:57 -05002062 parser = commandline.ArgumentParser(description=__doc__)
2063 parser.add_argument('--dir', default=None,
2064 help='The directory that the project lives in. If not '
2065 'specified, use the git project root based on the cwd.')
2066 parser.add_argument('--project', default=None,
2067 help='The project repo path; this can affect how the '
2068 'hooks get run, since some hooks are project-specific. '
2069 'For chromite this is chromiumos/chromite. If not '
2070 'specified, the repo tool will be used to figure this '
2071 'out based on the dir.')
2072 parser.add_argument('--rerun-since', default=None,
Vadim Bendebury75447b92018-01-10 12:06:01 -08002073 help='Rerun hooks on old commits since some point '
2074 'in the past. The argument could be a date (should '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002075 "match git log's concept of a date, e.g. 2012-06-20), "
Vadim Bendebury75447b92018-01-10 12:06:01 -08002076 'or a SHA1, or just a number of commits to check (from 1 '
2077 'to 99). This option is mutually exclusive with '
2078 '--pre-submit.')
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002079 parser.add_argument('--pre-submit', action='store_true',
Mike Frysinger66142932014-12-18 14:55:57 -05002080 help='Run the check against the pending commit. '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002081 "This option should be used at the 'git commit' "
2082 "phase as opposed to 'repo upload'. This option "
Mike Frysinger66142932014-12-18 14:55:57 -05002083 'is mutually exclusive with --rerun-since.')
2084 parser.add_argument('commits', nargs='*',
2085 help='Check specific commits')
2086 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07002087
Doug Anderson14749562013-06-26 13:38:29 -07002088 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05002089 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002090 raise BadInvocation("Can't pass commits and use rerun-since: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002091 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07002092
Vadim Bendebury75447b92018-01-10 12:06:01 -08002093 if len(opts.rerun_since) < 3 and opts.rerun_since.isdigit():
2094 # This must be the number of commits to check. We don't expect the user
2095 # to want to check more than 99 commits.
2096 limit = '-n%s' % opts.rerun_since
2097 elif git.IsSHA1(opts.rerun_since, False):
2098 limit = '%s..' % opts.rerun_since
2099 else:
2100 # This better be a date.
2101 limit = '--since=%s' % opts.rerun_since
2102 cmd = ['git', 'log', limit, '--pretty=%H']
Doug Anderson14749562013-06-26 13:38:29 -07002103 all_commits = _run_command(cmd).splitlines()
2104 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
2105
2106 # Eliminate chrome-bot commits but keep ordering the same...
2107 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05002108 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07002109
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002110 if opts.pre_submit:
2111 raise BadInvocation('rerun-since and pre-submit can not be '
2112 'used together')
2113 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05002114 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002115 raise BadInvocation("Can't pass commits and use pre-submit: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002116 ' '.join(opts.commits))
2117 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07002118
2119 # Check/normlaize git dir; if unspecified, we'll use the root of the git
2120 # project from CWD
2121 if opts.dir is None:
2122 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002123 redirect_stderr=True).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002124 if not git_dir:
2125 raise BadInvocation('The current directory is not part of a git project.')
2126 opts.dir = os.path.dirname(os.path.abspath(git_dir))
2127 elif not os.path.isdir(opts.dir):
2128 raise BadInvocation('Invalid dir: %s' % opts.dir)
2129 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
2130 raise BadInvocation('Not a git directory: %s' % opts.dir)
2131
2132 # Identify the project if it wasn't specified; this _requires_ the repo
2133 # tool to be installed and for the project to be part of a repo checkout.
2134 if not opts.project:
2135 opts.project = _identify_project(opts.dir)
2136 if not opts.project:
2137 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
2138
Doug Anderson14749562013-06-26 13:38:29 -07002139 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05002140 commit_list=opts.commits,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002141 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07002142 if found_error:
2143 return 1
2144 return 0
2145
2146
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002147if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05002148 sys.exit(direct_main(sys.argv[1:]))