blob: 43248bb23227332c320199bb47b680954cc680ad [file] [log] [blame]
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001#!/usr/bin/env python2
Mike Frysingerb7d552e2017-11-23 11:50:47 -05002# -*- coding: utf-8 -*-
Jon Salz98255932012-08-18 14:48:02 +08003# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07004# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
Mike Frysingerae409522014-02-01 03:16:11 -05007"""Presubmit checks to run when doing `repo upload`.
8
9You can add new checks by adding a functions to the HOOKS constants.
10"""
11
Mike Frysinger09d6a3d2013-10-08 22:21:03 -040012from __future__ import print_function
13
Keigo Okadd908822019-06-04 11:30:25 +090014import argparse
Alex Deymo643ac4c2015-09-03 10:40:50 -070015import collections
Keigo Oka7e880ac2019-07-03 15:03:43 +090016import datetime
Daniel Erate3ea3fc2015-02-13 15:27:52 -070017import fnmatch
Jon Salz3ee59de2012-08-18 13:54:22 +080018import functools
Mike Frysinger13302d42019-09-13 17:21:24 -040019import io
Dale Curtis2975c432011-05-03 17:25:20 -070020import json
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070021import os
Ryan Cuiec4d6332011-05-02 14:15:25 -070022import re
Mandeep Singh Bainesa7ffa4b2011-05-03 11:37:02 -070023import sys
Peter Ammon811f6702014-06-12 15:45:38 -070024import stat
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070025
Mike Frysinger7bfc89f2019-09-13 15:45:51 -040026from six.moves import configparser
27
Ryan Cui1562fb82011-05-09 11:01:31 -070028from errors import (VerifyException, HookFailure, PrintErrorForProject,
29 PrintErrorsForCommit)
Ryan Cuiec4d6332011-05-02 14:15:25 -070030
Mike Frysinger6850d512018-05-21 12:12:14 -040031# If repo imports us, the __name__ will be __builtin__, and the cwd will be in
32# the top level of the checkout (i.e. $CHROMEOS_CHECKOUT). chromite will be in
33# that directory, so add it to our path. This works whether we're running the
34# repo in $CHROMEOS_CHECKOUT/.repo/repo/ or a custom version in a completely
35# different tree.
36if __name__ == '__builtin__':
37 sys.path.insert(0, os.getcwd())
38
39# If we're run directly, we'll find chromite relative to the repohooks dir in
40# $CHROMEOS_CHECKOUT/src/repohooks, so go up two dirs.
41if __name__ == '__main__':
David Jamesc3b68b32013-04-03 09:17:03 -070042 sys.path.insert(0, os.path.join(os.path.dirname(sys.argv[0]), '..', '..'))
43
Mike Frysinger66142932014-12-18 14:55:57 -050044from chromite.lib import commandline
Aviv Keshet5ac59522017-01-31 14:28:27 -080045from chromite.lib import constants
Rahul Chaudhry0e515342015-08-07 12:00:43 -070046from chromite.lib import cros_build_lib
Mike Frysingerd3bd32c2014-11-24 23:34:29 -050047from chromite.lib import git
Daniel Erata350fd32014-09-29 14:02:34 -070048from chromite.lib import osutils
David Jamesc3b68b32013-04-03 09:17:03 -070049from chromite.lib import patch
Mike Frysinger2ec70ed2014-08-17 19:28:34 -040050from chromite.licensing import licenses_lib
David Jamesc3b68b32013-04-03 09:17:03 -070051
Vadim Bendebury2b62d742014-06-22 13:14:51 -070052PRE_SUBMIT = 'pre-submit'
Ryan Cuiec4d6332011-05-02 14:15:25 -070053
54COMMON_INCLUDED_PATHS = [
Mike Frysingerae409522014-02-01 03:16:11 -050055 # C++ and friends
Mike Frysinger24dd3c52019-08-17 14:22:48 -040056 r'.*\.c$', r'.*\.cc$', r'.*\.cpp$', r'.*\.h$', r'.*\.m$', r'.*\.mm$',
57 r'.*\.inl$', r'.*\.asm$', r'.*\.hxx$', r'.*\.hpp$', r'.*\.s$', r'.*\.S$',
Mike Frysingerae409522014-02-01 03:16:11 -050058 # Scripts
Mike Frysinger24dd3c52019-08-17 14:22:48 -040059 r'.*\.js$', r'.*\.py$', r'.*\.sh$', r'.*\.rb$', r'.*\.pl$', r'.*\.pm$',
Mike Frysingerae409522014-02-01 03:16:11 -050060 # No extension at all, note that ALL CAPS files are black listed in
61 # COMMON_EXCLUDED_LIST below.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040062 r'(^|.*[\\\/])[^.]+$',
Mike Frysingerae409522014-02-01 03:16:11 -050063 # Other
Mike Frysinger24dd3c52019-08-17 14:22:48 -040064 r'.*\.java$', r'.*\.mk$', r'.*\.am$',
65 r'.*\.policy$', r'.*\.conf$', r'.*\.go$',
66 r'(^OWNERS|/OWNERS)',
Ryan Cuiec4d6332011-05-02 14:15:25 -070067]
68
Ryan Cui1562fb82011-05-09 11:01:31 -070069
Ryan Cuiec4d6332011-05-02 14:15:25 -070070COMMON_EXCLUDED_PATHS = [
Daniel Erate3ea3fc2015-02-13 15:27:52 -070071 # For ebuild trees, ignore any caches and manifest data.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040072 r'.*/Manifest$',
73 r'.*/metadata/[^/]*cache[^/]*/[^/]+/[^/]+$',
Doug Anderson5bfb6792011-10-25 16:45:41 -070074
Daniel Erate3ea3fc2015-02-13 15:27:52 -070075 # Ignore profiles data (like overlay-tegra2/profiles).
Mike Frysinger24dd3c52019-08-17 14:22:48 -040076 r'(^|.*/)overlay-.*/profiles/.*',
77 r'^profiles/.*$',
Mike Frysinger98638102014-08-28 00:15:08 -040078
C Shapiro8f90e9b2017-06-28 09:54:50 -060079 # Ignore config files in ebuild setup.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040080 r'(^|.*/)overlay-.*/chromeos-base/chromeos-bsp.*/files/.*',
81 r'^chromeos-base/chromeos-bsp.*/files/.*',
C Shapiro8f90e9b2017-06-28 09:54:50 -060082
Daniel Erate3ea3fc2015-02-13 15:27:52 -070083 # Ignore minified js and jquery.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040084 r'.*\.min\.js',
85 r'.*jquery.*\.js',
Mike Frysinger33a458d2014-03-03 17:00:51 -050086
87 # Ignore license files as the content is often taken verbatim.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040088 r'.*/licenses/.*',
Alex Klein619c0912019-01-30 17:13:23 -070089
Mike Frysinger13650402019-07-31 14:31:46 -040090 # Exclude generated protobuf bindings.
Mike Frysinger24dd3c52019-08-17 14:22:48 -040091 r'.*_pb2\.py$',
92 r'.*\.pb\.go$',
Ryan Cuiec4d6332011-05-02 14:15:25 -070093]
Mandeep Singh Baines116ad102011-04-27 15:16:37 -070094
Ken Turnerd07564b2018-02-08 17:57:59 +110095LICENSE_EXCLUDED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -040096 r'^(.*/)?OWNERS$',
Ken Turnerd07564b2018-02-08 17:57:59 +110097]
Ryan Cui1562fb82011-05-09 11:01:31 -070098
Ryan Cui9b651632011-05-11 11:38:58 -070099_CONFIG_FILE = 'PRESUBMIT.cfg'
100
101
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700102# File containing wildcards, one per line, matching files that should be
103# excluded from presubmit checks. Lines beginning with '#' are ignored.
104_IGNORE_FILE = '.presubmitignore'
105
Doug Anderson44a644f2011-11-02 10:37:37 -0700106# Exceptions
107
108
109class BadInvocation(Exception):
110 """An Exception indicating a bad invocation of the program."""
111 pass
112
113
Ryan Cui1562fb82011-05-09 11:01:31 -0700114# General Helpers
115
Sean Paulba01d402011-05-05 11:36:23 -0400116
Mike Frysingerb2496652019-09-12 23:35:46 -0400117class Cache(object):
118 """General helper for caching git content."""
119
120 def __init__(self):
121 self._cache = {}
122
123 def get_subcache(self, scope):
124 return self._cache.setdefault(scope, {})
125
126 def clear(self):
127 self._cache.clear()
128
129CACHE = Cache()
130
131
Alex Deymo643ac4c2015-09-03 10:40:50 -0700132Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
133
134
Mike Frysinger526a5f82019-09-13 18:05:30 -0400135def _run_command(cmd, **kwargs):
Doug Anderson44a644f2011-11-02 10:37:37 -0700136 """Executes the passed in command and returns raw stdout output.
137
Mike Frysinger526a5f82019-09-13 18:05:30 -0400138 This is a convenience func to set some RunCommand defaults differently.
139
Doug Anderson44a644f2011-11-02 10:37:37 -0700140 Args:
141 cmd: The command to run; should be a list of strings.
Mike Frysinger526a5f82019-09-13 18:05:30 -0400142 **kwargs: Same as cros_build_lib.RunCommand.
Doug Anderson44a644f2011-11-02 10:37:37 -0700143
144 Returns:
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700145 The stdout from the process (discards stderr and returncode).
Doug Anderson44a644f2011-11-02 10:37:37 -0700146 """
Mike Frysinger526a5f82019-09-13 18:05:30 -0400147 kwargs.setdefault('print_cmd', False)
148 kwargs.setdefault('stdout_to_pipe', True)
149 kwargs.setdefault('error_code_ok', True)
Mike Frysinger71e643e2019-09-13 17:26:39 -0400150 result = cros_build_lib.RunCommand(cmd, **kwargs)
151 return result.output.decode('utf-8', 'replace')
Ryan Cui72834d12011-05-05 14:51:33 -0700152
Ryan Cui1562fb82011-05-09 11:01:31 -0700153
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700154def _get_hooks_dir():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700155 """Returns the absolute path to the repohooks directory."""
Doug Anderson44a644f2011-11-02 10:37:37 -0700156 if __name__ == '__main__':
157 # Works when file is run on its own (__file__ is defined)...
158 return os.path.abspath(os.path.dirname(__file__))
159 else:
160 # We need to do this when we're run through repo. Since repo executes
161 # us with execfile(), we don't get __file__ defined.
162 cmd = ['repo', 'forall', 'chromiumos/repohooks', '-c', 'pwd']
163 return _run_command(cmd).strip()
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700164
Ryan Cui1562fb82011-05-09 11:01:31 -0700165
Ryan Cuiec4d6332011-05-02 14:15:25 -0700166def _match_regex_list(subject, expressions):
167 """Try to match a list of regular expressions to a string.
168
169 Args:
170 subject: The string to match regexes on
171 expressions: A list of regular expressions to check for matches with.
172
173 Returns:
174 Whether the passed in subject matches any of the passed in regexes.
175 """
176 for expr in expressions:
Mike Frysingerae409522014-02-01 03:16:11 -0500177 if re.search(expr, subject):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700178 return True
179 return False
180
Ryan Cui1562fb82011-05-09 11:01:31 -0700181
Mike Frysingerae409522014-02-01 03:16:11 -0500182def _filter_files(files, include_list, exclude_list=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700183 """Filter out files based on the conditions passed in.
184
185 Args:
186 files: list of filepaths to filter
187 include_list: list of regex that when matched with a file path will cause it
188 to be added to the output list unless the file is also matched with a
189 regex in the exclude_list.
190 exclude_list: list of regex that when matched with a file will prevent it
191 from being added to the output list, even if it is also matched with a
192 regex in the include_list.
193
194 Returns:
195 A list of filepaths that contain files matched in the include_list and not
196 in the exclude_list.
197 """
198 filtered = []
199 for f in files:
200 if (_match_regex_list(f, include_list) and
201 not _match_regex_list(f, exclude_list)):
202 filtered.append(f)
203 return filtered
204
Ryan Cuiec4d6332011-05-02 14:15:25 -0700205
206# Git Helpers
Ryan Cui1562fb82011-05-09 11:01:31 -0700207
208
Ryan Cui4725d952011-05-05 15:41:19 -0700209def _get_upstream_branch():
210 """Returns the upstream tracking branch of the current branch.
211
212 Raises:
213 Error if there is no tracking branch
214 """
215 current_branch = _run_command(['git', 'symbolic-ref', 'HEAD']).strip()
216 current_branch = current_branch.replace('refs/heads/', '')
217 if not current_branch:
Ryan Cui1562fb82011-05-09 11:01:31 -0700218 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700219
220 cfg_option = 'branch.' + current_branch + '.%s'
221 full_upstream = _run_command(['git', 'config', cfg_option % 'merge']).strip()
222 remote = _run_command(['git', 'config', cfg_option % 'remote']).strip()
223 if not remote or not full_upstream:
Ryan Cui1562fb82011-05-09 11:01:31 -0700224 raise VerifyException('Need to be on a tracking branch')
Ryan Cui4725d952011-05-05 15:41:19 -0700225
226 return full_upstream.replace('heads', 'remotes/' + remote)
227
Ryan Cui1562fb82011-05-09 11:01:31 -0700228
Che-Liang Chiou5ce2d7b2013-03-22 18:47:55 -0700229def _get_patch(commit):
230 """Returns the patch for this commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700231 if commit == PRE_SUBMIT:
232 return _run_command(['git', 'diff', '--cached', 'HEAD'])
233 else:
234 return _run_command(['git', 'format-patch', '--stdout', '-1', commit])
Mandeep Singh Baines116ad102011-04-27 15:16:37 -0700235
Ryan Cui1562fb82011-05-09 11:01:31 -0700236
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500237def _get_file_content(path, commit):
238 """Returns the content of a file at a specific commit.
239
240 We can't rely on the file as it exists in the filesystem as people might be
241 uploading a series of changes which modifies the file multiple times.
242
243 Note: The "content" of a symlink is just the target. So if you're expecting
244 a full file, you should check that first. One way to detect is that the
245 content will not have any newlines.
246 """
Mike Frysingerb2496652019-09-12 23:35:46 -0400247 # Make sure people don't accidentally pass in full paths which will never
248 # work. You need to use relative=True with _get_affected_files.
249 if path.startswith('/'):
250 raise ValueError('_get_file_content must be called with relative paths: %s'
251 % (path,))
252
253 # {<commit>: {<path1>: <content>, <path2>: <content>}}
254 cache = CACHE.get_subcache('get_file_content')
255 if path in cache:
256 return cache[path]
257
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700258 if commit == PRE_SUBMIT:
Mike Frysingerb2496652019-09-12 23:35:46 -0400259 content = _run_command(['git', 'diff', 'HEAD', path])
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700260 else:
Mike Frysingerb2496652019-09-12 23:35:46 -0400261 content = _run_command(['git', 'show', '%s:%s' % (commit, path)])
262 cache[path] = content
263 return content
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500264
265
Mike Frysingerae409522014-02-01 03:16:11 -0500266def _get_file_diff(path, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700267 """Returns a list of (linenum, lines) tuples that the commit touched."""
Mike Frysingerb2496652019-09-12 23:35:46 -0400268 # {<commit>: {<path1>: <content>, <path2>: <content>}}
269 cache = CACHE.get_subcache('get_file_diff')
270 if path in cache:
271 return cache[path]
272
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700273 if commit == PRE_SUBMIT:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800274 command = ['git', 'diff', '-p', '--pretty=format:', '--no-ext-diff', 'HEAD',
275 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700276 else:
Prathmesh Prabhua9de1722016-12-22 14:56:40 -0800277 command = ['git', 'show', '-p', '--pretty=format:', '--no-ext-diff', commit,
278 path]
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700279 output = _run_command(command)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700280
281 new_lines = []
282 line_num = 0
283 for line in output.splitlines():
284 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line)
285 if m:
286 line_num = int(m.groups(1)[0])
287 continue
288 if line.startswith('+') and not line.startswith('++'):
Mike Frysinger71e643e2019-09-13 17:26:39 -0400289 new_lines.append((line_num, line[1:]))
Ryan Cuiec4d6332011-05-02 14:15:25 -0700290 if not line.startswith('-'):
291 line_num += 1
Mike Frysingerb2496652019-09-12 23:35:46 -0400292 cache[path] = new_lines
Ryan Cuiec4d6332011-05-02 14:15:25 -0700293 return new_lines
294
Ryan Cui1562fb82011-05-09 11:01:31 -0700295
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700296def _get_ignore_wildcards(directory, cache):
297 """Get wildcards listed in a directory's _IGNORE_FILE.
298
299 Args:
300 directory: A string containing a directory path.
301 cache: A dictionary (opaque to caller) caching previously-read wildcards.
302
303 Returns:
304 A list of wildcards from _IGNORE_FILE or an empty list if _IGNORE_FILE
305 wasn't present.
306 """
307 # In the cache, keys are directories and values are lists of wildcards from
308 # _IGNORE_FILE within those directories (and empty if no file was present).
309 if directory not in cache:
310 wildcards = []
311 dotfile_path = os.path.join(directory, _IGNORE_FILE)
312 if os.path.exists(dotfile_path):
313 # TODO(derat): Consider using _get_file_content() to get the file as of
314 # this commit instead of the on-disk version. This may have a noticeable
315 # performance impact, as each call to _get_file_content() runs git.
316 with open(dotfile_path, 'r') as dotfile:
317 for line in dotfile.readlines():
318 line = line.strip()
319 if line.startswith('#'):
320 continue
321 if line.endswith('/'):
322 line += '*'
323 wildcards.append(line)
324 cache[directory] = wildcards
325
326 return cache[directory]
327
328
329def _path_is_ignored(path, cache):
330 """Check whether a path is ignored by _IGNORE_FILE.
331
332 Args:
333 path: A string containing a path.
334 cache: A dictionary (opaque to caller) caching previously-read wildcards.
335
336 Returns:
337 True if a file named _IGNORE_FILE in one of the passed-in path's parent
338 directories contains a wildcard matching the path.
339 """
340 # Skip ignore files.
341 if os.path.basename(path) == _IGNORE_FILE:
342 return True
343
344 path = os.path.abspath(path)
345 base = os.getcwd()
346
347 prefix = os.path.dirname(path)
348 while prefix.startswith(base):
349 rel_path = path[len(prefix) + 1:]
350 for wildcard in _get_ignore_wildcards(prefix, cache):
351 if fnmatch.fnmatch(rel_path, wildcard):
352 return True
353 prefix = os.path.dirname(prefix)
354
355 return False
356
357
Mike Frysinger292b45d2014-11-25 01:17:10 -0500358def _get_affected_files(commit, include_deletes=False, relative=False,
359 include_symlinks=False, include_adds=True,
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700360 full_details=False, use_ignore_files=True):
Peter Ammon811f6702014-06-12 15:45:38 -0700361 """Returns list of file paths that were modified/added, excluding symlinks.
362
363 Args:
364 commit: The commit
365 include_deletes: If true, we'll include deleted files in the result
366 relative: Whether to return relative or full paths to files
Mike Frysinger292b45d2014-11-25 01:17:10 -0500367 include_symlinks: If true, we'll include symlinks in the result
368 include_adds: If true, we'll include new files in the result
369 full_details: If False, return filenames, else return structured results.
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700370 use_ignore_files: Whether we ignore files matched by _IGNORE_FILE files.
Peter Ammon811f6702014-06-12 15:45:38 -0700371
372 Returns:
373 A list of modified/added (and perhaps deleted) files
374 """
Mike Frysinger292b45d2014-11-25 01:17:10 -0500375 if not relative and full_details:
376 raise ValueError('full_details only supports relative paths currently')
377
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700378 if commit == PRE_SUBMIT:
379 return _run_command(['git', 'diff-index', '--cached',
380 '--name-only', 'HEAD']).split()
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500381
382 path = os.getcwd()
Mike Frysingerb2496652019-09-12 23:35:46 -0400383 # {<commit>: {<path1>: <content>, <path2>: <content>}}
384 cache = CACHE.get_subcache('get_affected_files')
385 if path not in cache:
386 cache[path] = git.RawDiff(path, '%s^!' % commit)
387 files = cache[path]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500388
389 # Filter out symlinks.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500390 if not include_symlinks:
391 files = [x for x in files if not stat.S_ISLNK(int(x.dst_mode, 8))]
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500392
393 if not include_deletes:
394 files = [x for x in files if x.status != 'D']
395
Mike Frysinger292b45d2014-11-25 01:17:10 -0500396 if not include_adds:
397 files = [x for x in files if x.status != 'A']
398
Daniel Erate3ea3fc2015-02-13 15:27:52 -0700399 if use_ignore_files:
400 cache = {}
401 is_ignored = lambda x: _path_is_ignored(x.dst_file or x.src_file, cache)
402 files = [x for x in files if not is_ignored(x)]
403
Mike Frysinger292b45d2014-11-25 01:17:10 -0500404 if full_details:
405 # Caller wants the raw objects to parse status/etc... themselves.
Mike Frysingerd3bd32c2014-11-24 23:34:29 -0500406 return files
407 else:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500408 # Caller only cares about filenames.
409 files = [x.dst_file if x.dst_file else x.src_file for x in files]
410 if relative:
411 return files
412 else:
413 return [os.path.join(path, x) for x in files]
Peter Ammon811f6702014-06-12 15:45:38 -0700414
415
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700416def _get_commits():
Ryan Cuiec4d6332011-05-02 14:15:25 -0700417 """Returns a list of commits for this review."""
Mike Frysingere300c7d2019-09-12 23:33:52 -0400418 cmd = ['git', 'log', '--no-merges', '--format=%H',
419 '%s..' % _get_upstream_branch()]
Ryan Cui72834d12011-05-05 14:51:33 -0700420 return _run_command(cmd).split()
Mandeep Singh Bainesb9ed1402011-04-29 15:32:06 -0700421
Ryan Cui1562fb82011-05-09 11:01:31 -0700422
Ryan Cuiec4d6332011-05-02 14:15:25 -0700423def _get_commit_desc(commit):
424 """Returns the full commit message of a commit."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -0700425 if commit == PRE_SUBMIT:
426 return ''
Mike Frysingerb2496652019-09-12 23:35:46 -0400427
428 # {<commit>: <content>}
429 cache = CACHE.get_subcache('get_commit_desc')
430 if commit not in cache:
431 cache[commit] = _run_command(['git', 'log', '--format=%s%n%n%b',
432 commit + '^!'])
433 return cache[commit]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700434
435
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800436def _check_lines_in_diff(commit, files, check_callable, error_description):
437 """Checks given file for errors via the given check.
438
439 This is a convenience function for common per-line checks. It goes through all
440 files and returns a HookFailure with the error description listing all the
441 failures.
442
443 Args:
444 commit: The commit we're working on.
445 files: The files to check.
446 check_callable: A callable that takes a line and returns True if this line
447 _fails_ the check.
448 error_description: A string describing the error.
449 """
450 errors = []
451 for afile in files:
452 for line_num, line in _get_file_diff(afile, commit):
453 if check_callable(line):
454 errors.append('%s, line %s' % (afile, line_num))
455 if errors:
456 return HookFailure(error_description, errors)
457
458
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900459def _parse_common_inclusion_options(options):
460 """Parses common hook options for including/excluding files.
461
462 Args:
463 options: Option string list.
464
465 Returns:
466 (included, excluded) where each one is a list of regex strings.
467 """
468 parser = argparse.ArgumentParser()
469 parser.add_argument('--exclude_regex', action='append')
470 parser.add_argument('--include_regex', action='append')
471 opts = parser.parse_args(options)
472 included = opts.include_regex or []
473 excluded = opts.exclude_regex or []
474 return included, excluded
475
476
Ryan Cuiec4d6332011-05-02 14:15:25 -0700477# Common Hooks
478
Ryan Cui1562fb82011-05-09 11:01:31 -0700479
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900480def _check_no_long_lines(_project, commit, options=()):
Mike Frysinger55f85b52014-12-18 14:45:21 -0500481 """Checks there are no lines longer than MAX_LEN in any of the text files."""
Keigo Oka9732e382019-06-28 17:44:59 +0900482 LONG_LINE_OK_PATHS = [
483 # Go has no line length limit.
484 # https://golang.org/doc/effective_go.html#formatting
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400485 r'.*\.go$',
Keigo Oka9732e382019-06-28 17:44:59 +0900486 ]
Mike Frysinger55f85b52014-12-18 14:45:21 -0500487
Ryan Cuiec4d6332011-05-02 14:15:25 -0700488 MAX_LEN = 80
489
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900490 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700491 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900492 included + COMMON_INCLUDED_PATHS,
Keigo Oka9732e382019-06-28 17:44:59 +0900493 excluded + COMMON_EXCLUDED_PATHS + LONG_LINE_OK_PATHS)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700494
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900495 errors = []
Ryan Cuiec4d6332011-05-02 14:15:25 -0700496 for afile in files:
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700497 skip_regexps = (
498 r'https?://',
499 r'^#\s*(define|include|import|pragma|if|ifndef|endif)\b',
500 )
501
502 if os.path.basename(afile).startswith('OWNERS'):
503 # File paths can get long, and there's no way to break them up into
504 # multiple lines.
505 skip_regexps += (
506 r'^include\b',
507 r'file:',
508 )
509
510 skip_regexps = [re.compile(x) for x in skip_regexps]
Ryan Cuiec4d6332011-05-02 14:15:25 -0700511 for line_num, line in _get_file_diff(afile, commit):
512 # Allow certain lines to exceed the maxlen rule.
George Burgess IVf9f79eb2019-07-09 20:12:55 -0700513 if len(line) <= MAX_LEN or any(x.search(line) for x in skip_regexps):
Jon Salz98255932012-08-18 14:48:02 +0800514 continue
515
516 errors.append('%s, line %s, %s chars' % (afile, line_num, len(line)))
517 if len(errors) == 5: # Just show the first 5 errors.
518 break
Ryan Cuiec4d6332011-05-02 14:15:25 -0700519
520 if errors:
521 msg = 'Found lines longer than %s characters (first 5 shown):' % MAX_LEN
Ryan Cui1562fb82011-05-09 11:01:31 -0700522 return HookFailure(msg, errors)
523
Ryan Cuiec4d6332011-05-02 14:15:25 -0700524
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900525def _check_no_stray_whitespace(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700526 """Checks that there is no stray whitespace at source lines end."""
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900527 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700528 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900529 included + COMMON_INCLUDED_PATHS,
530 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800531 return _check_lines_in_diff(commit, files,
532 lambda line: line.rstrip() != line,
533 'Found line ending with white space in:')
Ryan Cui1562fb82011-05-09 11:01:31 -0700534
Ryan Cuiec4d6332011-05-02 14:15:25 -0700535
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900536def _check_no_tabs(_project, commit, options=()):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700537 """Checks there are no unexpanded tabs."""
Mike Frysingercd134512017-10-26 04:36:33 -0400538 # Don't add entire repos here. Update the PRESUBMIT.cfg in each repo instead.
539 # We only whitelist known specific filetypes here that show up in all repos.
Ryan Cuiec4d6332011-05-02 14:15:25 -0700540 TAB_OK_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400541 r'.*\.ebuild$',
542 r'.*\.eclass$',
543 r'.*\.go$',
544 r'.*/[M|m]akefile$',
545 r'.*\.mk$',
Ryan Cuiec4d6332011-05-02 14:15:25 -0700546 ]
547
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900548 included, excluded = _parse_common_inclusion_options(options)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700549 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900550 included + COMMON_INCLUDED_PATHS,
551 excluded + COMMON_EXCLUDED_PATHS + TAB_OK_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800552 return _check_lines_in_diff(commit, files,
553 lambda line: '\t' in line,
554 'Found a tab character in:')
Ryan Cuiec4d6332011-05-02 14:15:25 -0700555
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800556
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900557def _check_tabbed_indents(_project, commit, options=()):
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800558 """Checks that indents use tabs only."""
559 TABS_REQUIRED_PATHS = [
Mike Frysinger24dd3c52019-08-17 14:22:48 -0400560 r'.*\.ebuild$',
561 r'.*\.eclass$',
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800562 ]
563 LEADING_SPACE_RE = re.compile('[\t]* ')
564
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900565 included, excluded = _parse_common_inclusion_options(options)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800566 files = _filter_files(_get_affected_files(commit),
Shuhei Takahashiabc20f32017-07-10 19:35:45 +0900567 included + TABS_REQUIRED_PATHS,
568 excluded + COMMON_EXCLUDED_PATHS)
Prathmesh Prabhuc5254652016-12-22 12:58:05 -0800569 return _check_lines_in_diff(
570 commit, files,
571 lambda line: LEADING_SPACE_RE.match(line) is not None,
572 'Found a space in indentation (must be all tabs):')
Ryan Cui1562fb82011-05-09 11:01:31 -0700573
Ryan Cuiec4d6332011-05-02 14:15:25 -0700574
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700575def _check_gofmt(_project, commit):
576 """Checks that Go files are formatted with gofmt."""
577 errors = []
578 files = _filter_files(_get_affected_files(commit, relative=True),
579 [r'\.go$'])
580
581 for gofile in files:
582 contents = _get_file_content(gofile, commit)
Rahul Chaudhry0e515342015-08-07 12:00:43 -0700583 output = _run_command(cmd=['gofmt', '-l'], input=contents,
584 combine_stdout_stderr=True)
Rahul Chaudhry09f61372015-07-31 17:14:26 -0700585 if output:
586 errors.append(gofile)
587 if errors:
588 return HookFailure('Files not formatted with gofmt:', errors)
589
590
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -0600591def _check_rustfmt(_project, commit):
592 """Checks that Rust files are formatted with rustfmt."""
593 errors = []
594 files = _filter_files(_get_affected_files(commit, relative=True),
595 [r'\.rs$'])
596
597 for rustfile in files:
598 contents = _get_file_content(rustfile, commit)
599 output = _run_command(cmd=['rustfmt'], input=contents,
600 combine_stdout_stderr=True)
601 if output != contents:
602 errors.append(rustfile)
603 if errors:
604 return HookFailure('Files not formatted with rustfmt: '
605 "(run 'cargo fmt' to fix)", errors)
606
607
Mike Frysingerae409522014-02-01 03:16:11 -0500608def _check_change_has_test_field(_project, commit):
Ryan Cuiec4d6332011-05-02 14:15:25 -0700609 """Check for a non-empty 'TEST=' field in the commit message."""
David McMahon8f6553e2011-06-10 15:46:36 -0700610 TEST_RE = r'\nTEST=\S+'
Ryan Cuiec4d6332011-05-02 14:15:25 -0700611
Mandeep Singh Baines96a53be2011-05-03 11:10:25 -0700612 if not re.search(TEST_RE, _get_commit_desc(commit)):
Ryan Cui1562fb82011-05-09 11:01:31 -0700613 msg = 'Changelist description needs TEST field (after first line)'
614 return HookFailure(msg)
615
Ryan Cuiec4d6332011-05-02 14:15:25 -0700616
Mike Frysingerae409522014-02-01 03:16:11 -0500617def _check_change_has_valid_cq_depend(_project, commit):
Jason D. Clinton299e3222019-05-23 09:42:03 -0600618 """Check for a correctly formatted Cq-Depend field in the commit message."""
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700619 desc = _get_commit_desc(commit)
Jason D. Clinton299e3222019-05-23 09:42:03 -0600620 msg = 'Changelist has invalid Cq-Depend target.'
621 example = 'Example: Cq-Depend: chromium:1234, chrome-internal:2345'
David Jamesc3b68b32013-04-03 09:17:03 -0700622 try:
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700623 patch.GetPaladinDeps(desc)
David Jamesc3b68b32013-04-03 09:17:03 -0700624 except ValueError as ex:
625 return HookFailure(msg, [example, str(ex)])
Luigi Semenzatob8c7d7d2019-06-03 09:43:21 -0700626 # Check that Cq-Depend is in the same paragraph as Change-Id.
627 msg = 'Cq-Depend (or CQ-DEPEND) is not in the same paragraph as Change-Id.'
628 paragraphs = desc.split('\n\n')
629 for paragraph in paragraphs:
630 if (re.search(r'^Cq-Depend:', paragraph, re.M) or
631 re.search(r'^CQ-DEPEND=', paragraph, re.M)) \
632 and not re.search('^Change-Id:', paragraph, re.M):
633 return HookFailure(msg)
David Jamesc3b68b32013-04-03 09:17:03 -0700634
635
Bernie Thompsonf8fea992016-01-14 10:27:18 -0800636def _check_change_is_contribution(_project, commit):
637 """Check that the change is a contribution."""
638 NO_CONTRIB = 'not a contribution'
639 if NO_CONTRIB in _get_commit_desc(commit).lower():
640 msg = ('Changelist is not a contribution, this cannot be accepted.\n'
641 'Please remove the "%s" text from the commit message.') % NO_CONTRIB
642 return HookFailure(msg)
643
644
Alex Deymo643ac4c2015-09-03 10:40:50 -0700645def _check_change_has_bug_field(project, commit):
David McMahon8f6553e2011-06-10 15:46:36 -0700646 """Check for a correctly formatted 'BUG=' field in the commit message."""
David James5c0073d2013-04-03 08:48:52 -0700647 OLD_BUG_RE = r'\nBUG=.*chromium-os'
648 if re.search(OLD_BUG_RE, _get_commit_desc(commit)):
649 msg = ('The chromium-os bug tracker is now deprecated. Please use\n'
650 'the chromium tracker in your BUG= line now.')
651 return HookFailure(msg)
Ryan Cuiec4d6332011-05-02 14:15:25 -0700652
Alex Deymo643ac4c2015-09-03 10:40:50 -0700653 # Android internal and external projects use "Bug: " to track bugs in
654 # buganizer.
655 BUG_COLON_REMOTES = (
656 'aosp',
657 'goog',
658 )
659 if project.remote in BUG_COLON_REMOTES:
660 BUG_RE = r'\nBug: ?([Nn]one|\d+)'
661 if not re.search(BUG_RE, _get_commit_desc(commit)):
662 msg = ('Changelist description needs BUG field (after first line):\n'
663 'Bug: 9999 (for buganizer)\n'
664 'BUG=None')
665 return HookFailure(msg)
666 else:
Jorge Lucangeli Obesdce214e2017-10-25 15:04:39 -0400667 BUG_RE = r'\nBUG=([Nn]one|(chromium|b):\d+)'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700668 if not re.search(BUG_RE, _get_commit_desc(commit)):
669 msg = ('Changelist description needs BUG field (after first line):\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700670 'BUG=chromium:9999 (for public tracker)\n'
Alex Deymo643ac4c2015-09-03 10:40:50 -0700671 'BUG=b:9999 (for buganizer)\n'
672 'BUG=None')
673 return HookFailure(msg)
Ryan Cui1562fb82011-05-09 11:01:31 -0700674
Ryan Cuiec4d6332011-05-02 14:15:25 -0700675
Jack Neus8edbf642019-07-10 16:08:31 -0600676def _check_change_no_include_oem(project, commit):
677 """Check that the change does not reference OEMs."""
678 ALLOWLIST = {
679 'chromiumos/platform/ec',
680 # Used by unit tests.
681 'project',
682 }
683 if project.name not in ALLOWLIST:
684 return None
685
Mike Frysingerbb34a222019-07-31 14:40:46 -0400686 TAGS = {
Jack Neus8edbf642019-07-10 16:08:31 -0600687 'Reviewed-on',
688 'Reviewed-by',
689 'Signed-off-by',
690 'Commit-Ready',
691 'Tested-by',
692 'Commit-Queue',
693 'Legacy-Commit-Queue',
694 'Acked-by',
695 'Modified-by',
696 'CC',
697 'Suggested-by',
698 'Reported-by',
699 'Acked-for-chrome-by',
Mike Frysingerbb34a222019-07-31 14:40:46 -0400700 }
Jack Neus8edbf642019-07-10 16:08:31 -0600701
702 # Ignore tags, which could reasonably contain OEM names
703 # (e.g. Reviewed-by: foo@oem.corp-partner.google.com).
Jack Neus8edbf642019-07-10 16:08:31 -0600704 commit_message = ' '.join(
Mike Frysingerbb34a222019-07-31 14:40:46 -0400705 x for x in _get_commit_desc(commit).splitlines()
706 if ':' not in x or x.split(':', 1)[0] not in TAGS)
707
Jack Neus8edbf642019-07-10 16:08:31 -0600708 commit_message = re.sub(r'[\s_-]+', ' ', commit_message)
709
710 # Exercise caution when expanding these lists. Adding a name
711 # could indicate a new relationship with a company!
712 OEMS = ['hp', 'hewlett packard', 'dell', 'lenovo', 'acer', 'asus', 'samsung']
713 ODMS = [
714 'bitland', 'compal', 'haier', 'huaqin', 'inventec', 'lg', 'pegatron',
715 'pegatron(ems)', 'quanta', 'samsung', 'wistron'
716 ]
717
718 for name_type, name_list in [('OEM', OEMS), ('ODM', ODMS)]:
719 # Construct regex
720 name_re = r'\b(%s)\b' % '|'.join([re.escape(x) for x in name_list])
721 matches = [x[0] for x in re.findall(name_re, commit_message, re.IGNORECASE)]
722 if len(matches):
723 # If there's a match, throw an error.
724 error_msg = ('Changelist description contains the name of an'
725 ' %s: "%s".' % (name_type, '","'.join(matches)))
726 return HookFailure(error_msg)
727
728
Mike Frysinger292b45d2014-11-25 01:17:10 -0500729def _check_for_uprev(project, commit, project_top=None):
Doug Anderson42b8a052013-06-26 10:45:36 -0700730 """Check that we're not missing a revbump of an ebuild in the given commit.
731
732 If the given commit touches files in a directory that has ebuilds somewhere
733 up the directory hierarchy, it's very likely that we need an ebuild revbump
734 in order for those changes to take effect.
735
736 It's not totally trivial to detect a revbump, so at least detect that an
737 ebuild with a revision number in it was touched. This should handle the
738 common case where we use a symlink to do the revbump.
739
740 TODO: it would be nice to enhance this hook to:
741 * Handle cases where people revbump with a slightly different syntax. I see
742 one ebuild (puppy) that revbumps with _pN. This is a false positive.
743 * Catches cases where people aren't using symlinks for revbumps. If they
744 edit a revisioned file directly (and are expected to rename it for revbump)
745 we'll miss that. Perhaps we could detect that the file touched is a
746 symlink?
747
748 If a project doesn't use symlinks we'll potentially miss a revbump, but we're
749 still better off than without this check.
750
751 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700752 project: The Project to look at
Doug Anderson42b8a052013-06-26 10:45:36 -0700753 commit: The commit to look at
Mike Frysinger292b45d2014-11-25 01:17:10 -0500754 project_top: Top dir to process commits in
Doug Anderson42b8a052013-06-26 10:45:36 -0700755
756 Returns:
757 A HookFailure or None.
758 """
Mike Frysinger011af942014-01-17 16:12:22 -0500759 # If this is the portage-stable overlay, then ignore the check. It's rare
760 # that we're doing anything other than importing files from upstream, so
761 # forcing a rev bump makes no sense.
762 whitelist = (
763 'chromiumos/overlays/portage-stable',
764 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700765 if project.name in whitelist:
Mike Frysinger011af942014-01-17 16:12:22 -0500766 return None
767
Mike Frysinger292b45d2014-11-25 01:17:10 -0500768 def FinalName(obj):
769 # If the file is being deleted, then the dst_file is not set.
770 if obj.dst_file is None:
771 return obj.src_file
772 else:
773 return obj.dst_file
774
775 affected_path_objs = _get_affected_files(
776 commit, include_deletes=True, include_symlinks=True, relative=True,
777 full_details=True)
Doug Anderson42b8a052013-06-26 10:45:36 -0700778
779 # Don't yell about changes to whitelisted files...
Aviv Keshet272f2e52016-04-25 14:49:44 -0700780 whitelist = ('ChangeLog', 'Manifest', 'metadata.xml', 'COMMIT-QUEUE.ini')
Mike Frysinger292b45d2014-11-25 01:17:10 -0500781 affected_path_objs = [x for x in affected_path_objs
782 if os.path.basename(FinalName(x)) not in whitelist]
783 if not affected_path_objs:
Doug Anderson42b8a052013-06-26 10:45:36 -0700784 return None
785
786 # If we've touched any file named with a -rN.ebuild then we'll say we're
787 # OK right away. See TODO above about enhancing this.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500788 touched_revved_ebuild = any(re.search(r'-r\d*\.ebuild$', FinalName(x))
789 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700790 if touched_revved_ebuild:
791 return None
792
Mike Frysinger292b45d2014-11-25 01:17:10 -0500793 # If we're creating new ebuilds from scratch, then we don't need an uprev.
794 # Find all the dirs that new ebuilds and ignore their files/.
795 ebuild_dirs = [os.path.dirname(FinalName(x)) + '/' for x in affected_path_objs
796 if FinalName(x).endswith('.ebuild') and x.status == 'A']
797 affected_path_objs = [obj for obj in affected_path_objs
798 if not any(FinalName(obj).startswith(x)
799 for x in ebuild_dirs)]
800 if not affected_path_objs:
801 return
802
Doug Anderson42b8a052013-06-26 10:45:36 -0700803 # We want to examine the current contents of all directories that are parents
804 # of files that were touched (up to the top of the project).
805 #
806 # ...note: we use the current directory contents even though it may have
807 # changed since the commit we're looking at. This is just a heuristic after
808 # all. Worst case we don't flag a missing revbump.
Mike Frysinger292b45d2014-11-25 01:17:10 -0500809 if project_top is None:
810 project_top = os.getcwd()
Doug Anderson42b8a052013-06-26 10:45:36 -0700811 dirs_to_check = set([project_top])
Mike Frysinger292b45d2014-11-25 01:17:10 -0500812 for obj in affected_path_objs:
813 path = os.path.join(project_top, os.path.dirname(FinalName(obj)))
Doug Anderson42b8a052013-06-26 10:45:36 -0700814 while os.path.exists(path) and not os.path.samefile(path, project_top):
815 dirs_to_check.add(path)
816 path = os.path.dirname(path)
817
818 # Look through each directory. If it's got an ebuild in it then we'll
819 # consider this as a case when we need a revbump.
Gwendal Grignoua3086c32014-12-09 11:17:22 -0800820 affected_paths = set(os.path.join(project_top, FinalName(x))
821 for x in affected_path_objs)
Doug Anderson42b8a052013-06-26 10:45:36 -0700822 for dir_path in dirs_to_check:
823 contents = os.listdir(dir_path)
824 ebuilds = [os.path.join(dir_path, path)
825 for path in contents if path.endswith('.ebuild')]
826 ebuilds_9999 = [path for path in ebuilds if path.endswith('-9999.ebuild')]
827
C Shapiroae157ae2017-09-18 16:24:03 -0600828 affected_paths_under_9999_ebuilds = set()
829 for affected_path in affected_paths:
830 for ebuild_9999 in ebuilds_9999:
831 ebuild_dir = os.path.dirname(ebuild_9999)
832 if affected_path.startswith(ebuild_dir):
833 affected_paths_under_9999_ebuilds.add(affected_path)
834
835 # If every file changed exists under a 9999 ebuild, then skip
836 if len(affected_paths_under_9999_ebuilds) == len(affected_paths):
837 continue
838
Doug Anderson42b8a052013-06-26 10:45:36 -0700839 # If the -9999.ebuild file was touched the bot will uprev for us.
840 # ...we'll use a simple intersection here as a heuristic...
Mike Frysinger292b45d2014-11-25 01:17:10 -0500841 if set(ebuilds_9999) & affected_paths:
Doug Anderson42b8a052013-06-26 10:45:36 -0700842 continue
843
844 if ebuilds:
Mike Frysinger292b45d2014-11-25 01:17:10 -0500845 return HookFailure('Changelist probably needs a revbump of an ebuild, '
846 'or a -r1.ebuild symlink if this is a new ebuild:\n'
847 '%s' % dir_path)
Doug Anderson42b8a052013-06-26 10:45:36 -0700848
849 return None
850
851
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500852def _check_ebuild_eapi(project, commit):
Mike Frysinger948284a2018-02-01 15:22:56 -0500853 """Make sure we have people use EAPI=5 or newer with custom ebuilds.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500854
855 We want to get away from older EAPI's as it makes life confusing and they
856 have less builtin error checking.
857
858 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700859 project: The Project to look at
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500860 commit: The commit to look at
861
862 Returns:
863 A HookFailure or None.
864 """
865 # If this is the portage-stable overlay, then ignore the check. It's rare
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500866 # that we're doing anything other than importing files from upstream, and
867 # we shouldn't be rewriting things fundamentally anyways.
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500868 whitelist = (
869 'chromiumos/overlays/portage-stable',
870 )
Alex Deymo643ac4c2015-09-03 10:40:50 -0700871 if project.name in whitelist:
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500872 return None
873
Mike Frysinger948284a2018-02-01 15:22:56 -0500874 BAD_EAPIS = ('0', '1', '2', '3', '4')
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500875
876 get_eapi = re.compile(r'^\s*EAPI=[\'"]?([^\'"]+)')
877
878 ebuilds_re = [r'\.ebuild$']
879 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
880 ebuilds_re)
881 bad_ebuilds = []
882
883 for ebuild in ebuilds:
884 # If the ebuild does not specify an EAPI, it defaults to 0.
885 eapi = '0'
886
887 lines = _get_file_content(ebuild, commit).splitlines()
888 if len(lines) == 1:
889 # This is most likely a symlink, so skip it entirely.
890 continue
891
892 for line in lines:
893 m = get_eapi.match(line)
894 if m:
895 # Once we hit the first EAPI line in this ebuild, stop processing.
896 # The spec requires that there only be one and it be first, so
897 # checking all possible values is pointless. We also assume that
898 # it's "the" EAPI line and not something in the middle of a heredoc.
899 eapi = m.group(1)
900 break
901
902 if eapi in BAD_EAPIS:
903 bad_ebuilds.append((ebuild, eapi))
904
905 if bad_ebuilds:
906 # pylint: disable=C0301
907 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/upgrade-ebuild-eapis'
908 # pylint: enable=C0301
909 return HookFailure(
Mike Frysingercd6adfc2014-02-06 01:03:56 -0500910 'These ebuilds are using old EAPIs. If these are imported from\n'
911 'Gentoo, then you may ignore and upload once with the --no-verify\n'
Mike Frysinger948284a2018-02-01 15:22:56 -0500912 'flag. Otherwise, please update to 5 or newer.\n'
Mike Frysingerbf8b91c2014-02-01 02:50:27 -0500913 '\t%s\n'
914 'See this guide for more details:\n%s\n' %
915 ('\n\t'.join(['%s: EAPI=%s' % x for x in bad_ebuilds]), url))
916
917
Mike Frysinger89bdb852014-02-01 05:26:26 -0500918def _check_ebuild_keywords(_project, commit):
Mike Frysingerc51ece72014-01-17 16:23:40 -0500919 """Make sure we use the new style KEYWORDS when possible in ebuilds.
920
921 If an ebuild generally does not care about the arch it is running on, then
922 ebuilds should flag it with one of:
923 KEYWORDS="*" # A stable ebuild.
924 KEYWORDS="~*" # An unstable ebuild.
925 KEYWORDS="-* ..." # Is known to only work on specific arches.
926
927 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -0700928 project: The Project to look at
Mike Frysingerc51ece72014-01-17 16:23:40 -0500929 commit: The commit to look at
930
931 Returns:
932 A HookFailure or None.
933 """
934 WHITELIST = set(('*', '-*', '~*'))
935
936 get_keywords = re.compile(r'^\s*KEYWORDS="(.*)"')
937
Mike Frysinger89bdb852014-02-01 05:26:26 -0500938 ebuilds_re = [r'\.ebuild$']
939 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
940 ebuilds_re)
941
Mike Frysinger8d42d742014-09-22 15:50:21 -0400942 bad_ebuilds = []
Mike Frysingerc51ece72014-01-17 16:23:40 -0500943 for ebuild in ebuilds:
Mike Frysinger5c9e58d2014-09-09 03:32:50 -0400944 # We get the full content rather than a diff as the latter does not work
945 # on new files (like when adding new ebuilds).
946 lines = _get_file_content(ebuild, commit).splitlines()
947 for line in lines:
Mike Frysingerc51ece72014-01-17 16:23:40 -0500948 m = get_keywords.match(line)
949 if m:
950 keywords = set(m.group(1).split())
951 if not keywords or WHITELIST - keywords != WHITELIST:
952 continue
953
Mike Frysinger8d42d742014-09-22 15:50:21 -0400954 bad_ebuilds.append(ebuild)
955
956 if bad_ebuilds:
957 return HookFailure(
958 '%s\n'
959 'Please update KEYWORDS to use a glob:\n'
960 'If the ebuild should be marked stable (normal for non-9999 ebuilds):\n'
961 ' KEYWORDS="*"\n'
962 'If the ebuild should be marked unstable (normal for '
963 'cros-workon / 9999 ebuilds):\n'
964 ' KEYWORDS="~*"\n'
Mike Frysingerde8efea2015-05-17 03:42:26 -0400965 'If the ebuild needs to be marked for only specific arches, '
Mike Frysinger8d42d742014-09-22 15:50:21 -0400966 'then use -* like so:\n'
967 ' KEYWORDS="-* arm ..."\n' % '\n* '.join(bad_ebuilds))
Mike Frysingerc51ece72014-01-17 16:23:40 -0500968
969
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800970def _check_ebuild_licenses(_project, commit):
971 """Check if the LICENSE field in the ebuild is correct."""
Brian Norris7a610e82016-02-17 12:24:54 -0800972 affected_paths = _get_affected_files(commit, relative=True)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800973 touched_ebuilds = [x for x in affected_paths if x.endswith('.ebuild')]
974
975 # A list of licenses to ignore for now.
Yu-Ju Hongc0963fa2014-03-03 12:36:52 -0800976 LICENSES_IGNORE = ['||', '(', ')']
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800977
978 for ebuild in touched_ebuilds:
979 # Skip virutal packages.
980 if ebuild.split('/')[-3] == 'virtual':
981 continue
982
Alex Kleinb5953522018-08-03 11:44:21 -0600983 # e.g. path/to/overlay/category/package/package.ebuild -> path/to/overlay
984 overlay_path = os.sep.join(ebuild.split(os.sep)[:-3])
985
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800986 try:
Brian Norris7a610e82016-02-17 12:24:54 -0800987 ebuild_content = _get_file_content(ebuild, commit)
Alex Kleinb5953522018-08-03 11:44:21 -0600988 license_types = licenses_lib.GetLicenseTypesFromEbuild(ebuild_content,
989 overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800990 except ValueError as e:
991 return HookFailure(e.message, [ebuild])
992
993 # Also ignore licenses ending with '?'
994 for license_type in [x for x in license_types
995 if x not in LICENSES_IGNORE and not x.endswith('?')]:
996 try:
Alex Kleinb5953522018-08-03 11:44:21 -0600997 licenses_lib.Licensing.FindLicenseType(license_type,
998 overlay_path=overlay_path)
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -0800999 except AssertionError as e:
1000 return HookFailure(e.message, [ebuild])
1001
1002
Mike Frysingercd363c82014-02-01 05:20:18 -05001003def _check_ebuild_virtual_pv(project, commit):
1004 """Enforce the virtual PV policies."""
1005 # If this is the portage-stable overlay, then ignore the check.
1006 # We want to import virtuals as-is from upstream Gentoo.
1007 whitelist = (
1008 'chromiumos/overlays/portage-stable',
1009 )
Alex Deymo643ac4c2015-09-03 10:40:50 -07001010 if project.name in whitelist:
Mike Frysingercd363c82014-02-01 05:20:18 -05001011 return None
1012
1013 # We assume the repo name is the same as the dir name on disk.
1014 # It would be dumb to not have them match though.
Alex Deymo643ac4c2015-09-03 10:40:50 -07001015 project_base = os.path.basename(project.name)
Mike Frysingercd363c82014-02-01 05:20:18 -05001016
1017 is_variant = lambda x: x.startswith('overlay-variant-')
1018 is_board = lambda x: x.startswith('overlay-')
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001019 is_baseboard = lambda x: x.startswith('baseboard-')
1020 is_chipset = lambda x: x.startswith('chipset-')
1021 is_project = lambda x: x.startswith('project-')
Mike Frysingercd363c82014-02-01 05:20:18 -05001022 is_private = lambda x: x.endswith('-private')
1023
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001024 is_special_overlay = lambda x: (is_board(x) or is_chipset(x) or
1025 is_baseboard(x) or is_project(x))
1026
Mike Frysingercd363c82014-02-01 05:20:18 -05001027 get_pv = re.compile(r'(.*?)virtual/([^/]+)/\2-([^/]*)\.ebuild$')
1028
1029 ebuilds_re = [r'\.ebuild$']
1030 ebuilds = _filter_files(_get_affected_files(commit, relative=True),
1031 ebuilds_re)
1032 bad_ebuilds = []
1033
1034 for ebuild in ebuilds:
1035 m = get_pv.match(ebuild)
1036 if m:
1037 overlay = m.group(1)
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001038 if not overlay or not is_special_overlay(overlay):
Alex Deymo643ac4c2015-09-03 10:40:50 -07001039 overlay = project_base
Mike Frysingercd363c82014-02-01 05:20:18 -05001040
1041 pv = m.group(3).split('-', 1)[0]
1042
Bernie Thompsone5ee1822016-01-12 14:22:23 -08001043 # Virtual versions >= 4 are special cases used above the standard
1044 # versioning structure, e.g. if one has a board inheriting a board.
1045 if float(pv) >= 4:
1046 want_pv = pv
Mike Frysingercd363c82014-02-01 05:20:18 -05001047 elif is_board(overlay):
Douglas Andersonb43df7f2018-06-25 13:40:50 -07001048 if is_private(overlay):
1049 want_pv = '3.5' if is_variant(overlay) else '3'
1050 elif is_board(overlay):
1051 want_pv = '2.5' if is_variant(overlay) else '2'
1052 elif is_baseboard(overlay):
1053 want_pv = '1.9'
1054 elif is_chipset(overlay):
1055 want_pv = '1.8'
1056 elif is_project(overlay):
1057 want_pv = '1.7' if is_private(overlay) else '1.5'
Mike Frysingercd363c82014-02-01 05:20:18 -05001058 else:
1059 want_pv = '1'
1060
1061 if pv != want_pv:
1062 bad_ebuilds.append((ebuild, pv, want_pv))
1063
1064 if bad_ebuilds:
1065 # pylint: disable=C0301
1066 url = 'http://dev.chromium.org/chromium-os/how-tos-and-troubleshooting/portage-build-faq#TOC-Virtuals-and-central-management'
1067 # pylint: enable=C0301
1068 return HookFailure(
1069 'These virtuals have incorrect package versions (PVs). Please adjust:\n'
1070 '\t%s\n'
1071 'If this is an upstream Gentoo virtual, then you may ignore this\n'
1072 'check (and re-run w/--no-verify). Otherwise, please see this\n'
1073 'page for more details:\n%s\n' %
1074 ('\n\t'.join(['%s:\n\t\tPV is %s but should be %s' % x
1075 for x in bad_ebuilds]), url))
1076
1077
Daniel Erat9d203ff2015-02-17 10:12:21 -07001078def _check_portage_make_use_var(_project, commit):
1079 """Verify that $USE is set correctly in make.conf and make.defaults."""
1080 files = _filter_files(_get_affected_files(commit, relative=True),
1081 [r'(^|/)make.(conf|defaults)$'])
1082
1083 errors = []
1084 for path in files:
1085 basename = os.path.basename(path)
1086
1087 # Has a USE= line already been encountered in this file?
1088 saw_use = False
1089
1090 for i, line in enumerate(_get_file_content(path, commit).splitlines(), 1):
1091 if not line.startswith('USE='):
1092 continue
1093
1094 preserves_use = '${USE}' in line or '$USE' in line
1095
1096 if (basename == 'make.conf' or
1097 (basename == 'make.defaults' and saw_use)) and not preserves_use:
1098 errors.append('%s:%d: missing ${USE}' % (path, i))
1099 elif basename == 'make.defaults' and not saw_use and preserves_use:
1100 errors.append('%s:%d: ${USE} referenced in initial declaration' %
1101 (path, i))
1102
1103 saw_use = True
1104
1105 if errors:
1106 return HookFailure(
1107 'One or more Portage make files appear to set USE incorrectly.\n'
1108 '\n'
1109 'All USE assignments in make.conf and all assignments after the\n'
1110 'initial declaration in make.defaults should contain "${USE}" to\n'
1111 'preserve previously-set flags.\n'
1112 '\n'
1113 'The initial USE declaration in make.defaults should not contain\n'
1114 '"${USE}".\n',
1115 errors)
1116
1117
Mike Frysingerae409522014-02-01 03:16:11 -05001118def _check_change_has_proper_changeid(_project, commit):
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001119 """Verify that Change-ID is present in last paragraph of commit message."""
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001120 CHANGE_ID_RE = r'\nChange-Id: I[a-f0-9]+\n'
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001121 desc = _get_commit_desc(commit)
Mike Frysinger4a22bf02014-10-31 13:53:35 -04001122 m = re.search(CHANGE_ID_RE, desc)
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001123 if not m:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001124 return HookFailure('Last paragraph of description must include Change-Id.')
Ryan Cui1562fb82011-05-09 11:01:31 -07001125
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001126 # S-o-b tags always allowed to follow Change-ID.
1127 allowed_tags = ['Signed-off-by']
1128
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001129 end = desc[m.end():].strip().splitlines()
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001130 cherry_pick_marker = 'cherry picked from commit'
1131
1132 if end and cherry_pick_marker in end[-1]:
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001133 # Cherry picked patches allow more tags in the last paragraph.
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001134 allowed_tags += ['Commit-Queue', 'Commit-Ready', 'Reviewed-by',
1135 'Reviewed-on', 'Tested-by']
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001136 end = end[:-1]
1137
Vadim Bendebury6504aea2017-09-13 18:35:49 -07001138 # Note that descriptions could have multiple cherry pick markers.
1139 tag_search = r'^(%s:|\(%s) ' % (':|'.join(allowed_tags), cherry_pick_marker)
Vadim Bendebury20532ba2017-05-23 17:26:15 -07001140
1141 if [x for x in end if not re.search(tag_search, x)]:
1142 return HookFailure('Only "%s:" tag(s) may follow the Change-Id.' %
1143 ':", "'.join(allowed_tags))
Mike Frysinger02b88bd2014-11-21 00:29:38 -05001144
Mandeep Singh Bainesa23eb5f2011-05-04 13:43:25 -07001145
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001146def _check_commit_message_style(_project, commit):
1147 """Verify that the commit message matches our style.
1148
1149 We do not check for BUG=/TEST=/etc... lines here as that is handled by other
1150 commit hooks.
1151 """
1152 desc = _get_commit_desc(commit)
1153
1154 # The first line should be by itself.
1155 lines = desc.splitlines()
1156 if len(lines) > 1 and lines[1]:
1157 return HookFailure('The second line of the commit message must be blank.')
1158
1159 # The first line should be one sentence.
1160 if '. ' in lines[0]:
1161 return HookFailure('The first line cannot be more than one sentence.')
1162
1163 # The first line cannot be too long.
1164 MAX_FIRST_LINE_LEN = 100
1165 if len(lines[0]) > MAX_FIRST_LINE_LEN:
1166 return HookFailure('The first line must be less than %i chars.' %
1167 MAX_FIRST_LINE_LEN)
1168
1169
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001170def _check_cros_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001171 """Verifies the Chromium OS license/copyright header.
Ryan Cuiec4d6332011-05-02 14:15:25 -07001172
Mike Frysinger98638102014-08-28 00:15:08 -04001173 Should be following the spec:
1174 http://dev.chromium.org/developers/coding-style#TOC-File-headers
1175 """
1176 # For older years, be a bit more flexible as our policy says leave them be.
1177 LICENSE_HEADER = (
Keigo Oka7e880ac2019-07-03 15:03:43 +09001178 r'.*Copyright(?: \(c\))? (20[0-9]{2})(?:-20[0-9]{2})? The Chromium OS '
1179 r'Authors\. All rights reserved\.\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001180 r'.*Use of this source code is governed by a BSD-style license that can '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001181 r'be\n'
Brian Norris68838dd2018-09-26 18:30:24 -07001182 r'.*found in the LICENSE file\.'
Mike Frysingerb81102f2014-11-21 00:33:35 -05001183 r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001184 )
1185 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1186
1187 # For newer years, be stricter.
Keigo Oka7e880ac2019-07-03 15:03:43 +09001188 BAD_COPYRIGHT_LINE = (
Brian Norris68838dd2018-09-26 18:30:24 -07001189 r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
Mike Frysingerb81102f2014-11-21 00:33:35 -05001190 r'All rights reserved\.' r'\n'
Mike Frysinger98638102014-08-28 00:15:08 -04001191 )
Keigo Oka7e880ac2019-07-03 15:03:43 +09001192 bad_copyright_re = re.compile(BAD_COPYRIGHT_LINE)
Mike Frysinger98638102014-08-28 00:15:08 -04001193
Shuhei Takahashiabc20f32017-07-10 19:35:45 +09001194 included, excluded = _parse_common_inclusion_options(options)
Filipe Brandenburger4b542b12015-10-09 12:46:31 -07001195
Mike Frysinger98638102014-08-28 00:15:08 -04001196 bad_files = []
1197 bad_copyright_files = []
Keigo Oka7e880ac2019-07-03 15:03:43 +09001198 bad_year_files = []
1199
Ken Turnerd07564b2018-02-08 17:57:59 +11001200 files = _filter_files(
1201 _get_affected_files(commit, relative=True),
1202 included + COMMON_INCLUDED_PATHS,
1203 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001204 existing_files = set(_get_affected_files(commit, relative=True,
1205 include_adds=False))
Mike Frysinger98638102014-08-28 00:15:08 -04001206
Keigo Oka7e880ac2019-07-03 15:03:43 +09001207 current_year = str(datetime.datetime.now().year)
Mike Frysinger98638102014-08-28 00:15:08 -04001208 for f in files:
1209 contents = _get_file_content(f, commit)
1210 if not contents:
1211 # Ignore empty files.
1212 continue
1213
Keigo Oka7e880ac2019-07-03 15:03:43 +09001214 m = license_re.search(contents)
1215 if not m:
Mike Frysinger98638102014-08-28 00:15:08 -04001216 bad_files.append(f)
Keigo Oka7e880ac2019-07-03 15:03:43 +09001217 elif bad_copyright_re.search(contents):
Mike Frysinger98638102014-08-28 00:15:08 -04001218 bad_copyright_files.append(f)
1219
Keigo Oka7e880ac2019-07-03 15:03:43 +09001220 if m and f not in existing_files:
1221 year = m.group(1)
1222 if year != current_year:
1223 bad_year_files.append(f)
1224
1225 errors = []
Mike Frysinger98638102014-08-28 00:15:08 -04001226 if bad_files:
1227 msg = '%s:\n%s\n%s' % (
1228 'License must match', license_re.pattern,
1229 'Found a bad header in these files:')
Keigo Oka7e880ac2019-07-03 15:03:43 +09001230 errors.append(HookFailure(msg, bad_files))
Mike Frysinger98638102014-08-28 00:15:08 -04001231 if bad_copyright_files:
1232 msg = 'Do not use (c) in copyright headers in new files:'
Keigo Oka7e880ac2019-07-03 15:03:43 +09001233 errors.append(HookFailure(msg, bad_copyright_files))
1234 if bad_year_files:
1235 msg = 'Use current year (%s) in copyright headers in new files:' % (
1236 current_year)
1237 errors.append(HookFailure(msg, bad_year_files))
Ryan Cuiec4d6332011-05-02 14:15:25 -07001238
Keigo Oka7e880ac2019-07-03 15:03:43 +09001239 return errors
Ryan Cuiec4d6332011-05-02 14:15:25 -07001240
Amin Hassani391efa92018-01-26 17:58:05 -08001241def _check_aosp_license(_project, commit, options=()):
Alex Deymof5792ce2015-08-24 22:50:08 -07001242 """Verifies the AOSP license/copyright header.
1243
1244 AOSP uses the Apache2 License:
1245 https://source.android.com/source/licenses.html
1246 """
1247 LICENSE_HEADER = (
1248 r"""^[#/\*]*
1249[#/\*]* ?Copyright( \([cC]\))? 20[-0-9]{2,7} The Android Open Source Project
1250[#/\*]* ?
1251[#/\*]* ?Licensed under the Apache License, Version 2.0 \(the "License"\);
1252[#/\*]* ?you may not use this file except in compliance with the License\.
1253[#/\*]* ?You may obtain a copy of the License at
1254[#/\*]* ?
1255[#/\*]* ? http://www\.apache\.org/licenses/LICENSE-2\.0
1256[#/\*]* ?
1257[#/\*]* ?Unless required by applicable law or agreed to in writing, software
1258[#/\*]* ?distributed under the License is distributed on an "AS IS" BASIS,
1259[#/\*]* ?WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or """
1260 r"""implied\.
1261[#/\*]* ?See the License for the specific language governing permissions and
1262[#/\*]* ?limitations under the License\.
1263[#/\*]*$
1264"""
1265 )
1266 license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
1267
Amin Hassani391efa92018-01-26 17:58:05 -08001268 included, excluded = _parse_common_inclusion_options(options)
1269
Ken Turnerd07564b2018-02-08 17:57:59 +11001270 files = _filter_files(
1271 _get_affected_files(commit, relative=True),
1272 included + COMMON_INCLUDED_PATHS,
1273 excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
Alex Deymof5792ce2015-08-24 22:50:08 -07001274
1275 bad_files = []
1276 for f in files:
1277 contents = _get_file_content(f, commit)
1278 if not contents:
1279 # Ignore empty files.
1280 continue
1281
1282 if not license_re.search(contents):
1283 bad_files.append(f)
1284
1285 if bad_files:
1286 msg = ('License must match:\n%s\nFound a bad header in these files:' %
1287 license_re.pattern)
1288 return HookFailure(msg, bad_files)
1289
1290
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001291def _check_layout_conf(_project, commit):
1292 """Verifies the metadata/layout.conf file."""
1293 repo_name = 'profiles/repo_name'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001294 repo_names = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001295 layout_path = 'metadata/layout.conf'
Mike Frysinger94a670c2014-09-19 12:46:26 -04001296 layout_paths = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001297
Mike Frysinger94a670c2014-09-19 12:46:26 -04001298 # Handle multiple overlays in a single commit (like the public tree).
1299 for f in _get_affected_files(commit, relative=True):
1300 if f.endswith(repo_name):
1301 repo_names.append(f)
1302 elif f.endswith(layout_path):
1303 layout_paths.append(f)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001304
1305 # Disallow new repos with the repo_name file.
Mike Frysinger94a670c2014-09-19 12:46:26 -04001306 if repo_names:
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001307 return HookFailure('%s: use "repo-name" in %s instead' %
Mike Frysinger94a670c2014-09-19 12:46:26 -04001308 (repo_names, layout_path))
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001309
Mike Frysinger94a670c2014-09-19 12:46:26 -04001310 # Gather all the errors in one pass so we show one full message.
1311 all_errors = {}
1312 for layout_path in layout_paths:
1313 all_errors[layout_path] = errors = []
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001314
Mike Frysinger94a670c2014-09-19 12:46:26 -04001315 # Make sure the config file is sorted.
1316 data = [x for x in _get_file_content(layout_path, commit).splitlines()
1317 if x and x[0] != '#']
1318 if sorted(data) != data:
1319 errors += ['keep lines sorted']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001320
Mike Frysinger94a670c2014-09-19 12:46:26 -04001321 # Require people to set specific values all the time.
1322 settings = (
1323 # TODO: Enable this for everyone. http://crbug.com/408038
1324 #('fast caching', 'cache-format = md5-dict'),
1325 ('fast manifests', 'thin-manifests = true'),
Mike Frysingerd7734522015-02-26 16:12:43 -05001326 ('extra features', 'profile-formats = portage-2 profile-default-eapi'),
1327 ('newer eapi', 'profile_eapi_when_unspecified = 5-progress'),
Mike Frysinger94a670c2014-09-19 12:46:26 -04001328 )
1329 for reason, line in settings:
1330 if line not in data:
1331 errors += ['enable %s with: %s' % (reason, line)]
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001332
Mike Frysinger94a670c2014-09-19 12:46:26 -04001333 # Require one of these settings.
Mike Frysinger5fae62d2015-11-11 20:12:15 -05001334 if 'use-manifests = strict' not in data:
1335 errors += ['enable file checking with: use-manifests = strict']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001336
Mike Frysinger94a670c2014-09-19 12:46:26 -04001337 # Require repo-name to be set.
Mike Frysinger324cf682014-09-22 15:52:50 -04001338 for line in data:
1339 if line.startswith('repo-name = '):
1340 break
1341 else:
Mike Frysinger94a670c2014-09-19 12:46:26 -04001342 errors += ['set the board name with: repo-name = $BOARD']
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001343
Mike Frysinger94a670c2014-09-19 12:46:26 -04001344 # Summarize all the errors we saw (if any).
1345 lines = ''
1346 for layout_path, errors in all_errors.items():
1347 if errors:
1348 lines += '\n\t- '.join(['\n* %s:' % layout_path] + errors)
1349 if lines:
1350 lines = 'See the portage(5) man page for layout.conf details' + lines + '\n'
1351 return HookFailure(lines)
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001352
1353
Keigo Oka4a09bd92019-05-07 14:01:00 +09001354def _check_no_new_gyp(_project, commit):
1355 """Verifies no project starts to use GYP."""
1356 whitelist = [
Keigo Oka4a09bd92019-05-07 14:01:00 +09001357 'chromeos/ap',
1358 'chromeos/ap-daemons',
Keigo Oka150a6fd2019-06-04 11:30:25 +09001359 'chromeos/ap/security',
1360 'chromeos/ap/wireless',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001361 'chromeos/platform/actions',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001362 'chromeos/platform/drivefs-google3',
1363 'chromeos/platform/experimental-touch-fw',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001364 'chromeos/thermald',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001365 'chromiumos/platform2',
Keigo Oka4a09bd92019-05-07 14:01:00 +09001366 'weave/libweave',
1367 ]
1368 if _project.name in whitelist:
1369 return None
1370
1371 gypfiles = _filter_files(
1372 _get_affected_files(commit, include_symlinks=True, relative=True),
1373 [r'\.gyp$'])
1374
1375 if gypfiles:
1376 return HookFailure('GYP is deprecated and not allowed in a new project:',
1377 gypfiles)
1378
1379
Ryan Cuiec4d6332011-05-02 14:15:25 -07001380# Project-specific hooks
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001381
Ryan Cui1562fb82011-05-09 11:01:31 -07001382
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001383def _check_clang_format(_project, commit, options=()):
1384 """Runs clang-format on the given project"""
1385 hooks_dir = _get_hooks_dir()
1386 options = list(options)
1387 if commit == PRE_SUBMIT:
1388 options.append('--commit=HEAD')
1389 else:
1390 options.extend(['--commit', commit])
Brian Norris0c62a142018-12-11 13:24:29 -08001391 cmd = [os.path.join(hooks_dir, 'clang-format.py')] + options
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001392 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1393 print_cmd=False,
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001394 stdout_to_pipe=True,
1395 combine_stdout_stderr=True,
1396 error_code_ok=True)
1397 if cmd_result.returncode:
1398 return HookFailure('clang-format.py errors/warnings\n\n' +
1399 cmd_result.output)
1400
1401
Mike Frysingerae409522014-02-01 03:16:11 -05001402def _run_checkpatch(_project, commit, options=()):
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001403 """Runs checkpatch.pl on the given project"""
1404 hooks_dir = _get_hooks_dir()
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001405 options = list(options)
1406 if commit == PRE_SUBMIT:
1407 # The --ignore option must be present and include 'MISSING_SIGN_OFF' in
1408 # this case.
1409 options.append('--ignore=MISSING_SIGN_OFF')
Filipe Brandenburger28d48d62015-10-07 09:48:54 -07001410 # Always ignore the check for the MAINTAINERS file. We do not track that
1411 # information on that file in our source trees, so let's suppress the
1412 # warning.
1413 options.append('--ignore=FILE_PATH_CHANGES')
Filipe Brandenburgerce978322015-10-09 10:04:15 -07001414 # Do not complain about the Change-Id: fields, since we use Gerrit.
1415 # Upstream does not want those lines (since they do not use Gerrit), but
1416 # we always do, so disable the check globally.
Daisuke Nojiri4844f892015-10-08 09:54:33 -07001417 options.append('--ignore=GERRIT_CHANGE_ID')
Brian Norris0c62a142018-12-11 13:24:29 -08001418 cmd = [os.path.join(hooks_dir, 'checkpatch.pl')] + options + ['-']
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001419 cmd_result = cros_build_lib.RunCommand(cmd=cmd,
1420 print_cmd=False,
1421 input=_get_patch(commit),
1422 stdout_to_pipe=True,
1423 combine_stdout_stderr=True,
1424 error_code_ok=True)
1425 if cmd_result.returncode:
1426 return HookFailure('checkpatch.pl errors/warnings\n\n' + cmd_result.output)
Ryan Cuiec4d6332011-05-02 14:15:25 -07001427
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001428
Brian Norris23c62e92018-11-14 12:25:51 -08001429def _run_kerneldoc(_project, commit, options=()):
1430 """Runs kernel-doc validator on the given project"""
1431 included, excluded = _parse_common_inclusion_options(options)
1432 files = _filter_files(_get_affected_files(commit, relative=True),
1433 included, excluded)
1434 if files:
1435 hooks_dir = _get_hooks_dir()
Brian Norris0c62a142018-12-11 13:24:29 -08001436 cmd = [os.path.join(hooks_dir, 'kernel-doc'), '-none'] + files
Brian Norris23c62e92018-11-14 12:25:51 -08001437 output = _run_command(cmd=cmd, combine_stdout_stderr=True)
1438 if output:
Brian Norris0c62a142018-12-11 13:24:29 -08001439 return HookFailure('kernel-doc errors/warnings:',
1440 items=output.splitlines())
Brian Norris23c62e92018-11-14 12:25:51 -08001441
1442
Mike Frysingerae409522014-02-01 03:16:11 -05001443def _kernel_configcheck(_project, commit):
Olof Johanssona96810f2012-09-04 16:20:03 -07001444 """Makes sure kernel config changes are not mixed with code changes"""
1445 files = _get_affected_files(commit)
1446 if not len(_filter_files(files, [r'chromeos/config'])) in [0, len(files)]:
1447 return HookFailure('Changes to chromeos/config/ and regular files must '
1448 'be in separate commits:\n%s' % '\n'.join(files))
Anton Staaf815d6852011-08-22 10:08:45 -07001449
Mike Frysingerae409522014-02-01 03:16:11 -05001450
1451def _run_json_check(_project, commit):
Dale Curtis2975c432011-05-03 17:25:20 -07001452 """Checks that all JSON files are syntactically valid."""
Mike Frysinger908be682018-01-04 02:21:50 -05001453 ret = []
1454
1455 files = _filter_files(_get_affected_files(commit, relative=True),
1456 [r'.*\.json$'])
1457 for f in files:
1458 data = _get_file_content(f, commit)
Dale Curtis2975c432011-05-03 17:25:20 -07001459 try:
Mike Frysinger908be682018-01-04 02:21:50 -05001460 json.loads(data)
1461 except Exception as e:
1462 ret.append('%s: Invalid JSON: %s' % (f, e))
1463
1464 if ret:
1465 return HookFailure('\n'.join(ret))
Dale Curtis2975c432011-05-03 17:25:20 -07001466
1467
Mike Frysingerae409522014-02-01 03:16:11 -05001468def _check_manifests(_project, commit):
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001469 """Make sure Manifest files only have comments & DIST lines."""
1470 ret = []
Mike Frysinger52b537e2013-08-22 22:59:53 -04001471
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001472 manifests = _filter_files(_get_affected_files(commit, relative=True),
1473 [r'.*/Manifest$'])
1474 for path in manifests:
1475 data = _get_file_content(path, commit)
1476
1477 # Disallow blank files.
1478 if not data.strip():
1479 ret.append('%s: delete empty file' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001480 continue
1481
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001482 # Make sure the last newline isn't omitted.
1483 if data[-1] != '\n':
1484 ret.append('%s: missing trailing newline' % (path,))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001485
Mike Frysingeraae3cb52018-01-03 16:49:33 -05001486 # Do not allow leading or trailing blank lines.
1487 lines = data.splitlines()
1488 if not lines[0]:
1489 ret.append('%s: delete leading blank lines' % (path,))
1490 if not lines[-1]:
1491 ret.append('%s: delete trailing blank lines' % (path,))
1492
1493 for line in lines:
1494 # Disallow leading/trailing whitespace.
1495 if line != line.strip():
1496 ret.append('%s: remove leading/trailing whitespace: %s' % (path, line))
1497
1498 # Allow blank lines & comments.
1499 line = line.split('#', 1)[0]
1500 if not line:
1501 continue
1502
1503 # All other linse should start with DIST.
1504 if not line.startswith('DIST '):
1505 ret.append('%s: remove non-DIST lines: %s' % (path, line))
1506 break
1507
1508 if ret:
1509 return HookFailure('\n'.join(ret))
Mike Frysinger52b537e2013-08-22 22:59:53 -04001510
1511
Mike Frysingerae409522014-02-01 03:16:11 -05001512def _check_change_has_branch_field(_project, commit):
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001513 """Check for a non-empty 'BRANCH=' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001514 if commit == PRE_SUBMIT:
1515 return
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001516 BRANCH_RE = r'\nBRANCH=\S+'
1517
1518 if not re.search(BRANCH_RE, _get_commit_desc(commit)):
1519 msg = ('Changelist description needs BRANCH field (after first line)\n'
1520 'E.g. BRANCH=none or BRANCH=link,snow')
1521 return HookFailure(msg)
1522
1523
Mike Frysingerae409522014-02-01 03:16:11 -05001524def _check_change_has_signoff_field(_project, commit):
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001525 """Check for a non-empty 'Signed-off-by:' field in the commit message."""
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001526 if commit == PRE_SUBMIT:
1527 return
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001528 SIGNOFF_RE = r'\nSigned-off-by: \S+'
1529
1530 if not re.search(SIGNOFF_RE, _get_commit_desc(commit)):
1531 msg = ('Changelist description needs Signed-off-by: field\n'
1532 'E.g. Signed-off-by: My Name <me@chromium.org>')
1533 return HookFailure(msg)
1534
1535
Aviv Keshet5ac59522017-01-31 14:28:27 -08001536def _check_cq_ini_well_formed(_project, commit):
1537 """Check that any modified COMMIT-QUEUE.ini files are well formed."""
1538 pattern = '.*' + constants.CQ_CONFIG_FILENAME
Mike Frysingerd0523442018-01-03 17:05:29 -05001539 files = _filter_files(_get_affected_files(commit, relative=True), (pattern,))
Aviv Keshet5ac59522017-01-31 14:28:27 -08001540
1541 # TODO(akeshet): Check not only that the file is parseable, but that all the
1542 # pre-cq configs it requests are existing ones.
1543 for f in files:
1544 try:
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001545 parser = configparser.SafeConfigParser()
Aviv Keshet5ac59522017-01-31 14:28:27 -08001546 # Prior to python3, ConfigParser has no read_string method, so we must
1547 # pass it either a file path or file like object. And we must use
1548 # _get_file_content to fetch file contents to ensure we are examining the
1549 # commit diff, rather than whatever's on disk.
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001550 # TODO(vapier): Once we migrate this to Python 3 only, cut it over.
Aviv Keshet5ac59522017-01-31 14:28:27 -08001551 contents = _get_file_content(f, commit)
Mike Frysinger13302d42019-09-13 17:21:24 -04001552 parser.readfp(io.StringIO(contents))
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001553 except configparser.Error as e:
Aviv Keshet5ac59522017-01-31 14:28:27 -08001554 msg = ('Unable to parse COMMIT-QUEUE.ini file at %s due to %s.' %
1555 (f, e))
1556 return HookFailure(msg)
1557
1558
Jon Salz3ee59de2012-08-18 13:54:22 +08001559def _run_project_hook_script(script, project, commit):
1560 """Runs a project hook script.
1561
1562 The script is run with the following environment variables set:
1563 PRESUBMIT_PROJECT: The affected project
1564 PRESUBMIT_COMMIT: The affected commit
1565 PRESUBMIT_FILES: A newline-separated list of affected files
1566
1567 The script is considered to fail if the exit code is non-zero. It should
1568 write an error message to stdout.
1569 """
1570 env = dict(os.environ)
Alex Deymo643ac4c2015-09-03 10:40:50 -07001571 env['PRESUBMIT_PROJECT'] = project.name
Jon Salz3ee59de2012-08-18 13:54:22 +08001572 env['PRESUBMIT_COMMIT'] = commit
1573
1574 # Put affected files in an environment variable
1575 files = _get_affected_files(commit)
1576 env['PRESUBMIT_FILES'] = '\n'.join(files)
1577
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001578 cmd_result = cros_build_lib.RunCommand(cmd=script,
1579 env=env,
1580 shell=True,
1581 print_cmd=False,
1582 input=os.devnull,
1583 stdout_to_pipe=True,
1584 combine_stdout_stderr=True,
1585 error_code_ok=True)
1586 if cmd_result.returncode:
1587 stdout = cmd_result.output
Jon Salz7b618af2012-08-31 06:03:16 +08001588 if stdout:
1589 stdout = re.sub('(?m)^', ' ', stdout)
1590 return HookFailure('Hook script "%s" failed with code %d%s' %
Rahul Chaudhry0e515342015-08-07 12:00:43 -07001591 (script, cmd_result.returncode,
Jon Salz3ee59de2012-08-18 13:54:22 +08001592 ':\n' + stdout if stdout else ''))
1593
1594
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001595def _check_project_prefix(_project, commit):
Mike Frysinger55f85b52014-12-18 14:45:21 -05001596 """Require the commit message have a project specific prefix as needed."""
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001597
Brian Norris77608e12018-04-06 10:38:43 -07001598 files = _get_affected_files(commit, include_deletes=True, relative=True)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001599 prefix = os.path.commonprefix(files)
1600 prefix = os.path.dirname(prefix)
1601
1602 # If there is no common prefix, the CL span multiple projects.
Daniel Erata350fd32014-09-29 14:02:34 -07001603 if not prefix:
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001604 return
1605
1606 project_name = prefix.split('/')[0]
Daniel Erata350fd32014-09-29 14:02:34 -07001607
1608 # The common files may all be within a subdirectory of the main project
1609 # directory, so walk up the tree until we find an alias file.
1610 # _get_affected_files() should return relative paths, but check against '/' to
1611 # ensure that this loop terminates even if it receives an absolute path.
1612 while prefix and prefix != '/':
1613 alias_file = os.path.join(prefix, '.project_alias')
1614
1615 # If an alias exists, use it.
1616 if os.path.isfile(alias_file):
1617 project_name = osutils.ReadFile(alias_file).strip()
1618
1619 prefix = os.path.dirname(prefix)
Bertrand SIMONNET0022fff2014-07-07 09:52:15 -07001620
1621 if not _get_commit_desc(commit).startswith(project_name + ': '):
1622 return HookFailure('The commit title for changes affecting only %s'
1623 ' should start with \"%s: \"'
1624 % (project_name, project_name))
1625
1626
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001627def _check_filepath_chartype(_project, commit):
1628 """Checks that FilePath::CharType stuff is not used."""
1629
1630 FILEPATH_REGEXP = re.compile('|'.join(
1631 [r'(?:base::)?FilePath::(?:Char|String|StringPiece)Type',
Satoru Takabayashi4ca37922018-08-08 10:16:38 +09001632 r'(?:base::)?FilePath::FromUTF8Unsafe',
1633 r'AsUTF8Unsafe',
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001634 r'FILE_PATH_LITERAL']))
1635 files = _filter_files(_get_affected_files(commit, relative=True),
1636 [r'.*\.(cc|h)$'])
1637
1638 errors = []
1639 for afile in files:
1640 for line_num, line in _get_file_diff(afile, commit):
1641 m = re.search(FILEPATH_REGEXP, line)
1642 if m:
1643 errors.append('%s, line %s has %s' % (afile, line_num, m.group(0)))
1644
1645 if errors:
1646 msg = 'Please assume FilePath::CharType is char (crbug.com/870621):'
1647 return HookFailure(msg, errors)
1648
1649
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001650def _check_exec_files(_project, commit):
1651 """Make +x bits on files."""
1652 # List of files that should never be +x.
1653 NO_EXEC = (
1654 'ChangeLog*',
1655 'COPYING',
1656 'make.conf',
1657 'make.defaults',
1658 'Manifest',
1659 'OWNERS',
1660 'package.use',
1661 'package.keywords',
1662 'package.mask',
1663 'parent',
1664 'README',
1665 'TODO',
1666 '.gitignore',
1667 '*.[achly]',
1668 '*.[ch]xx',
1669 '*.boto',
1670 '*.cc',
1671 '*.cfg',
1672 '*.conf',
1673 '*.config',
1674 '*.cpp',
1675 '*.css',
1676 '*.ebuild',
1677 '*.eclass',
Tatsuhisa Yamaguchi3b053632019-01-10 14:45:14 +09001678 '*.gn',
1679 '*.gni',
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001680 '*.gyp',
1681 '*.gypi',
1682 '*.htm',
1683 '*.html',
1684 '*.ini',
1685 '*.js',
1686 '*.json',
1687 '*.md',
1688 '*.mk',
1689 '*.patch',
1690 '*.policy',
1691 '*.proto',
1692 '*.raw',
1693 '*.rules',
1694 '*.service',
1695 '*.target',
1696 '*.txt',
1697 '*.xml',
1698 '*.yaml',
1699 )
1700
1701 def FinalName(obj):
1702 # If the file is being deleted, then the dst_file is not set.
1703 if obj.dst_file is None:
1704 return obj.src_file
1705 else:
1706 return obj.dst_file
1707
1708 bad_files = []
1709 files = _get_affected_files(commit, relative=True, full_details=True)
1710 for f in files:
1711 mode = int(f.dst_mode, 8)
1712 if not mode & 0o111:
1713 continue
1714 name = FinalName(f)
1715 for no_exec in NO_EXEC:
1716 if fnmatch.fnmatch(name, no_exec):
1717 bad_files.append(name)
1718 break
1719
1720 if bad_files:
1721 return HookFailure('These files should not be executable. '
1722 'Please `chmod -x` them.', bad_files)
1723
1724
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001725# Base
1726
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001727# A list of hooks which are not project specific and check patch description
1728# (as opposed to patch body).
1729_PATCH_DESCRIPTION_HOOKS = [
Ryan Cui9b651632011-05-11 11:38:58 -07001730 _check_change_has_bug_field,
David Jamesc3b68b32013-04-03 09:17:03 -07001731 _check_change_has_valid_cq_depend,
Ryan Cui9b651632011-05-11 11:38:58 -07001732 _check_change_has_test_field,
1733 _check_change_has_proper_changeid,
Mike Frysinger36b2ebc2014-10-31 14:02:03 -04001734 _check_commit_message_style,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001735 _check_change_is_contribution,
Jack Neus8edbf642019-07-10 16:08:31 -06001736 _check_change_no_include_oem,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001737]
1738
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001739# A list of hooks that are not project-specific
1740_COMMON_HOOKS = [
Aviv Keshet5ac59522017-01-31 14:28:27 -08001741 _check_cq_ini_well_formed,
1742 _check_cros_license,
Mike Frysingerbf8b91c2014-02-01 02:50:27 -05001743 _check_ebuild_eapi,
Mike Frysinger89bdb852014-02-01 05:26:26 -05001744 _check_ebuild_keywords,
Yu-Ju Hong5e0efa72013-11-19 16:28:10 -08001745 _check_ebuild_licenses,
Mike Frysingercd363c82014-02-01 05:20:18 -05001746 _check_ebuild_virtual_pv,
Mike Frysingerf9d41b32017-02-23 15:20:04 -05001747 _check_exec_files,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001748 _check_for_uprev,
Rahul Chaudhry09f61372015-07-31 17:14:26 -07001749 _check_gofmt,
Mike Frysinger998c2cc2014-08-27 05:20:23 -04001750 _check_layout_conf,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001751 _check_no_long_lines,
Keigo Oka4a09bd92019-05-07 14:01:00 +09001752 _check_no_new_gyp,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001753 _check_no_stray_whitespace,
Ryan Cui9b651632011-05-11 11:38:58 -07001754 _check_no_tabs,
Daniel Erat9d203ff2015-02-17 10:12:21 -07001755 _check_portage_make_use_var,
Fletcher Woodruffce1cb1b2019-08-16 15:59:32 -06001756 _check_rustfmt,
Aviv Keshet5ac59522017-01-31 14:28:27 -08001757 _check_tabbed_indents,
Ryan Cui9b651632011-05-11 11:38:58 -07001758]
Ryan Cuiec4d6332011-05-02 14:15:25 -07001759
Ryan Cui1562fb82011-05-09 11:01:31 -07001760
Ryan Cui9b651632011-05-11 11:38:58 -07001761# A dictionary of project-specific hooks(callbacks), indexed by project name.
1762# dict[project] = [callback1, callback2]
1763_PROJECT_SPECIFIC_HOOKS = {
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001764 'chromiumos/third_party/kernel': [_kernel_configcheck],
1765 'chromiumos/third_party/kernel-next': [_kernel_configcheck],
Ryan Cui9b651632011-05-11 11:38:58 -07001766}
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001767
Ryan Cui1562fb82011-05-09 11:01:31 -07001768
Ryan Cui9b651632011-05-11 11:38:58 -07001769# A dictionary of flags (keys) that can appear in the config file, and the hook
Mike Frysinger3554bc92015-03-11 04:59:21 -04001770# that the flag controls (value).
1771_HOOK_FLAGS = {
Luis Hector Chavezb50391d2017-09-26 15:48:15 -07001772 'clang_format_check': _check_clang_format,
Mike Frysingera7642f52015-03-25 18:31:42 -04001773 'checkpatch_check': _run_checkpatch,
Brian Norris23c62e92018-11-14 12:25:51 -08001774 'kerneldoc_check': _run_kerneldoc,
Ryan Cui9b651632011-05-11 11:38:58 -07001775 'stray_whitespace_check': _check_no_stray_whitespace,
Mike Frysingerff6c7d62015-03-24 13:49:46 -04001776 'json_check': _run_json_check,
Ryan Cui9b651632011-05-11 11:38:58 -07001777 'long_line_check': _check_no_long_lines,
Alex Deymof5792ce2015-08-24 22:50:08 -07001778 'cros_license_check': _check_cros_license,
1779 'aosp_license_check': _check_aosp_license,
Ryan Cui9b651632011-05-11 11:38:58 -07001780 'tab_check': _check_no_tabs,
Prathmesh Prabhuc5254652016-12-22 12:58:05 -08001781 'tabbed_indent_required_check': _check_tabbed_indents,
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001782 'branch_check': _check_change_has_branch_field,
Shawn Nematbakhsh51e16ac2014-01-28 15:31:07 -08001783 'signoff_check': _check_change_has_signoff_field,
Josh Triplett0e8fc7f2014-04-23 16:00:00 -07001784 'bug_field_check': _check_change_has_bug_field,
1785 'test_field_check': _check_change_has_test_field,
Steve Fung49ec7e92015-03-23 16:07:12 -07001786 'manifest_check': _check_manifests,
Bernie Thompsonf8fea992016-01-14 10:27:18 -08001787 'contribution_check': _check_change_is_contribution,
Mike Frysinger47bd1252018-06-11 12:12:20 -04001788 'project_prefix_check': _check_project_prefix,
Satoru Takabayashi15d17a52018-08-06 11:12:15 +09001789 'filepath_chartype_check': _check_filepath_chartype,
Ryan Cui9b651632011-05-11 11:38:58 -07001790}
1791
1792
Mike Frysinger3554bc92015-03-11 04:59:21 -04001793def _get_override_hooks(config):
1794 """Returns a set of hooks controlled by the current project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001795
1796 Expects to be called within the project root.
Jon Salz3ee59de2012-08-18 13:54:22 +08001797
1798 Args:
1799 config: A ConfigParser for the project's config file.
Ryan Cui9b651632011-05-11 11:38:58 -07001800 """
1801 SECTION = 'Hook Overrides'
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001802 SECTION_OPTIONS = 'Hook Overrides Options'
Jon Salz3ee59de2012-08-18 13:54:22 +08001803 if not config.has_section(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001804 return set(), set()
Ryan Cui9b651632011-05-11 11:38:58 -07001805
Mike Frysinger56e8de02019-07-31 14:40:14 -04001806 valid_keys = set(_HOOK_FLAGS.keys())
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001807 hooks = _HOOK_FLAGS.copy()
Mike Frysinger3554bc92015-03-11 04:59:21 -04001808
1809 enable_flags = []
Ryan Cui9b651632011-05-11 11:38:58 -07001810 disable_flags = []
Jon Salz3ee59de2012-08-18 13:54:22 +08001811 for flag in config.options(SECTION):
Mike Frysinger3554bc92015-03-11 04:59:21 -04001812 if flag not in valid_keys:
1813 raise ValueError('Error: unknown key "%s" in hook section of "%s"' %
1814 (flag, _CONFIG_FILE))
1815
Ryan Cui9b651632011-05-11 11:38:58 -07001816 try:
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001817 enabled = config.getboolean(SECTION, flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001818 except ValueError as e:
Mike Frysinger3554bc92015-03-11 04:59:21 -04001819 raise ValueError('Error: parsing flag "%s" in "%s" failed: %s' %
1820 (flag, _CONFIG_FILE, e))
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001821 if enabled:
1822 enable_flags.append(flag)
1823 else:
1824 disable_flags.append(flag)
Ryan Cui9b651632011-05-11 11:38:58 -07001825
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001826 # See if this hook has custom options.
1827 if enabled:
1828 try:
1829 options = config.get(SECTION_OPTIONS, flag)
1830 hooks[flag] = functools.partial(hooks[flag], options=options.split())
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001831 hooks[flag].__name__ = flag
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001832 except (configparser.NoOptionError, configparser.NoSectionError):
Mike Frysingerf8ce1712015-03-25 18:32:33 -04001833 pass
1834
1835 enabled_hooks = set(hooks[x] for x in enable_flags)
1836 disabled_hooks = set(hooks[x] for x in disable_flags)
Mike Frysinger3554bc92015-03-11 04:59:21 -04001837 return enabled_hooks, disabled_hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001838
1839
Jon Salz3ee59de2012-08-18 13:54:22 +08001840def _get_project_hook_scripts(config):
1841 """Returns a list of project-specific hook scripts.
1842
1843 Args:
1844 config: A ConfigParser for the project's config file.
1845 """
1846 SECTION = 'Hook Scripts'
1847 if not config.has_section(SECTION):
1848 return []
1849
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001850 return config.items(SECTION)
Jon Salz3ee59de2012-08-18 13:54:22 +08001851
1852
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001853def _get_project_hooks(project, presubmit):
Ryan Cui9b651632011-05-11 11:38:58 -07001854 """Returns a list of hooks that need to be run for a project.
1855
1856 Expects to be called from within the project root.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001857
1858 Args:
1859 project: A string, name of the project.
1860 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui9b651632011-05-11 11:38:58 -07001861 """
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001862 config = configparser.RawConfigParser()
Jon Salz3ee59de2012-08-18 13:54:22 +08001863 try:
1864 config.read(_CONFIG_FILE)
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001865 except configparser.Error:
Jon Salz3ee59de2012-08-18 13:54:22 +08001866 # Just use an empty config file
Mike Frysinger7bfc89f2019-09-13 15:45:51 -04001867 config = configparser.RawConfigParser()
Jon Salz3ee59de2012-08-18 13:54:22 +08001868
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001869 if presubmit:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001870 hooks = _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001871 else:
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001872 hooks = _PATCH_DESCRIPTION_HOOKS + _COMMON_HOOKS
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001873
Mike Frysinger3554bc92015-03-11 04:59:21 -04001874 enabled_hooks, disabled_hooks = _get_override_hooks(config)
Filipe Brandenburgerf70d32c2015-10-09 13:35:45 -07001875 hooks = [hook for hook in hooks if hook not in disabled_hooks]
1876
1877 # If a list is both in _COMMON_HOOKS and also enabled explicitly through an
1878 # override, keep the override only. Note that the override may end up being
1879 # a functools.partial, in which case we need to extract the .func to compare
1880 # it to the common hooks.
1881 unwrapped_hooks = [getattr(hook, 'func', hook) for hook in enabled_hooks]
1882 hooks = [hook for hook in hooks if hook not in unwrapped_hooks]
1883
1884 hooks = list(enabled_hooks) + hooks
Ryan Cui9b651632011-05-11 11:38:58 -07001885
1886 if project in _PROJECT_SPECIFIC_HOOKS:
Puneet Kumarc80e3f62012-08-13 19:01:18 -07001887 hooks.extend(hook for hook in _PROJECT_SPECIFIC_HOOKS[project]
1888 if hook not in disabled_hooks)
Ryan Cui9b651632011-05-11 11:38:58 -07001889
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001890 for name, script in _get_project_hook_scripts(config):
1891 func = functools.partial(_run_project_hook_script, script)
1892 func.__name__ = name
1893 hooks.append(func)
Jon Salz3ee59de2012-08-18 13:54:22 +08001894
Ryan Cui9b651632011-05-11 11:38:58 -07001895 return hooks
1896
1897
Alex Deymo643ac4c2015-09-03 10:40:50 -07001898def _run_project_hooks(project_name, proj_dir=None,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001899 commit_list=None, presubmit=False):
Ryan Cui1562fb82011-05-09 11:01:31 -07001900 """For each project run its project specific hook from the hooks dictionary.
1901
1902 Args:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001903 project_name: The name of project to run hooks for.
Doug Anderson44a644f2011-11-02 10:37:37 -07001904 proj_dir: If non-None, this is the directory the project is in. If None,
1905 we'll ask repo.
Doug Anderson14749562013-06-26 13:38:29 -07001906 commit_list: A list of commits to run hooks against. If None or empty list
1907 then we'll automatically get the list of commits that would be uploaded.
Vadim Bendebury2b62d742014-06-22 13:14:51 -07001908 presubmit: A Boolean, True if the check is run as a git pre-submit script.
Ryan Cui1562fb82011-05-09 11:01:31 -07001909
1910 Returns:
1911 Boolean value of whether any errors were ecountered while running the hooks.
1912 """
Doug Anderson44a644f2011-11-02 10:37:37 -07001913 if proj_dir is None:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001914 proj_dirs = _run_command(
1915 ['repo', 'forall', project_name, '-c', 'pwd']).split()
David James2edd9002013-10-11 14:09:19 -07001916 if len(proj_dirs) == 0:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001917 print('%s cannot be found.' % project_name, file=sys.stderr)
David James2edd9002013-10-11 14:09:19 -07001918 print('Please specify a valid project.', file=sys.stderr)
1919 return True
1920 if len(proj_dirs) > 1:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001921 print('%s is associated with multiple directories.' % project_name,
David James2edd9002013-10-11 14:09:19 -07001922 file=sys.stderr)
1923 print('Please specify a directory to help disambiguate.', file=sys.stderr)
1924 return True
1925 proj_dir = proj_dirs[0]
Doug Anderson44a644f2011-11-02 10:37:37 -07001926
Ryan Cuiec4d6332011-05-02 14:15:25 -07001927 pwd = os.getcwd()
1928 # hooks assume they are run from the root of the project
1929 os.chdir(proj_dir)
1930
Alex Deymo643ac4c2015-09-03 10:40:50 -07001931 remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
1932 '--symbolic-full-name', '@{u}']).strip()
1933 if not remote_branch:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04001934 print("Your project %s doesn't track any remote repo." % project_name,
Alex Deymo643ac4c2015-09-03 10:40:50 -07001935 file=sys.stderr)
1936 remote = None
1937 else:
Josh Pratt15d13ab2018-08-13 11:52:48 +10001938 branch_items = remote_branch.split('/', 1)
1939 if len(branch_items) != 2:
1940 PrintErrorForProject(
1941 project_name,
1942 HookFailure(
1943 'Cannot get remote and branch name (%s)' % remote_branch))
1944 os.chdir(pwd)
1945 return True
1946 remote, _branch = branch_items
Alex Deymo643ac4c2015-09-03 10:40:50 -07001947
1948 project = Project(name=project_name, dir=proj_dir, remote=remote)
1949
Doug Anderson14749562013-06-26 13:38:29 -07001950 if not commit_list:
1951 try:
1952 commit_list = _get_commits()
1953 except VerifyException as e:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001954 PrintErrorForProject(project.name, HookFailure(str(e)))
Doug Anderson14749562013-06-26 13:38:29 -07001955 os.chdir(pwd)
1956 return True
Ryan Cuifa55df52011-05-06 11:16:55 -07001957
Alex Deymo643ac4c2015-09-03 10:40:50 -07001958 hooks = _get_project_hooks(project.name, presubmit)
Ryan Cui1562fb82011-05-09 11:01:31 -07001959 error_found = False
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001960 commit_count = len(commit_list)
Mike Frysingerb99b3772019-08-17 14:19:44 -04001961 hook_count = len(hooks)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001962 for i, commit in enumerate(commit_list):
Mike Frysingerb2496652019-09-12 23:35:46 -04001963 CACHE.clear()
1964
Ryan Cui1562fb82011-05-09 11:01:31 -07001965 error_list = []
Mike Frysingerb99b3772019-08-17 14:19:44 -04001966 for h, hook in enumerate(hooks):
1967 output = ('PRESUBMIT.cfg: [%i/%i]: %s: Running [%i/%i] %s' %
Ben Chaneb806d82019-09-16 11:52:52 -07001968 (i + 1, commit_count, commit, h + 1, hook_count, hook.__name__))
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001969 print(output, end='\r')
1970 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001971 hook_error = hook(project, commit)
Mike Frysingerb7d552e2017-11-23 11:50:47 -05001972 print(' ' * len(output), end='\r')
1973 sys.stdout.flush()
Ryan Cui1562fb82011-05-09 11:01:31 -07001974 if hook_error:
Keigo Oka7e880ac2019-07-03 15:03:43 +09001975 if isinstance(hook_error, list):
1976 error_list.extend(hook_error)
1977 else:
1978 error_list.append(hook_error)
Ryan Cui1562fb82011-05-09 11:01:31 -07001979 error_found = True
1980 if error_list:
Alex Deymo643ac4c2015-09-03 10:40:50 -07001981 PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
Ryan Cui1562fb82011-05-09 11:01:31 -07001982 error_list)
Don Garrettdba548a2011-05-05 15:17:14 -07001983
Ryan Cuiec4d6332011-05-02 14:15:25 -07001984 os.chdir(pwd)
Ryan Cui1562fb82011-05-09 11:01:31 -07001985 return error_found
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001986
Mike Frysingerae409522014-02-01 03:16:11 -05001987
Mandeep Singh Baines116ad102011-04-27 15:16:37 -07001988# Main
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07001989
Ryan Cui1562fb82011-05-09 11:01:31 -07001990
Mike Frysingerae409522014-02-01 03:16:11 -05001991def main(project_list, worktree_list=None, **_kwargs):
Doug Anderson06456632012-01-05 11:02:14 -08001992 """Main function invoked directly by repo.
1993
1994 This function will exit directly upon error so that repo doesn't print some
1995 obscure error message.
1996
1997 Args:
1998 project_list: List of projects to run on.
David James2edd9002013-10-11 14:09:19 -07001999 worktree_list: A list of directories. It should be the same length as
2000 project_list, so that each entry in project_list matches with a directory
2001 in worktree_list. If None, we will attempt to calculate the directories
2002 automatically.
Doug Anderson06456632012-01-05 11:02:14 -08002003 kwargs: Leave this here for forward-compatibility.
2004 """
Ryan Cui1562fb82011-05-09 11:01:31 -07002005 found_error = False
David James2edd9002013-10-11 14:09:19 -07002006 if not worktree_list:
2007 worktree_list = [None] * len(project_list)
2008 for project, worktree in zip(project_list, worktree_list):
2009 if _run_project_hooks(project, proj_dir=worktree):
Ryan Cui1562fb82011-05-09 11:01:31 -07002010 found_error = True
2011
Mike Frysingerae409522014-02-01 03:16:11 -05002012 if found_error:
Ryan Cui1562fb82011-05-09 11:01:31 -07002013 msg = ('Preupload failed due to errors in project(s). HINTS:\n'
Ryan Cui9b651632011-05-11 11:38:58 -07002014 '- To disable some source style checks, and for other hints, see '
2015 '<checkout_dir>/src/repohooks/README\n'
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002016 "- To upload only current project, run 'repo upload .'")
Mike Frysinger09d6a3d2013-10-08 22:21:03 -04002017 print(msg, file=sys.stderr)
Don Garrettdba548a2011-05-05 15:17:14 -07002018 sys.exit(1)
Anush Elangovan63afad72011-03-23 00:41:27 -07002019
Ryan Cui1562fb82011-05-09 11:01:31 -07002020
Doug Anderson44a644f2011-11-02 10:37:37 -07002021def _identify_project(path):
2022 """Identify the repo project associated with the given path.
2023
2024 Returns:
2025 A string indicating what project is associated with the path passed in or
2026 a blank string upon failure.
2027 """
2028 return _run_command(['repo', 'forall', '.', '-c', 'echo ${REPO_PROJECT}'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002029 redirect_stderr=True, cwd=path).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002030
2031
Mike Frysinger55f85b52014-12-18 14:45:21 -05002032def direct_main(argv):
Doug Anderson44a644f2011-11-02 10:37:37 -07002033 """Run hooks directly (outside of the context of repo).
2034
Doug Anderson44a644f2011-11-02 10:37:37 -07002035 Args:
Mike Frysinger55f85b52014-12-18 14:45:21 -05002036 argv: The command line args to process
Doug Anderson44a644f2011-11-02 10:37:37 -07002037
2038 Returns:
2039 0 if no pre-upload failures, 1 if failures.
2040
2041 Raises:
2042 BadInvocation: On some types of invocation errors.
2043 """
Mike Frysinger66142932014-12-18 14:55:57 -05002044 parser = commandline.ArgumentParser(description=__doc__)
2045 parser.add_argument('--dir', default=None,
2046 help='The directory that the project lives in. If not '
2047 'specified, use the git project root based on the cwd.')
2048 parser.add_argument('--project', default=None,
2049 help='The project repo path; this can affect how the '
2050 'hooks get run, since some hooks are project-specific. '
2051 'For chromite this is chromiumos/chromite. If not '
2052 'specified, the repo tool will be used to figure this '
2053 'out based on the dir.')
2054 parser.add_argument('--rerun-since', default=None,
Vadim Bendebury75447b92018-01-10 12:06:01 -08002055 help='Rerun hooks on old commits since some point '
2056 'in the past. The argument could be a date (should '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002057 "match git log's concept of a date, e.g. 2012-06-20), "
Vadim Bendebury75447b92018-01-10 12:06:01 -08002058 'or a SHA1, or just a number of commits to check (from 1 '
2059 'to 99). This option is mutually exclusive with '
2060 '--pre-submit.')
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002061 parser.add_argument('--pre-submit', action='store_true',
Mike Frysinger66142932014-12-18 14:55:57 -05002062 help='Run the check against the pending commit. '
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002063 "This option should be used at the 'git commit' "
2064 "phase as opposed to 'repo upload'. This option "
Mike Frysinger66142932014-12-18 14:55:57 -05002065 'is mutually exclusive with --rerun-since.')
2066 parser.add_argument('commits', nargs='*',
2067 help='Check specific commits')
2068 opts = parser.parse_args(argv)
Doug Anderson44a644f2011-11-02 10:37:37 -07002069
Doug Anderson14749562013-06-26 13:38:29 -07002070 if opts.rerun_since:
Mike Frysinger66142932014-12-18 14:55:57 -05002071 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002072 raise BadInvocation("Can't pass commits and use rerun-since: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002073 ' '.join(opts.commits))
Doug Anderson14749562013-06-26 13:38:29 -07002074
Vadim Bendebury75447b92018-01-10 12:06:01 -08002075 if len(opts.rerun_since) < 3 and opts.rerun_since.isdigit():
2076 # This must be the number of commits to check. We don't expect the user
2077 # to want to check more than 99 commits.
2078 limit = '-n%s' % opts.rerun_since
2079 elif git.IsSHA1(opts.rerun_since, False):
2080 limit = '%s..' % opts.rerun_since
2081 else:
2082 # This better be a date.
2083 limit = '--since=%s' % opts.rerun_since
2084 cmd = ['git', 'log', limit, '--pretty=%H']
Doug Anderson14749562013-06-26 13:38:29 -07002085 all_commits = _run_command(cmd).splitlines()
2086 bot_commits = _run_command(cmd + ['--author=chrome-bot']).splitlines()
2087
2088 # Eliminate chrome-bot commits but keep ordering the same...
2089 bot_commits = set(bot_commits)
Mike Frysinger66142932014-12-18 14:55:57 -05002090 opts.commits = [c for c in all_commits if c not in bot_commits]
Doug Anderson14749562013-06-26 13:38:29 -07002091
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002092 if opts.pre_submit:
2093 raise BadInvocation('rerun-since and pre-submit can not be '
2094 'used together')
2095 if opts.pre_submit:
Mike Frysinger66142932014-12-18 14:55:57 -05002096 if opts.commits:
Mike Frysinger24dd3c52019-08-17 14:22:48 -04002097 raise BadInvocation("Can't pass commits and use pre-submit: %s" %
Mike Frysinger66142932014-12-18 14:55:57 -05002098 ' '.join(opts.commits))
2099 opts.commits = [PRE_SUBMIT,]
Doug Anderson44a644f2011-11-02 10:37:37 -07002100
2101 # Check/normlaize git dir; if unspecified, we'll use the root of the git
2102 # project from CWD
2103 if opts.dir is None:
2104 git_dir = _run_command(['git', 'rev-parse', '--git-dir'],
Rahul Chaudhry0e515342015-08-07 12:00:43 -07002105 redirect_stderr=True).strip()
Doug Anderson44a644f2011-11-02 10:37:37 -07002106 if not git_dir:
2107 raise BadInvocation('The current directory is not part of a git project.')
2108 opts.dir = os.path.dirname(os.path.abspath(git_dir))
2109 elif not os.path.isdir(opts.dir):
2110 raise BadInvocation('Invalid dir: %s' % opts.dir)
2111 elif not os.path.isdir(os.path.join(opts.dir, '.git')):
2112 raise BadInvocation('Not a git directory: %s' % opts.dir)
2113
2114 # Identify the project if it wasn't specified; this _requires_ the repo
2115 # tool to be installed and for the project to be part of a repo checkout.
2116 if not opts.project:
2117 opts.project = _identify_project(opts.dir)
2118 if not opts.project:
2119 raise BadInvocation("Repo couldn't identify the project of %s" % opts.dir)
2120
Doug Anderson14749562013-06-26 13:38:29 -07002121 found_error = _run_project_hooks(opts.project, proj_dir=opts.dir,
Mike Frysinger66142932014-12-18 14:55:57 -05002122 commit_list=opts.commits,
Vadim Bendebury2b62d742014-06-22 13:14:51 -07002123 presubmit=opts.pre_submit)
Doug Anderson44a644f2011-11-02 10:37:37 -07002124 if found_error:
2125 return 1
2126 return 0
2127
2128
Mandeep Singh Baines69e470e2011-04-06 10:34:52 -07002129if __name__ == '__main__':
Mike Frysinger55f85b52014-12-18 14:45:21 -05002130 sys.exit(direct_main(sys.argv[1:]))