Snap for 6533464 from bf70aed7478925fb510a3622e16195d699223e4c to sdk-release
Change-Id: I6b9451119398134353ea3af86c9a931d618493a8
diff --git a/README.md b/README.md
index 83572ac..d35a4c4 100644
--- a/README.md
+++ b/README.md
@@ -181,6 +181,9 @@
* `commit_msg_relnote_field_format`: Check for possible misspellings of the
`Relnote:` field and that multiline release notes are properly formatted with
quotes.
+* `commit_msg_relnote_for_current_txt`: Check that CLs with changes to
+ current.txt or public_plus_experimental_current.txt also contain a
+ `Relnote:` field in the commit message.
* `commit_msg_test_field`: Require a `Test:` line.
* `cpplint`: Run through the cpplint tool (for C++ code).
* `gofmt`: Run Go code through `gofmt`.
diff --git a/rh/hooks.py b/rh/hooks.py
index 21be3cc..223323a 100644
--- a/rh/hooks.py
+++ b/rh/hooks.py
@@ -648,6 +648,61 @@
return ret
+RELNOTE_REQUIRED_CURRENT_TXT_MSG = """\
+Commit contains a change to current.txt or public_plus_experimental_current.txt,
+but the commit message does not contain the required `Relnote:` tag. It must
+match the regex:
+
+ %s
+
+The Relnote: stanza is free-form and should describe what developers need to
+know about your change. If you are making infrastructure changes, you
+can set the Relnote: stanza to be "N/A" for the commit to not be included
+in release notes.
+
+Some examples:
+
+Relnote: "Added a new API `Class#isBetter` to determine whether or not the
+class is better"
+Relnote: Fixed an issue where the UI would hang on a double tap.
+Relnote: N/A
+
+Check the git history for more examples.
+"""
+
+def check_commit_msg_relnote_for_current_txt(project, commit, desc, diff,
+ options=None):
+ """Check changes to current.txt contain the 'Relnote:' stanza."""
+ field = 'Relnote'
+ regex = r'^%s: .+$' % (field,)
+ check_re = re.compile(regex, re.IGNORECASE)
+
+ if options.args():
+ raise ValueError('commit msg %s check takes no options' % (field,))
+
+ filtered = _filter_diff(
+ diff,
+ [r'^(public_plus_experimental_current|current)\.txt$']
+ )
+ # If the commit does not contain a change to *current.txt, then this repo
+ # hook check no longer applies.
+ if not filtered:
+ return None
+
+ found = []
+ for line in desc.splitlines():
+ if check_re.match(line):
+ found.append(line)
+
+ if not found:
+ error = RELNOTE_REQUIRED_CURRENT_TXT_MSG % (regex)
+ else:
+ return None
+
+ return [rh.results.HookResult('commit msg: "%s:" check' % (field,),
+ project, commit, error=error)]
+
+
def check_cpplint(project, commit, _desc, diff, options=None):
"""Run cpplint."""
# This list matches what cpplint expects. We could run on more (like .cxx),
@@ -726,11 +781,11 @@
def check_pylint3(project, commit, desc, diff, options=None):
"""Run pylint through Python 3."""
return _check_pylint(project, commit, desc, diff,
- extra_args=['--executable-path=pylint3'],
+ extra_args=['--py3'],
options=options)
-def check_rustfmt(project, commit, desc, diff, options=None):
+def check_rustfmt(project, commit, _desc, diff, options=None):
"""Run "rustfmt --check" on diffed rust files"""
filtered = _filter_diff(diff, [r'\.rs$'])
if not filtered:
@@ -812,6 +867,8 @@
'commit_msg_prebuilt_apk_fields': check_commit_msg_prebuilt_apk_fields,
'commit_msg_test_field': check_commit_msg_test_field,
'commit_msg_relnote_field_format': check_commit_msg_relnote_field_format,
+ 'commit_msg_relnote_for_current_txt':
+ check_commit_msg_relnote_for_current_txt,
'cpplint': check_cpplint,
'gofmt': check_gofmt,
'google_java_format': check_google_java_format,
diff --git a/rh/hooks_unittest.py b/rh/hooks_unittest.py
index 0442f4e..4c44b21 100755
--- a/rh/hooks_unittest.py
+++ b/rh/hooks_unittest.py
@@ -573,6 +573,83 @@
'Bug: 1234'),
))
+ def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run):
+ """Verify the commit_msg_relnote_for_current_txt builtin hook."""
+ diff_without_current_txt = ['foo.txt',
+ 'foo.cpp',
+ 'foo.java',
+ 'current.java']
+ diff_with_current_txt = diff_without_current_txt + ['current.txt']
+ diff_with_experimental_current_txt = \
+ diff_without_current_txt + ['public_plus_experimental_current.txt']
+ # Check some good messages.
+ self._test_commit_messages(
+ rh.hooks.check_commit_msg_relnote_for_current_txt,
+ True,
+ (
+ 'subj\n\nRelnote: This is a release note\n',
+ 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
+ ('subj\n\nRelnote: This is release note 1 with\n'
+ 'an incorrectly formatted second line.\n\n'
+ 'Relnote: "This is release note 2, and it\n'
+ 'contains a correctly formatted second line."\n'
+ 'Bug: 1234'),
+ ),
+ files=diff_with_current_txt,
+ )
+ # Check some good messages.
+ self._test_commit_messages(
+ rh.hooks.check_commit_msg_relnote_for_current_txt,
+ True,
+ (
+ 'subj\n\nRelnote: This is a release note\n',
+ 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
+ ('subj\n\nRelnote: This is release note 1 with\n'
+ 'an incorrectly formatted second line.\n\n'
+ 'Relnote: "This is release note 2, and it\n'
+ 'contains a correctly formatted second line."\n'
+ 'Bug: 1234'),
+ ),
+ files=diff_with_experimental_current_txt,
+ )
+ # Check some good messages.
+ self._test_commit_messages(
+ rh.hooks.check_commit_msg_relnote_for_current_txt,
+ True,
+ (
+ 'subj',
+ 'subj\nBug: 12345\nChange-Id: 1234',
+ 'subj\n\nRelnote: This is a release note\n',
+ 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
+ ('subj\n\nRelnote: This is release note 1 with\n'
+ 'an incorrectly formatted second line.\n\n'
+ 'Relnote: "This is release note 2, and it\n'
+ 'contains a correctly formatted second line."\n'
+ 'Bug: 1234'),
+ ),
+ files=diff_without_current_txt,
+ )
+ # Check some bad messages.
+ self._test_commit_messages(
+ rh.hooks.check_commit_msg_relnote_for_current_txt,
+ False,
+ (
+ 'subj'
+ 'subj\nBug: 12345\nChange-Id: 1234',
+ ),
+ files=diff_with_current_txt,
+ )
+ # Check some bad messages.
+ self._test_commit_messages(
+ rh.hooks.check_commit_msg_relnote_for_current_txt,
+ False,
+ (
+ 'subj'
+ 'subj\nBug: 12345\nChange-Id: 1234',
+ ),
+ files=diff_with_experimental_current_txt,
+ )
+
def test_cpplint(self, mock_check, _mock_run):
"""Verify the cpplint builtin hook."""
self._test_file_filter(mock_check, rh.hooks.check_cpplint,
diff --git a/rh/utils.py b/rh/utils.py
index 75df2e1..6486e1b 100644
--- a/rh/utils.py
+++ b/rh/utils.py
@@ -426,7 +426,8 @@
if e.errno == errno.EACCES:
estr += '; does the program need `chmod a+x`?'
if not check:
- result = CompletedProcess(args=cmd, stderr=estr, returncode=255)
+ result = CompletedProcess(
+ args=cmd, stderr=estr.encode('utf-8'), returncode=255)
else:
raise CalledProcessError(
result.returncode, result.cmd, stdout=result.stdout,
diff --git a/tools/pylint.py b/tools/pylint.py
index 7885dcf..b805df6 100755
--- a/tools/pylint.py
+++ b/tools/pylint.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/env python3
# -*- coding:utf-8 -*-
# Copyright 2016 The Android Open Source Project
#
@@ -21,20 +21,52 @@
import argparse
import errno
import os
+import shutil
import sys
+import subprocess
+
+
+assert (sys.version_info.major, sys.version_info.minor) >= (3, 5), (
+ 'Python 3.5 or newer is required; found %s' % (sys.version,))
DEFAULT_PYLINTRC_PATH = os.path.join(
os.path.dirname(os.path.realpath(__file__)), 'pylintrc')
+def find_pylint3():
+ """Figure out the name of the pylint tool for Python 3.
+
+ It keeps changing with Python 2->3 migrations. Fun.
+ """
+ # Prefer pylint3 as that's what we want.
+ if shutil.which('pylint3'):
+ return 'pylint3'
+
+ # If there's no pylint, give up.
+ if not shutil.which('pylint'):
+ print('%s: unable to locate pylint; please install:\n'
+ 'sudo apt-get install pylint' % (__file__,), file=sys.stderr)
+ sys.exit(1)
+
+ # Make sure pylint is using Python 3.
+ result = subprocess.run(['pylint', '--version'], stdout=subprocess.PIPE,
+ check=True)
+ if b'Python 3' not in result.stdout:
+ print('%s: unable to locate a Python 3 version of pylint; Python 3 '
+ 'support cannot be guaranteed' % (__file__,), file=sys.stderr)
+
+ return 'pylint'
+
+
def get_parser():
"""Return a command line parser."""
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--init-hook', help='Init hook commands to run.')
+ parser.add_argument('--py3', action='store_true',
+ help='Force Python 3 mode')
parser.add_argument('--executable-path',
- help='The path of the pylint executable.',
- default='pylint')
+ help='The path of the pylint executable.')
parser.add_argument('--no-rcfile',
help='Specify to use the executable\'s default '
'configuration.',
@@ -48,7 +80,14 @@
parser = get_parser()
opts, unknown = parser.parse_known_args(argv)
- cmd = [opts.executable_path]
+ pylint = opts.executable_path
+ if pylint is None:
+ if opts.py3:
+ pylint = find_pylint3()
+ else:
+ pylint = 'pylint'
+
+ cmd = [pylint]
if not opts.no_rcfile:
# We assume pylint is running in the top directory of the project,
# so load the pylintrc file from there if it's available.