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.