Add repohook to check ebuild license/copyright

We will use the GPLv2 license and Google LLC copyright for new ebuilds
created by the COS team. This repohook enforces that new ebuild files in
cos/overlays/board-overlays contain GPLv2 license and Google LLC
copyright headers. Existing ebuilds with the ChromeOS license/copyright
are left alone.

BUG=b/149121748
TEST=./pre-upload_unittest.py and manual test in
cos/overlays/board-overlays

Change-Id: I76694ce86d825296c7e38ed4239368d3c5986490
diff --git a/pre-upload.py b/pre-upload.py
index b0c3f30..569df9d 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -1536,6 +1536,75 @@
   return errors
 
 
+def _check_cos_ebuild_license_header(_project, commit, options=()):
+  """Verifies the license/copyright header for COS ebuild sources.
+
+  New ebuild files originated from COS team should have the GPLv2 license
+  and "Google LLC" copyright header.
+  """
+  LICENSE_HEADER = (
+      r"""^#
+# Copyright (20[0-9]{2}) Google LLC
+#
+# This program is free software; you can redistribute it and\/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation\.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE\. See the
+# GNU General Public License for more details\.
+#$
+"""
+  )
+  license_re = re.compile(LICENSE_HEADER, re.MULTILINE)
+
+  included, excluded = _parse_common_inclusion_options(options)
+
+  bad_files = []
+  bad_year_files = []
+
+  files = _filter_files(
+      _get_affected_files(commit, relative=True),
+      included + [r'.*\.ebuild'],
+      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:
+    # We only want to check for new ebuild files
+    if f in existing_files:
+      continue
+
+    contents = _get_file_content(f, commit)
+    if not contents:
+      # Ignore empty files.
+      continue
+
+    m = license_re.search(contents)
+    if not m:
+      bad_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,
+        'Include Google copyright and GPLv2 license in new files:')
+    errors.append(HookFailure(msg, bad_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.
 
@@ -2072,6 +2141,7 @@
     'chromiumos/third_party/kernel-next': [_kernel_configcheck],
     'cos/manifest': [_check_cos_license],
     'cos/repohooks': [_check_cos_license],
+    'cos/overlays/board-overlays': [_check_cos_ebuild_license_header],
 }
 
 
diff --git a/pre-upload_unittest.py b/pre-upload_unittest.py
index 489f4ba..759f63c 100755
--- a/pre-upload_unittest.py
+++ b/pre-upload_unittest.py
@@ -1141,6 +1141,178 @@
     self.assertFalse(pre_upload._check_cos_license('proj', 'sha1'))
 
 
+class CheckCOSEbuildLicenseCopyrightHeader(PreUploadTestCase):
+  """Tests for _check_cos_ebuild_license_header."""
+
+  def setUp(self):
+    self.file_mock = self.PatchObject(pre_upload, '_get_affected_files')
+    self.content_mock = self.PatchObject(pre_upload, '_get_file_content')
+
+  def testHeaders(self):
+    """Accept old header styles."""
+    header = u"""#
+# Copyright 2020 Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+"""
+    def fake_get_affected_files(_, relative, include_adds=True):
+      _ = relative
+      if include_adds:
+        return ['test.ebuild']
+      else:
+        return []
+
+    self.file_mock.side_effect = fake_get_affected_files
+    self.content_mock.return_value = header
+    self.assertFalse(
+        pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+  def testRejectNoLinesAround(self):
+    """Reject headers missing the empty lines before/after the license."""
+    header = u"""# Copyright 2020 Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+"""
+    def fake_get_affected_files(_, relative, include_adds=True):
+      _ = relative
+      if include_adds:
+        return ['test.ebuild']
+      else:
+        return []
+
+    self.file_mock.side_effect = fake_get_affected_files
+    self.content_mock.return_value = header
+    self.assertTrue(
+        pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+  def testNewFileYear(self):
+    """Added files should have the current year in license header."""
+    year = datetime.datetime.now().year
+    HEADERS = (
+        u"""#
+# Copyright 2015 Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+# Copyright 2015 Google LLC
+#
+""",
+        u"""#
+# Copyright {} Google LLC
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# version 2 as published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+""".format(year),
+    )
+    want_error = (True, False)
+    def fake_get_affected_files(_, relative, include_adds=True):
+      _ = relative
+      if include_adds:
+        return ['test.ebuild']
+      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_cos_ebuild_license_header('proj', 'sha1'))
+      else:
+        self.assertFalse(
+            pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+  def testIgnoreExistingEbuilds(self):
+    """Existing ebuild files with ChromiumOS license/copyright are accepted."""
+    header = (
+        u'# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.\n'
+        u'# Use of this source code is governed by a BSD-style license that'
+        u' can be\n'
+        u'# found in the LICENSE file.\n'
+    )
+    self.file_mock.return_value = ['test.ebuild']
+    self.content_mock.return_value = header
+    self.assertFalse(
+        pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+  def testIgnoreNonEbuildFiles(self):
+    """Added non-ebuild files with ChromiumOS license/copyright are ignored."""
+    header = (
+        u'# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.\n'
+        u'# Use of this source code is governed by a BSD-style license that'
+        u' can be\n'
+        u'# found in the LICENSE file.\n'
+    )
+
+    def fake_get_affected_files(_, relative, include_adds=True):
+      _ = relative
+      if include_adds:
+        return ['file.py']
+      else:
+        return []
+
+    self.file_mock.side_effect = fake_get_affected_files
+    self.content_mock.return_value = header
+    self.assertFalse(
+        pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+  def testIgnoreExcludedPaths(self):
+    """Ignores excluded paths for license checks."""
+    def fake_get_affected_files(_, relative, include_adds=True):
+      _ = relative
+      if include_adds:
+        return ['foo/OWNERS']
+      else:
+        return []
+
+    self.file_mock.side_effect = fake_get_affected_files
+    self.content_mock.return_value = u'owner@chromium.org'
+    self.assertFalse(
+        pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+  def testIgnoreTopLevelExcludedPaths(self):
+    """Ignores excluded paths for license checks."""
+    def fake_get_affected_files(_, relative, include_adds=True):
+      _ = relative
+      if include_adds:
+        return ['OWNERS']
+      else:
+        return []
+
+    self.file_mock.side_effect = fake_get_affected_files
+    self.content_mock.return_value = u'owner@chromium.org'
+    self.assertFalse(
+        pre_upload._check_cos_ebuild_license_header('proj', 'sha1'))
+
+
 class CheckLayoutConfTestCase(PreUploadTestCase):
   """Tests for _check_layout_conf."""