[wpt-importer] Add GitHub diff link to CL description

Also, wrap the commit message and CL description as close to 72
characters as possible.

Bug: 40631540
Change-Id: I495aec7a281b58f8715902019144842a53d50d24
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5406820
Reviewed-by: Weizhong Xia <weizhong@google.com>
Commit-Queue: Jonathan Lee <jonathanjlee@google.com>
Cr-Commit-Position: refs/heads/main@{#1280767}
diff --git a/third_party/blink/tools/blinkpy/w3c/chromium_commit_mock.py b/third_party/blink/tools/blinkpy/w3c/chromium_commit_mock.py
index 3e5fa2d..a093003 100644
--- a/third_party/blink/tools/blinkpy/w3c/chromium_commit_mock.py
+++ b/third_party/blink/tools/blinkpy/w3c/chromium_commit_mock.py
@@ -5,7 +5,8 @@
 import hashlib
 
 
-class MockChromiumCommit(object):
+class MockChromiumCommit:
+
     def __init__(self,
                  host,
                  position='refs/heads/master@{#123}',
@@ -23,6 +24,9 @@
         self._body = body
         self._patch = patch
 
+    def __str__(self):
+        return f'{self.short_sha} "{self.subject()}"'
+
     @property
     def short_sha(self):
         return self.sha[0:10]
diff --git a/third_party/blink/tools/blinkpy/w3c/import_notifier.py b/third_party/blink/tools/blinkpy/w3c/import_notifier.py
index 78b9887..2bffe5a 100644
--- a/third_party/blink/tools/blinkpy/w3c/import_notifier.py
+++ b/third_party/blink/tools/blinkpy/w3c/import_notifier.py
@@ -26,6 +26,7 @@
 
 from blinkpy.common import path_finder
 from blinkpy.common.checkout.git import CommitRange
+from blinkpy.common.memoized import memoized
 from blinkpy.common.net.git_cl import CLRevisionID
 from blinkpy.common.system.executive import ScriptError
 from blinkpy.web_tests.models import typ_types
@@ -105,19 +106,14 @@
 
         Note: "test names" are paths of the tests relative to web_tests.
         """
-        # TODO(crbug.com/40631540): Add the GitHub comparison link to each
-        # import commit message. Then, the notifier can parse out the WPT
-        # revision range here without needing to look at the second most recent
-        # commit.
-        wpt_end_rev, import_rev = self._latest_wpt_revision_in_range()
+        wpt_end_rev, import_rev = self.latest_wpt_import()
         cl = self._cl_for_wpt_revision(wpt_end_rev)
         repo = self.host.project_config.gerrit_project
         _log.info(f'Identifying failures for {repo}@{import_rev} ({cl.url})')
         if self._bugs_already_filed(cl):
             _log.info(f'Bugs have already been filed.')
             return {}
-        wpt_start_rev, _ = self._latest_wpt_revision_in_range(
-            f'{import_rev}~1')
+        wpt_start_rev, _ = self.latest_wpt_import(f'{import_rev}~1')
 
         self.examine_baseline_changes(import_rev, cl.current_revision_id)
         self.examine_new_test_expectations(import_rev)
@@ -131,11 +127,16 @@
                 ', '.join(sorted(bug.link for bug in filed_bugs.values())))
         return filed_bugs
 
-    def _latest_wpt_revision_in_range(
+    @memoized
+    def latest_wpt_import(
             self,
             commits: Union[None, str, CommitRange] = None) -> Tuple[str, str]:
         """Get commit hashes for the last WPT import.
 
+        Arguments:
+            commits: The range to search. See `Git.most_recent_log_matching()`
+                docstring for usage.
+
         Returns:
             A pair of SHA-1 hex digests (40 hex digits each):
               * A valid commit in the WPT repo denoting how far WPT was rolled
diff --git a/third_party/blink/tools/blinkpy/w3c/test_importer.py b/third_party/blink/tools/blinkpy/w3c/test_importer.py
index 5fb394a..123883c 100644
--- a/third_party/blink/tools/blinkpy/w3c/test_importer.py
+++ b/third_party/blink/tools/blinkpy/w3c/test_importer.py
@@ -22,11 +22,19 @@
 from blinkpy.common.net.network_transaction import NetworkTimeout
 from blinkpy.common.path_finder import PathFinder
 from blinkpy.common.system.log_utils import configure_logging
-from blinkpy.w3c.buganizer import BuganizerClient, BuganizerIssue
+from blinkpy.w3c.buganizer import BuganizerIssue
+from blinkpy.w3c.chromium_commit import ChromiumCommit
 from blinkpy.w3c.chromium_exportable_commits import exportable_commits_over_last_n_commits
-from blinkpy.w3c.common import read_credentials, is_testharness_baseline, is_file_exportable, WPT_GH_URL
+from blinkpy.w3c.common import (
+    read_credentials,
+    is_testharness_baseline,
+    is_file_exportable,
+    WPT_GH_URL,
+    WPT_GH_RANGE_URL_TEMPLATE,
+)
 from blinkpy.w3c.directory_owners_extractor import DirectoryOwnersExtractor
 from blinkpy.w3c.gerrit import GerritAPI
+from blinkpy.w3c.import_notifier import ImportNotifier
 from blinkpy.w3c.local_wpt import LocalWPT
 from blinkpy.w3c.test_copier import TestCopier
 from blinkpy.w3c.wpt_expectations_updater import WPTExpectationsUpdater
@@ -122,33 +130,32 @@
         # File bugs for the previous imported CL. This is done at the start so
         # that manually revived CLs still receive bugs.
         gerrit_api = GerritAPI.from_credentials(self.host, credentials)
-        self.file_and_record_bugs(local_wpt,
-                                  gerrit_api,
+        notifier = ImportNotifier(self.host, self.project_git, local_wpt,
+                                  gerrit_api)
+        self.file_and_record_bugs(notifier,
                                   auto_file_bugs=options.auto_file_bugs)
 
         if options.revision is not None:
             _log.info('Checking out %s', options.revision)
             self.wpt_git.run(['checkout', options.revision])
 
-        _log.debug('Noting the revision we are importing.')
-        import_commit = f'wpt@{self.wpt_git.latest_git_commit()}'
-
-        _log.info('Importing %s to Chromium %s', import_commit,
+        new_wpt_revision = self.wpt_git.latest_git_commit()
+        _log.info('Importing wpt@%s to Chromium %s', new_wpt_revision,
                   chromium_revision)
 
         if options.ignore_exportable_commits:
-            commit_message = self._commit_message(chromium_revision,
-                                                  import_commit)
+            commits = []
         else:
             commits = self.apply_exportable_commits_locally(local_wpt)
             if commits is None:
                 _log.error('Could not apply some exportable commits cleanly.')
                 _log.error('Aborting import to prevent clobbering commits.')
                 return 1
-            commit_message = self._commit_message(
-                chromium_revision,
-                import_commit,
-                locally_applied_commits=commits)
+        last_wpt_revision, _ = notifier.latest_wpt_import()
+        wpt_range = CommitRange(last_wpt_revision, new_wpt_revision)
+        commit_message = self.commit_message(chromium_revision,
+                                             wpt_range,
+                                             locally_applied_commits=commits)
 
         self._clear_out_dest_path()
 
@@ -188,7 +195,7 @@
             return 0
 
         directory_owners = self.get_directory_owners()
-        description = self._cl_description(directory_owners)
+        description = self.cl_description(directory_owners)
         self._upload_cl(description)
         _log.info('Issue: %s', self.git_cl.run(['issue']).strip())
 
@@ -490,18 +497,24 @@
                 return True
         return False
 
-    def _commit_message(self,
-                        chromium_commit_sha,
-                        import_commit_sha,
-                        locally_applied_commits=None):
-        message = 'Import {}\n\nUsing wpt-import in Chromium {}.\n'.format(
-            import_commit_sha, chromium_commit_sha)
+    def commit_message(
+        self,
+        chromium_commit_sha: str,
+        wpt_range: CommitRange,
+        locally_applied_commits: Optional[List[ChromiumCommit]] = None,
+    ) -> str:
+        wpt_short_range = CommitRange(wpt_range.start[:10], wpt_range.end[:10])
+        message = textwrap.dedent(f"""\
+            {ImportNotifier.IMPORT_SUBJECT_PREFIX}{wpt_range.end}
+
+            {WPT_GH_RANGE_URL_TEMPLATE.format(*wpt_short_range)}
+
+            Using wpt-import in Chromium {chromium_commit_sha}.
+            """)
         if locally_applied_commits:
             message += 'With Chromium commits locally applied on WPT:\n'
-            message += '\n'.join(
-                str(commit) for commit in locally_applied_commits)
-        message += '\nNo-Export: true'
-        message += '\nValidate-Test-Flakiness: skip'
+            message += '\n'.join(f'  {textwrap.shorten(str(commit), 70)}'
+                                 for commit in locally_applied_commits) + '\n'
         return message
 
     def _delete_orphaned_baselines(self):
@@ -571,7 +584,7 @@
         extractor = DirectoryOwnersExtractor(self.host)
         return extractor.list_owners(changed_files)
 
-    def _cl_description(self, directory_owners):
+    def cl_description(self, directory_owners):
         """Returns a CL description string.
 
         Args:
@@ -579,13 +592,18 @@
         """
         # TODO(robertma): Add a method in Git for getting the commit body.
         description = self.project_git.run(['log', '-1', '--format=%B'])
-        description += (
-            'Note to sheriffs: This CL imports external tests and adds\n'
-            'expectations for those tests; if this CL is large and causes\n'
-            'a few new failures, please fix the failures by adding new\n'
-            'lines to TestExpectations rather than reverting. See:\n'
-            'https://chromium.googlesource.com'
-            '/chromium/src/+/main/docs/testing/web_platform_tests.md\n\n')
+        gardener_instructions = textwrap.dedent("""\
+            Note to gardeners: This CL imports external tests and adds
+            expectations for those tests; if this CL is large and causes a few
+            new failures, please fix the failures by adding new lines to
+            TestExpectations rather than reverting. See:
+            """)
+        description += '\n'.join([
+            '',
+            *textwrap.wrap(gardener_instructions, width=72),
+            'https://chromium.googlesource.com/chromium/src/+/'
+            'main/docs/testing/web_platform_tests.md',
+        ]) + '\n\n'
 
         if directory_owners:
             description += self._format_directory_owners(
@@ -593,11 +611,6 @@
 
         # Prevent FindIt from auto-reverting import CLs.
         description += 'NOAUTOREVERT=true\n'
-
-        # Move any No-Export tag and flakiness footers to the end of the description.
-        description = description.replace('No-Export: true', '')
-        description = description.replace('Validate-Test-Flakiness: skip', '')
-        description = description.replace('\n\n\n\n\n', '\n\n')
         description += 'No-Export: true\n'
         description += 'Validate-Test-Flakiness: skip\n'
 
@@ -662,9 +675,7 @@
 
     def file_and_record_bugs(
         self,
-        local_wpt: LocalWPT,
-        gerrit_api: GerritAPI,
-        buganizer_client: Optional[BuganizerClient] = None,
+        notifier: ImportNotifier,
         auto_file_bugs: bool = True,
     ):
         """File bugs for the last imported CL and add them to TestExpectations.
@@ -680,10 +691,6 @@
               (origin/main) <tip-of-tree>
         """
         _log.info('Filing bugs for the last WPT import')
-        from blinkpy.w3c.import_notifier import ImportNotifier
-        # Construct the notifier here so that any errors won't affect the import.
-        notifier = ImportNotifier(self.host, self.project_git, local_wpt,
-                                  gerrit_api, buganizer_client)
         bugs_by_dir = notifier.main(dry_run=(not auto_file_bugs))
         referenced_bugs = self._update_bugs_in_expectations(
             notifier, bugs_by_dir)
@@ -714,7 +721,7 @@
 
     def _update_bugs_in_expectations(
         self,
-        notifier,
+        notifier: ImportNotifier,
         bugs_by_dir: Mapping[str, BuganizerIssue],
     ) -> Set[int]:
         expectations = TestExpectations(
diff --git a/third_party/blink/tools/blinkpy/w3c/test_importer_unittest.py b/third_party/blink/tools/blinkpy/w3c/test_importer_unittest.py
index 6d2c5091..f9bf96c 100644
--- a/third_party/blink/tools/blinkpy/w3c/test_importer_unittest.py
+++ b/third_party/blink/tools/blinkpy/w3c/test_importer_unittest.py
@@ -8,6 +8,7 @@
 import unittest
 from unittest import mock
 
+from blinkpy.common.checkout.git import CommitRange
 from blinkpy.common.checkout.git_mock import MockGit
 from blinkpy.common.host_mock import MockHost
 from blinkpy.common.net.git_cl import TryJobStatus
@@ -21,6 +22,7 @@
 from blinkpy.w3c.buganizer import BuganizerIssue
 from blinkpy.w3c.chromium_commit_mock import MockChromiumCommit
 from blinkpy.w3c.directory_owners_extractor import WPTDirMetadata
+from blinkpy.w3c.import_notifier import ImportNotifier
 from blinkpy.w3c.local_wpt import LocalWPT
 from blinkpy.w3c.local_wpt_mock import MockLocalWPT
 from blinkpy.w3c.test_importer import TestImporter, ROTATIONS_URL, SHERIFF_EMAIL_FALLBACK, RUBBER_STAMPER_BOT
@@ -450,44 +452,68 @@
     def test_commit_message(self):
         importer = self._get_test_importer(self.mock_host())
         self.assertEqual(
-            importer._commit_message('aaaa', '1111'), 'Import 1111\n\n'
-            'Using wpt-import in Chromium aaaa.\n\n'
-            'No-Export: true\n'
-            'Validate-Test-Flakiness: skip')
+            importer.commit_message('aaaa', CommitRange('0000', '1111')),
+            textwrap.dedent("""\
+                Import wpt@1111
+
+                https://github.com/web-platform-tests/wpt/compare/0000...1111
+
+                Using wpt-import in Chromium aaaa.
+                """))
+
+    def test_commit_message_with_pending_exportable_changes(self):
+        host = self.mock_host()
+        importer = self._get_test_importer(host)
+        locally_applied_commits = [
+            MockChromiumCommit(host, subject='Pending export 1'),
+            MockChromiumCommit(
+                host,
+                'refs/heads/main@{#222)',
+                subject=f'Pending export 2 with very long subject {"a" * 80}'),
+        ]
+        self.assertEqual(
+            importer.commit_message('aaaa', CommitRange('0000', '1111'),
+                                    locally_applied_commits),
+            textwrap.dedent("""\
+                Import wpt@1111
+
+                https://github.com/web-platform-tests/wpt/compare/0000...1111
+
+                Using wpt-import in Chromium aaaa.
+                With Chromium commits locally applied on WPT:
+                  14fd77e88e "Pending export 1"
+                  3e977a7ce6 "Pending export 2 with very long subject [...]
+                """)),
 
     def test_cl_description_with_empty_environ(self):
         host = self.mock_host()
-        host.executive = MockExecutive(output='Last commit message\n\n')
+        host.executive = MockExecutive(output='Last commit message\n')
         importer = self._get_test_importer(host)
-        description = importer._cl_description(directory_owners={})
+        description = importer.cl_description(directory_owners={})
         self.assertEqual(
-            description, 'Last commit message\n\n'
-            'Note to sheriffs: This CL imports external tests and adds\n'
-            'expectations for those tests; if this CL is large and causes\n'
-            'a few new failures, please fix the failures by adding new\n'
-            'lines to TestExpectations rather than reverting. See:\n'
-            'https://chromium.googlesource.com'
-            '/chromium/src/+/main/docs/testing/web_platform_tests.md\n\n'
-            'NOAUTOREVERT=true\n'
-            'No-Export: true\n'
-            'Validate-Test-Flakiness: skip\n'
-            'Cq-Include-Trybots: luci.chromium.try:linux-blink-rel\n')
-        self.assertEqual(host.executive.calls,
-                         [MANIFEST_INSTALL_CMD] +
-                         [['git', 'log', '-1', '--format=%B']])
+            description,
+            textwrap.dedent("""\
+                Last commit message
 
-    def test_cl_description_moves_noexport_tag(self):
-        host = self.mock_host()
-        host.executive = MockExecutive(output='Summary\n\nNo-Export: true\n\n')
-        importer = self._get_test_importer(host)
-        description = importer._cl_description(directory_owners={})
-        self.assertIn('No-Export: true', description)
+                Note to gardeners: This CL imports external tests and adds expectations
+                for those tests; if this CL is large and causes a few new failures,
+                please fix the failures by adding new lines to TestExpectations rather
+                than reverting. See:
+                https://chromium.googlesource.com/chromium/src/+/main/docs/testing/web_platform_tests.md
+
+                NOAUTOREVERT=true
+                No-Export: true
+                Validate-Test-Flakiness: skip
+                Cq-Include-Trybots: luci.chromium.try:linux-blink-rel
+                """))
+        self.assertEqual(host.executive.calls, [MANIFEST_INSTALL_CMD] +
+                         [['git', 'log', '-1', '--format=%B']])
 
     def test_cl_description_with_directory_owners(self):
         host = self.mock_host()
-        host.executive = MockExecutive(output='Last commit message\n\n')
+        host.executive = MockExecutive(output='Last commit message\n')
         importer = self._get_test_importer(host)
-        description = importer._cl_description(
+        description = importer.cl_description(
             directory_owners={
                 ('someone@chromium.org', ):
                 ['external/wpt/foo', 'external/wpt/bar'],
@@ -631,12 +657,13 @@
                 **dataclasses.asdict(issue),
                 'issue_id': 111,
             })
+        notifier = ImportNotifier(host, git, local_wpt, gerrit_api,
+                                  buganizer_client)
         with mock.patch(
                 'blinkpy.w3c.import_notifier.'
                 'DirectoryOwnersExtractor.read_dir_metadata',
                 return_value=WPTDirMetadata(should_notify=True)):
-            importer.file_and_record_bugs(local_wpt, gerrit_api,
-                                          buganizer_client)
+            importer.file_and_record_bugs(notifier)
 
         gerrit_cl.post_comment.assert_called_once_with(
             'Filed bugs for failures introduced by this CL: '
@@ -697,12 +724,13 @@
         local_wpt = MockLocalWPT()
         gerrit_api, buganizer_client = mock.Mock(), mock.Mock()
         gerrit_api.query_cls.return_value = [mock.Mock(messages=[])]
+        notifier = ImportNotifier(host, git, local_wpt, gerrit_api,
+                                  buganizer_client)
         with mock.patch(
                 'blinkpy.w3c.import_notifier.'
                 'DirectoryOwnersExtractor.read_dir_metadata',
                 return_value=WPTDirMetadata(should_notify=True)):
-            importer.file_and_record_bugs(local_wpt, gerrit_api,
-                                          buganizer_client)
+            importer.file_and_record_bugs(notifier)
 
         self.assertEqual(
             git.show_blob(RELATIVE_WEB_TESTS + 'TestExpectations',