Reland "Refactor wpt-import to use common.checkout.git"

This is a reland of cd0c516c9dbe22b47d7f4325baae0a3b411ed634

Compared with the original CL, this one instantiates self.wpt_git
after local_wpt.fetch() to ensure local_wpt.path exists so that
Git.__init__ does not throw.

Original change's description:
> Refactor wpt-import to use common.checkout.git
>
> Some methods are not currently supported by common.checkout.git, and
> Git.run is used in such cases for now. Later we would like to port some
> of them (the reusable ones) to common.checkout.git.
>
> Bug: 676399
> Change-Id: I42459221c684f8ffeabb9991a64dea3489663d19
> Reviewed-on: https://chromium-review.googlesource.com/793984
> Reviewed-by: Quinten Yearsley <qyearsley@chromium.org>
> Commit-Queue: Robert Ma <robertma@chromium.org>
> Cr-Commit-Position: refs/heads/master@{#519777}

Bug: 676399
Change-Id: I919064f5599a1b503a9ce5db927cc6713d9f428f
Reviewed-on: https://chromium-review.googlesource.com/794610
Reviewed-by: Quinten Yearsley <qyearsley@chromium.org>
Commit-Queue: Robert Ma <robertma@chromium.org>
Cr-Commit-Position: refs/heads/master@{#520115}
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
index 25f8022d..173235c 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer.py
@@ -45,15 +45,27 @@
 
     def __init__(self, host, wpt_github=None):
         self.host = host
+        self.wpt_github = wpt_github
+
         self.executive = host.executive
         self.fs = host.filesystem
         self.finder = PathFinder(self.fs)
-        self.verbose = False
-        self.git_cl = None
-        self.wpt_github = wpt_github
+        self.chromium_git = self.host.git(self.finder.chromium_base())
         self.dest_path = self.finder.path_from_layout_tests('external', 'wpt')
 
+        # A common.net.git_cl.GitCL instance.
+        self.git_cl = None
+        # Another Git instance with local WPT as CWD, which can only be
+        # instantiated after the working directory is created.
+        self.wpt_git = None
+        # The WPT revision we are importing.
+        self.wpt_revision = None
+        self.verbose = False
+
     def main(self, argv=None):
+        # TODO(robertma): Test this method! Split it to make it easier to test
+        # if necessary.
+
         options = self.parse_args(argv)
 
         self.verbose = options.verbose
@@ -70,36 +82,36 @@
         gh_user = credentials.get('GH_USER')
         gh_token = credentials.get('GH_TOKEN')
         self.wpt_github = self.wpt_github or WPTGitHub(self.host, gh_user, gh_token)
-        local_wpt = LocalWPT(self.host, gh_token=gh_token)
         self.git_cl = GitCL(self.host, auth_refresh_token_json=options.auth_refresh_token_json)
 
-        _log.debug('Noting the current Chromium commit.')
-        # TODO(qyearsley): Use Git (self.host.git) to run git commands.
-        _, show_ref_output = self.run(['git', 'show-ref', '--verify', '--head', '--hash', 'HEAD'])
-        chromium_commit = show_ref_output.strip()
+        _log.debug('Noting the current Chromium revision.')
+        chromium_revision = self.chromium_git.latest_git_commit()
 
+        # Instantiate Git after local_wpt.fetch() to make sure the path exists.
+        local_wpt = LocalWPT(self.host, gh_token=gh_token)
         local_wpt.fetch()
+        self.wpt_git = self.host.git(local_wpt.path)
 
         if options.revision is not None:
             _log.info('Checking out %s', options.revision)
-            self.run(['git', 'checkout', options.revision], cwd=local_wpt.path)
+            self.wpt_git.run(['checkout', options.revision])
 
         _log.debug('Noting the revision we are importing.')
-        _, show_ref_output = self.run(['git', 'show-ref', 'origin/master'], cwd=local_wpt.path)
-        import_commit = 'wpt@%s' % show_ref_output.split()[0]
+        self.wpt_revision = self.wpt_git.latest_git_commit()
+        import_commit = 'wpt@%s' % self.wpt_revision
 
-        _log.info('Importing %s to Chromium %s', import_commit, chromium_commit)
+        _log.info('Importing %s to Chromium %s', import_commit, chromium_revision)
 
-        commit_message = self._commit_message(chromium_commit, import_commit)
-
-        if not options.ignore_exportable_commits:
+        if options.ignore_exportable_commits:
+            commit_message = self._commit_message(chromium_revision, import_commit)
+        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_commit, import_commit, locally_applied_commits=commits)
+                chromium_revision, import_commit, locally_applied_commits=commits)
 
         self._clear_out_dest_path()
 
