Check added files have current year in license header
Changed the hook to return a list of errors in the course of the change.
It will be helpful for users to find as many errors in a single run of
linter.
TEST=./pre-upload_unittest.py
TEST=tried to add new file with wrong year and checked linter complains.
BUG=chromium:980061
Change-Id: I2cd0ee95493f47114d62fcb81d72fccc627b8ca1
Reviewed-on: https://chromium-review.googlesource.com/1687034
Tested-by: Keigo Oka <oka@chromium.org>
Commit-Ready: Keigo Oka <oka@chromium.org>
Legacy-Commit-Queue: Commit Bot <commit-bot@chromium.org>
Reviewed-by: Mike Frysinger <vapier@chromium.org>
diff --git a/pre-upload.py b/pre-upload.py
index 7bcadb2..a96f52e 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -14,6 +14,7 @@
import argparse
import collections
import ConfigParser
+import datetime
import fnmatch
import functools
import json
@@ -1070,8 +1071,8 @@
"""
# For older years, be a bit more flexible as our policy says leave them be.
LICENSE_HEADER = (
- r'.*Copyright( \(c\))? 20[-0-9]{2,7} The Chromium OS Authors\. '
- r'All rights reserved\.' r'\n'
+ r'.*Copyright(?: \(c\))? (20[0-9]{2})(?:-20[0-9]{2})? The Chromium OS '
+ r'Authors\. All rights reserved\.\n'
r'.*Use of this source code is governed by a BSD-style license that can '
r'be\n'
r'.*found in the LICENSE file\.'
@@ -1080,42 +1081,58 @@
license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
# For newer years, be stricter.
- COPYRIGHT_LINE = (
+ BAD_COPYRIGHT_LINE = (
r'.*Copyright \(c\) 20(1[5-9]|[2-9][0-9]) The Chromium OS Authors\. '
r'All rights reserved\.' r'\n'
)
- copyright_re = re.compile(COPYRIGHT_LINE)
+ bad_copyright_re = re.compile(BAD_COPYRIGHT_LINE)
included, excluded = _parse_common_inclusion_options(options)
bad_files = []
bad_copyright_files = []
+ bad_year_files = []
+
files = _filter_files(
_get_affected_files(commit, relative=True),
included + COMMON_INCLUDED_PATHS,
excluded + COMMON_EXCLUDED_PATHS + LICENSE_EXCLUDED_PATHS)
+ existing_files = set(_get_affected_files(commit, relative=True,
+ include_adds=False))
+ current_year = str(datetime.datetime.now().year)
for f in files:
contents = _get_file_content(f, commit)
if not contents:
# Ignore empty files.
continue
- if not license_re.search(contents):
+ m = license_re.search(contents)
+ if not m:
bad_files.append(f)
- elif copyright_re.search(contents):
+ elif bad_copyright_re.search(contents):
bad_copyright_files.append(f)
+ if m and f not in existing_files:
+ year = m.group(1)
+ if year != current_year:
+ bad_year_files.append(f)
+
+ errors = []
if bad_files:
msg = '%s:\n%s\n%s' % (
'License must match', license_re.pattern,
'Found a bad header in these files:')
- return HookFailure(msg, bad_files)
-
+ errors.append(HookFailure(msg, bad_files))
if bad_copyright_files:
msg = 'Do not use (c) in copyright headers in new files:'
- return HookFailure(msg, bad_copyright_files)
+ errors.append(HookFailure(msg, bad_copyright_files))
+ if bad_year_files:
+ msg = 'Use current year (%s) in copyright headers in new files:' % (
+ current_year)
+ errors.append(HookFailure(msg, bad_year_files))
+ return errors
def _check_aosp_license(_project, commit, options=()):
"""Verifies the AOSP license/copyright header.
@@ -1846,7 +1863,10 @@
print(' ' * len(output), end='\r')
sys.stdout.flush()
if hook_error:
- error_list.append(hook_error)
+ if isinstance(hook_error, list):
+ error_list.extend(hook_error)
+ else:
+ error_list.append(hook_error)
error_found = True
if error_list:
PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index feb53c4..7d32d3b 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -8,6 +8,7 @@
from __future__ import print_function
+import datetime
import os
import sys
@@ -727,15 +728,44 @@
'# Use of this source code is governed by a BSD-style license that'
' can be\n'
'# found in the LICENSE file.\n'),
- ('// Copyright 2010-13 The Chromium OS Authors. All rights reserved.\n'
- '// Use of this source code is governed by a BSD-style license that'
+ ('// Copyright 2010-2013 The Chromium OS Authors. All rights reserved.'
+ '\n// Use of this source code is governed by a BSD-style license that'
' can be\n'
'// found in the LICENSE file.\n'),
)
self.file_mock.return_value = ['file']
for header in HEADERS:
self.content_mock.return_value = header
- self.assertEqual(None, pre_upload._check_cros_license('proj', 'sha1'))
+ self.assertFalse(pre_upload._check_cros_license('proj', 'sha1'))
+
+ def testNewFileYear(self):
+ """Added files should have the current year in license header."""
+ year = datetime.datetime.now().year
+ HEADERS = (
+ ('// Copyright 2016 The Chromium OS Authors. All rights reserved.\n'
+ '// Use of this source code is governed by a BSD-style license that'
+ ' can be\n'
+ '// found in the LICENSE file.\n'),
+ ('// Copyright %d The Chromium OS Authors. All rights reserved.\n'
+ '// Use of this source code is governed by a BSD-style license that'
+ ' can be\n'
+ '// found in the LICENSE file.\n') % year,
+ )
+ want_error = (True, False)
+ def fake_get_affected_files(_, relative, include_adds=True):
+ _ = relative
+ if include_adds:
+ return ['file']
+ else:
+ return []
+
+ self.file_mock.side_effect = fake_get_affected_files
+ for i, header in enumerate(HEADERS):
+ self.content_mock.return_value = header
+ if want_error[i]:
+ self.assertTrue(pre_upload._check_cros_license('proj', 'sha1'))
+ else:
+ self.assertFalse(pre_upload._check_cros_license('proj', 'sha1'))
def testRejectC(self):
"""Reject the (c) in newer headers."""
@@ -752,7 +782,7 @@
self.file_mock.return_value = ['file']
for header in HEADERS:
self.content_mock.return_value = header
- self.assertNotEqual(None, pre_upload._check_cros_license('proj', 'sha1'))
+ self.assertTrue(pre_upload._check_cros_license('proj', 'sha1'))
def testNoLeadingSpace(self):
"""Allow headers without leading space (e.g., not a source comment)"""
@@ -765,25 +795,25 @@
self.file_mock.return_value = ['file']
for header in HEADERS:
self.content_mock.return_value = header
- self.assertEqual(None, pre_upload._check_cros_license('proj', 'sha1'))
+ self.assertFalse(pre_upload._check_cros_license('proj', 'sha1'))
def testNoExcludedGolang(self):
"""Don't exclude .go files for license checks."""
self.file_mock.return_value = ['foo/main.go']
self.content_mock.return_value = ('package main\nfunc main() {}')
- self.assertNotEqual(None, pre_upload._check_cros_license('proj', 'sha1'))
+ self.assertTrue(pre_upload._check_cros_license('proj', 'sha1'))
def testIgnoreExcludedPaths(self):
"""Ignores excluded paths for license checks."""
self.file_mock.return_value = ['foo/OWNERS']
self.content_mock.return_value = ('owner@chromium.org')
- self.assertEqual(None, pre_upload._check_cros_license('proj', 'sha1'))
+ self.assertFalse(pre_upload._check_cros_license('proj', 'sha1'))
def testIgnoreTopLevelExcludedPaths(self):
"""Ignores excluded paths for license checks."""
self.file_mock.return_value = ['OWNERS']
self.content_mock.return_value = ('owner@chromium.org')
- self.assertEqual(None, pre_upload._check_cros_license('proj', 'sha1'))
+ self.assertFalse(pre_upload._check_cros_license('proj', 'sha1'))
class CheckAOSPLicenseCopyrightHeader(cros_test_lib.MockTestCase):
"""Tests for _check_aosp_license."""