Index: PRESUBMIT.py |
diff --git a/PRESUBMIT.py b/PRESUBMIT.py |
new file mode 100644 |
index 0000000000000000000000000000000000000000..4a3002afbd1df3e7d431b1f8ddc688d2f9e03d85 |
--- /dev/null |
+++ b/PRESUBMIT.py |
@@ -0,0 +1,270 @@ |
+#!python |
+# Copyright 2012 Google Inc. All Rights Reserved. |
+# |
+# Licensed under the Apache License, Version 2.0 (the "License"); |
+# you may not use this file except in compliance with the License. |
+# You may obtain a copy of the License at |
+# |
+# http://www.apache.org/licenses/LICENSE-2.0 |
+# |
+# Unless required by applicable law or agreed to in writing, software |
+# distributed under the License is distributed on an "AS IS" BASIS, |
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
+# See the License for the specific language governing permissions and |
+# limitations under the License. |
+# |
+# Presubmit script for Pipa, highly inspired by Syzygy's presubmit checks. |
+ |
+import itertools |
+import json |
+import os |
+import re |
+import subprocess |
+import sys |
+ |
+ |
+# Determine the root of the source tree. We use getcwd() instead of __file__ |
+# because gcl loads this script as text and runs it using eval(). In this |
+# context the variable __file__ is undefined. However, gcl assures us that |
+# the current working directory will be the directory containing this file. |
+PIPA_ROOT_DIR = os.path.join(os.path.abspath(os.getcwd()), 'pipa') |
+ |
+ |
+# Bring in some presubmit tools. |
+sys.path.insert(0, os.path.join(PIPA_ROOT_DIR, 'py')) |
+import deps_utils |
+from test_utils import presubmit |
+ |
+_UNITTEST_MESSAGE = """\ |
+Your %%s unittests must succeed before submitting! To clear this error, |
+ run: %s""" % os.path.join(PIPA_ROOT_DIR, 'run_all_tests.bat') |
+ |
+ |
+# License header and copyright line. |
+_LICENSE_HEADER = """\ |
+(#!python\n\ |
+)?.*? Copyright 20[0-9][0-9] (Google Inc|The Chromium Authors)\. \ |
+All Rights Reserved\.\n\ |
+.*?\n\ |
+""" |
+ |
+ |
+# Regular expressions to recognize source and header files. |
+# These are lifted from presubmit_support.py in depot_tools and are |
+# formulated as a list of regex strings so that they can be passed to |
+# input_api.FilterSourceFile() as the white_list parameter. |
+_CC_SOURCES = (r'.+\.c$', r'.+\.cc$', r'.+\.cpp$', r'.+\.rc$') |
+_CC_HEADERS = (r'.+\.h$', r'.+\.inl$', r'.+\.hxx$', r'.+\.hpp$') |
+_CC_FILES = _CC_SOURCES + _CC_HEADERS |
+_CC_SOURCES_RE = re.compile('|'.join('(?:%s)' % x for x in _CC_SOURCES)) |
+ |
+# When no files to blacklist. |
+_CC_FILES_BLACKLIST = [] |
+ |
+# Regular expressions used to extract headers and recognize empty lines. |
+_INCLUDE_RE = re.compile(r'^\s*#\s*include\s+(?P<header>[<"][^<"]+[>"])' |
+ r'\s*(?://.*(?P<nolint>NOLINT).*)?$') |
+_COMMENT_OR_BLANK_RE = re.compile(r'^\s*(?://.)?$') |
+ |
+ |
+def _IsSourceHeaderPair(source_path, header): |
+ # Returns true if source and header are a matched pair. |
+ # Source is the path on disk to the source file and header is the include |
+ # reference to the header (i.e., "blah/foo.h" or <blah/foo.h> including the |
+ # outer quotes or brackets. |
+ if not _CC_SOURCES_RE.match(source_path): |
+ return False |
+ |
+ source_root = os.path.splitext(source_path)[0] |
+ if source_root.endswith('_unittest'): |
+ source_root = source_root[0:-9] |
+ include = os.path.normpath(source_root + '.h') |
+ header = os.path.normpath(header[1:-1]) |
+ |
+ return include.endswith(header) |
+ |
+ |
+def _GetHeaderCompareKey(source_path, header): |
+ if _IsSourceHeaderPair(source_path, header): |
+ # We put the header that corresponds to this source file first. |
+ group = 0 |
+ elif header.startswith('<'): |
+ # C++ system headers should come after C system headers. |
+ group = 1 if header.endswith('.h>') else 2 |
+ else: |
+ group = 3 |
+ dirname, basename = os.path.split(header[1:-1]) |
+ return (group, dirname.lower(), basename.lower()) |
+ |
+ |
+def _GetHeaderCompareKeyFunc(source): |
+ return lambda header : _GetHeaderCompareKey(source, header) |
+ |
+ |
+def _HeaderGroups(source_lines): |
+ # Generates lists of headers in source, one per block of headers. |
+ # Each generated value is a tuple (line, headers) denoting on which |
+ # line of the source file an uninterrupted sequences of includes begins, |
+ # and the list of included headers (paths including the quotes or angle |
+ # brackets). |
+ start_line, headers = None, [] |
+ for line, num in itertools.izip(source_lines, itertools.count(1)): |
+ match = _INCLUDE_RE.match(line) |
+ if match: |
+ # The win32 api has all sorts of implicit include order dependencies. |
+ # Rather than encode exceptions for these, we require that they be |
+ # excluded from the ordering by a // NOLINT comment. |
+ if not match.group('nolint'): |
+ headers.append(match.group('header')) |
+ if start_line is None: |
+ # We just started a new run of headers. |
+ start_line = num |
+ elif headers and not _COMMENT_OR_BLANK_RE.match(line): |
+ # Any non-empty or non-comment line interrupts a sequence of includes. |
+ assert start_line is not None |
+ yield (start_line, headers) |
+ start_line = None |
+ headers = [] |
+ |
+ # Just in case we have some headers we haven't yielded yet, this is our |
+ # last chance to do so. |
+ if headers: |
+ assert start_line is not None |
+ yield (start_line, headers) |
+ |
+ |
+# Stolen from depot_tool's my_activity.py |
+def _DateTimeFromRietveld(date_string): |
+ try: |
+ return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S.%f') |
+ except ValueError: |
+ # Sometimes rietveld returns a value without the milliseconds part, so we |
+ # attempt to parse those cases as well. |
+ return datetime.strptime(date_string, '%Y-%m-%d %H:%M:%S') |
+ |
+ |
+def _ReadFile(input_api, path): |
+ file_path = input_api.change.RepositoryRoot() + '/' + path |
+ return input_api.ReadFile(file_path) |
+ |
+ |
+def CheckIncludeOrder(input_api, output_api): |
+ """Checks that the C/C++ include order is correct.""" |
+ errors = [] |
+ is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES) |
+ for f in input_api.AffectedFiles(include_deletes=False, |
+ file_filter=is_cc_file): |
+ for line_num, group in _HeaderGroups(f.NewContents()): |
+ sorted_group = sorted(group, key=_GetHeaderCompareKeyFunc(f.LocalPath())) |
+ if group != sorted_group: |
+ message = '%s, line %s: Out of order includes. ' \ |
+ 'Expected:\n\t#include %s' % ( |
+ f.LocalPath(), |
+ line_num, |
+ '\n\t#include '.join(sorted_group)) |
+ errors.append(output_api.PresubmitPromptWarning(message)) |
+ return errors |
+ |
+ |
+def CheckUnittestsRan(input_api, output_api, committing, configuration): |
+ """Checks that the unittests success file is newer than any modified file""" |
+ return presubmit.CheckTestSuccess(input_api, output_api, committing, |
+ configuration, 'ALL', |
+ message=_UNITTEST_MESSAGE % configuration) |
+ |
+ |
+def CheckAllDepsInGitignore(input_api, output_api): |
+ gitignore = str(_ReadFile(input_api, '.gitignore')) |
+ ignored_folders = filter(lambda line: line.startswith('/'), |
+ gitignore.split('\n')) |
+ # Remove trailing whitespaces and forward slashes and the beginning and at |
+ # the end of .gitignore lines. |
+ ignored = set([folder.strip().strip('/') for folder in ignored_folders]) |
+ |
+ messages = [] |
+ def AddErrorIfNotIgnored(path): |
+ # Check if |path| or its parent directories are ignored. |
+ path_segments = path.strip('/').split('/') |
+ for length in range(1, len(path_segments) + 1): |
+ if '/'.join(path_segments[:length]) in ignored: |
+ return |
+ messages.append(output_api.PresubmitError( |
+ 'External dependency path %s is not in .gitignore file' % path)) |
+ |
+ if 'DEPS' in input_api.LocalPaths(): |
+ deps_content = _ReadFile(input_api, 'DEPS') |
+ deps = deps_utils.ParseDeps(deps_content, 'DEPS') |
+ for root_dir in deps: |
+ if root_dir.startswith('src/'): |
+ root_dir = root_dir[3:] |
+ AddErrorIfNotIgnored(root_dir) |
+ |
+ if 'GITDEPS' in input_api.LocalPaths(): |
+ deps_content = _ReadFile(input_api, 'GITDEPS') |
+ deps = deps_utils.ParseDeps(deps_content, 'GITDEPS') |
+ for root_dir, (_, subdirs, _) in deps.iteritems(): |
+ if subdirs: |
+ for subdir in subdirs: |
+ AddErrorIfNotIgnored(root_dir + '/' + subdir) |
+ else: |
+ AddErrorIfNotIgnored(root_dir) |
+ |
+ return messages |
+ |
+ |
+def CheckChange(input_api, output_api, committing): |
+ if 'dcommit' in sys.argv: |
+ return [output_api.PresubmitPromptWarning( |
+ 'You must use "git cl land" and not "git cl dcommit".')] |
+ |
+ # The list of (canned) checks we perform on all files in all changes. |
+ checks = [ |
+ CheckAllDepsInGitignore, |
+ CheckIncludeOrder, |
+ input_api.canned_checks.CheckChangeHasDescription, |
+ input_api.canned_checks.CheckChangeHasNoCrAndHasOnlyOneEol, |
+ input_api.canned_checks.CheckChangeHasNoTabs, |
+ input_api.canned_checks.CheckChangeHasNoStrayWhitespace, |
+ input_api.canned_checks.CheckDoNotSubmit, |
+ input_api.canned_checks.CheckGenderNeutral, |
+ input_api.canned_checks.CheckPatchFormatted, |
+ ] |
+ if committing: |
+ checks.append(input_api.canned_checks.CheckDoNotSubmit) |
+ |
+ results = [] |
+ for check in checks: |
+ results += check(input_api, output_api) |
+ |
+ results += input_api.canned_checks.CheckLongLines(input_api, output_api, 80) |
+ |
+ # We run lint only on C/C++ files so that we avoid getting notices about |
+ # files being ignored. |
+ is_cc_file = lambda x: input_api.FilterSourceFile(x, white_list=_CC_FILES, |
+ black_list=_CC_FILES_BLACKLIST) |
+ results += input_api.canned_checks.CheckChangeLintsClean( |
+ input_api, output_api, source_file_filter=is_cc_file) |
+ |
+ # We check the license on the default recognized source file types, as well |
+ # as GN files. |
+ gn_file_re = r'.+\.gn?$' |
+ gni_file_re = r'.+\.gni?$' |
+ white_list = input_api.DEFAULT_WHITE_LIST + (gn_file_re, gni_file_re) |
+ sources = lambda x: input_api.FilterSourceFile(x, white_list=white_list) |
+ results += input_api.canned_checks.CheckLicense(input_api, |
+ output_api, |
+ _LICENSE_HEADER, |
+ source_file_filter=sources) |
+ |
+ # results += CheckUnittestsRan(input_api, output_api, committing, "Debug") |
+ # results += CheckUnittestsRan(input_api, output_api, committing, "Release") |
+ |
+ return results |
+ |
+ |
+def CheckChangeOnUpload(input_api, output_api): |
+ return CheckChange(input_api, output_api, False) |
+ |
+ |
+def CheckChangeOnCommit(input_api, output_api): |
+ return CheckChange(input_api, output_api, True) |