@@ -107,7 +119,8 @@
         test_copier = TestCopier(self.host, local_wpt.path)
         test_copier.do_import()
 
-        self.run(['git', 'add', '--all', 'external/wpt'])
+        # TODO(robertma): Implement `add --all` in Git (it is different from `commit --all`).
+        self.chromium_git.run(['add', '--all', self.dest_path])
 
         self._generate_manifest()
 
@@ -120,8 +133,7 @@
         _log.info('Updating TestExpectations for any removed or renamed tests.')
         self.update_all_test_expectations_files(self._list_deleted_tests(), self._list_renamed_tests())
 
-        has_changes = self._has_changes()
-        if not has_changes:
+        if not self.chromium_git.has_working_directory_changes():
             _log.info('Done: no changes to import.')
             return 0
 
@@ -177,10 +189,10 @@
 
         if try_results and self.git_cl.some_failed(try_results):
             self.fetch_new_expectations_and_baselines()
-            if self.host.git().has_working_directory_changes():
+            if self.chromium_git.has_working_directory_changes():
                 self._generate_manifest()
                 message = 'Update test expectations and baselines.'
-                self.check_run(['git', 'commit', '-a', '-m', message])
+                self._commit_changes(message)
                 self._upload_patchset(message)
         return True
 
@@ -259,13 +271,11 @@
         return parser.parse_args(argv)
 
     def checkout_is_okay(self):
-        git_diff_retcode, _ = self.run(
-            ['git', 'diff', '--quiet', 'HEAD'], exit_on_failure=False)
-        if git_diff_retcode:
+        if self.chromium_git.has_working_directory_changes():
             _log.warning('Checkout is dirty; aborting.')
             return False
-        _, local_commits = self.run(
-            ['git', 'log', '--oneline', 'origin/master..HEAD'])
+        # TODO(robertma): Add a method in Git to query a range of commits.
+        local_commits = self.chromium_git.run(['log', '--oneline', 'origin/master..HEAD'])
         if local_commits:
             _log.warning('Checkout has local commits before import.')
         return True
@@ -304,10 +314,7 @@
                 _log.error('Commit cannot be applied cleanly:')
                 _log.error(error)
                 return None
-            self.run(
-                ['git', 'commit', '--all', '-F', '-'],
-                stdin='Applying patch %s' % commit.sha,
-                cwd=local_wpt.path)
+            self.wpt_git.commit_locally_with_message('Applying patch %s' % commit.sha)
         return commits
 
     def exportable_but_not_exported_commits(self, local_wpt):
@@ -336,7 +343,7 @@
         manifest_base_path = self.fs.normpath(
             self.fs.join(self.dest_path, '..', 'WPT_BASE_MANIFEST.json'))
         self.copyfile(manifest_path, manifest_base_path)
-        self.run(['git', 'add', manifest_base_path])
+        self.chromium_git.add_list([manifest_base_path])
 
     def _clear_out_dest_path(self):
         """Removes all files that are synced with upstream from Chromium WPT.
@@ -353,14 +360,10 @@
 
     def _commit_changes(self, commit_message):
         _log.info('Committing changes.')
-        self.run(['git', 'commit', '--all', '-F', '-'], stdin=commit_message)
-
-    def _has_changes(self):
-        return_code, _ = self.run(['git', 'diff', '--quiet', 'HEAD'], exit_on_failure=False)
-        return return_code == 1
+        self.chromium_git.commit_locally_with_message(commit_message)
 
     def _only_wpt_manifest_changed(self):
-        changed_files = self.host.git().changed_files()
+        changed_files = self.chromium_git.changed_files()
         wpt_base_manifest = self.fs.relpath(
             self.fs.join(self.dest_path, '..', 'WPT_BASE_MANIFEST.json'),
             self.finder.chromium_base())
@@ -408,30 +411,6 @@
         base = '/' + rel_path.replace('-expected.txt', '')
         return any((base + ext) in wpt_urls for ext in Port.supported_file_extensions)
 
-    def run(self, cmd, exit_on_failure=True, cwd=None, stdin=''):
-        _log.debug('Running command: %s', ' '.join(cmd))
-
-        cwd = cwd or self.finder.path_from_layout_tests()
-        proc = self.executive.popen(cmd, stdout=self.executive.PIPE, stderr=self.executive.PIPE, stdin=self.executive.PIPE, cwd=cwd)
-        out, err = proc.communicate(stdin)
-        if proc.returncode or self.verbose:
-            _log.info('# ret> %d', proc.returncode)
-            if out:
-                for line in out.splitlines():
-                    _log.info('# out> %s', line)
-            if err:
-                for line in err.splitlines():
-                    _log.info('# err> %s', line)
-        if exit_on_failure and proc.returncode:
-            self.host.exit(proc.returncode)
-        return proc.returncode, out
-
-    def check_run(self, command):
-        return_code, out = self.run(command)
-        if return_code:
-            raise Exception('%s failed with exit code %d.' % ' '.join(command), return_code)
-        return out
-
     def copyfile(self, source, destination):
         _log.debug('cp %s %s', source, destination)
         self.fs.copyfile(source, destination)
@@ -469,7 +448,7 @@
     def get_directory_owners(self):
         """Returns a mapping of email addresses to owners of changed tests."""
         _log.info('Gathering directory owners emails to CC.')
-        changed_files = self.host.git().changed_files()
+        changed_files = self.chromium_git.changed_files()
         extractor = DirectoryOwnersExtractor(self.fs)
         return extractor.list_owners(changed_files)
 
@@ -479,7 +458,8 @@
         Args:
             directory_owners: A dict of tuples of owner names to lists of directories.
         """
-        description = self.check_run(['git', 'log', '-1', '--format=%B'])
+        # TODO(robertma): Add a method in Git for getting the commit body.
+        description = self.chromium_git.run(['log', '-1', '--format=%B'])
         build_link = current_build_link(self.host)
         if build_link:
             description += 'Build: %s\n\n' % build_link
@@ -586,7 +566,8 @@
 
     def _list_deleted_tests(self):
         """List of layout tests that have been deleted."""
-        out = self.check_run(['git', 'diff', 'origin/master', '-M100%', '--diff-filter=D', '--name-only'])
+        # TODO(robertma): Improve Git.changed_files so that we can use it here.
+        out = self.chromium_git.run(['diff', 'origin/master', '-M100%', '--diff-filter=D', '--name-only'])
         deleted_tests = []
         for path in out.splitlines():
             test = self._relative_to_layout_test_dir(path)
@@ -599,7 +580,7 @@
 
         Returns a dict mapping source name to destination name.
         """
-        out = self.check_run(['git', 'diff', 'origin/master', '-M100%', '--diff-filter=R', '--name-status'])
+        out = self.chromium_git.run(['diff', 'origin/master', '-M100%', '--diff-filter=R', '--name-status'])
         renamed_tests = {}
         for line in out.splitlines():
             _, source_path, dest_path = line.split()
diff --git a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py
index 140e173f..723f19b 100644
--- a/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py
+++ b/third_party/WebKit/Tools/Scripts/webkitpy/w3c/test_importer_unittest.py
@@ -222,6 +222,7 @@
         # TODO(robertma): Consider using MockLocalWPT.
         host = MockHost()
         importer = TestImporter(host, wpt_github=MockWPTGitHub(pull_requests=[]))
+        importer.wpt_git = MockGit(cwd='/tmp/wpt', executive=host.executive)
         fake_commit = MockChromiumCommit(
             host, subject='My fake commit',
             patch=(
@@ -233,6 +234,8 @@
         importer.exportable_but_not_exported_commits = lambda _: [fake_commit]
         applied = importer.apply_exportable_commits_locally(LocalWPT(host))
         self.assertEqual(applied, [fake_commit])
+        # This assertion is implementation details of LocalWPT.apply_patch.
+        # TODO(robertma): Move this to local_wpt_unittest.py.
         self.assertEqual(host.executive.full_calls, [
             MockCall(
                 ['git', 'apply', '-'],
@@ -248,11 +251,10 @@
                 }),
             MockCall(
                 ['git', 'add', '.'],
-                kwargs={'input': None, 'cwd': '/tmp/wpt', 'env': None}),
-            MockCall(
-                ['git', 'commit', '--all', '-F', '-'],
-                kwargs={'cwd': '/tmp/wpt', 'env': None})
+                kwargs={'input': None, 'cwd': '/tmp/wpt', 'env': None})
         ])
+        self.assertEqual(importer.wpt_git.local_commits(),
+                         [['Applying patch 14fd77e88e42147c57935c49d9e3b2412b8491b7']])
 
     def test_apply_exportable_commits_locally_returns_none_on_failure(self):
         host = MockHost()
@@ -290,10 +292,8 @@
         host.filesystem.write_text_file('/mock-checkout/third_party/WebKit/LayoutTests/W3CImportExpectations', '')
         host.filesystem.write_text_file('/mock-checkout/third_party/WebKit/LayoutTests/external/wpt/foo/OWNERS',
                                         'someone@chromium.org\n')
-        git = MockGit(filesystem=host.filesystem, executive=host.executive, platform=host.platform)
-        git.changed_files = lambda: ['third_party/WebKit/LayoutTests/external/wpt/foo/x.html']
-        host.git = lambda: git
         importer = TestImporter(host)
+        importer.chromium_git.changed_files = lambda: ['third_party/WebKit/LayoutTests/external/wpt/foo/x.html']
         self.assertEqual(importer.get_directory_owners(), {('someone@chromium.org',): ['external/wpt/foo']})
 
     def test_get_directory_owners_no_changed_files(self):
@@ -309,11 +309,8 @@
     def test_commit_changes(self):
         host = MockHost()
         importer = TestImporter(host)
-        importer._has_changes = lambda: True
         importer._commit_changes('dummy message')
-        self.assertEqual(
-            host.executive.calls,
-            [['git', 'commit', '--all', '-F', '-']])
+        self.assertEqual(importer.chromium_git.local_commits(), [['dummy message']])
 
     def test_commit_message(self):
         importer = TestImporter(MockHost())
@@ -472,23 +469,21 @@
                     '--work',
                     '--tests-root',
                     blink_path + '/LayoutTests/external/wpt',
-                ],
-                [
-                    'git',
-                    'add',
-                    blink_path + '/LayoutTests/external/WPT_BASE_MANIFEST.json',
                 ]
             ])
+        self.assertEqual(importer.chromium_git.added_paths,
+                         {blink_path + '/LayoutTests/external/WPT_BASE_MANIFEST.json'})
 
     def test_only_wpt_manifest_changed(self):
         host = MockHost()
-        git = host.git()
-        git.changed_files = lambda: ['third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json',
-                                     'third_party/WebKit/LayoutTests/external/wpt/foo/x.html']
         importer = TestImporter(host)
+        importer.chromium_git.changed_files = lambda: [
+            'third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json',
+            'third_party/WebKit/LayoutTests/external/wpt/foo/x.html']
         self.assertFalse(importer._only_wpt_manifest_changed())
 
-        git.changed_files = lambda: ['third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json']
+        importer.chromium_git.changed_files = lambda: [
+            'third_party/WebKit/LayoutTests/external/WPT_BASE_MANIFEST.json']
         self.assertTrue(importer._only_wpt_manifest_changed())
 
     def test_delete_orphaned_baselines_basic(self):