Title: [285765] trunk/Tools
Revision
285765
Author
jbed...@apple.com
Date
2021-11-12 17:16:24 -0800 (Fri, 12 Nov 2021)

Log Message

[git-webkit] Checkout pull-requests
https://bugs.webkit.org/show_bug.cgi?id=233042
<rdar://problem/85343364>

Reviewed by Dewei Zhu.

In GitHub, pull-requests are somewhat difficult to checkout because they're
attached to a specific user's mirror of WebKit. Automate this process.

* Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Scripts/libraries/webkitscmpy/webkitscmpy/__init.py__: Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py:
(Git): Add username:branch regex.
(Git.checkout): Allow checking out of branches by username:branch.
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py:
(Git.__init__): Add `git checkout -B`.
(Git.checkout): `-B` will force checkout a branch, even if one already exists.
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
(GitHub): Embed error message in 404 response.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py:
(Checkout.main): Allow direct checkout of pull-request instead of relying
exclusively on branches.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py:
(Clean.main): Add newline.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py:
(TestCheckout.test_no_pr_github):
(TestCheckout.test_no_pr_bitbucket):
(TestCheckout.test_pr_github):
(TestCheckout.test_pr_bitbucket):

Canonical link: https://commits.webkit.org/244211@main

Modified Paths

Diff

Modified: trunk/Tools/ChangeLog (285764 => 285765)


--- trunk/Tools/ChangeLog	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/ChangeLog	2021-11-13 01:16:24 UTC (rev 285765)
@@ -1,3 +1,35 @@
+2021-11-12  Jonathan Bedard  <jbed...@apple.com>
+
+        [git-webkit] Checkout pull-requests
+        https://bugs.webkit.org/show_bug.cgi?id=233042
+        <rdar://problem/85343364>
+
+        Reviewed by Dewei Zhu.
+
+        In GitHub, pull-requests are somewhat difficult to checkout because they're
+        attached to a specific user's mirror of WebKit. Automate this process.
+
+        * Scripts/libraries/webkitscmpy/setup.py: Bump version.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/__init.py__: Ditto.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py:
+        (Git): Add username:branch regex.
+        (Git.checkout): Allow checking out of branches by username:branch.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py:
+        (Git.__init__): Add `git checkout -B`.
+        (Git.checkout): `-B` will force checkout a branch, even if one already exists.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
+        (GitHub): Embed error message in 404 response.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py:
+        (Checkout.main): Allow direct checkout of pull-request instead of relying
+        exclusively on branches.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py:
+        (Clean.main): Add newline.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py:
+        (TestCheckout.test_no_pr_github):
+        (TestCheckout.test_no_pr_bitbucket):
+        (TestCheckout.test_pr_github):
+        (TestCheckout.test_pr_bitbucket):
+
 2021-11-12  Sihui Liu  <sihui_...@apple.com>
 
         Set default general storage directory to websiteDataDirectory

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -29,7 +29,7 @@
 
 setup(
     name='webkitscmpy',
-    version='3.0.0',
+    version='3.0.1',
     description='Library designed to interact with git and svn repositories.',
     long_description=readme(),
     classifiers=[

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -46,7 +46,7 @@
         "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
     )
 
-version = Version(3, 0, 0)
+version = Version(3, 0, 1)
 
 AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
 AutoInstall.register(Package('jinja2', Version(2, 11, 3)))

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -299,6 +299,7 @@
     SSH_REMOTE = re.compile('(ssh://)?git@(?P<host>[^:/]+)[:/](?P<path>.+).git')
     HTTP_REMOTE = re.compile(r'(?P<protocol>https?)://(?P<host>[^\/]+)/(?P<path>.+).git')
     REMOTE_BRANCH = re.compile(r'remotes\/(?P<remote>[^\/]+)\/(?P<branch>.+)')
+    USER_REMOTE = re.compile(r'(?P<username>[^:/]+):(?P<branch>.+)')
 
     @classmethod
     @decorators.Memoize()
@@ -820,6 +821,40 @@
         else:
             log_arg = []
 
+        match = self.USER_REMOTE.match(argument)
+        rmt = self.remote()
+        if match and isinstance(rmt, remote.GitHub):
+            username = match.group('username')
+            if not self.url(match.group('username')):
+                url = ""
+                if '://' in url:
+                    rmt = '{}://{}/{}/{}.git'.format(url.split(':')[0], url.split('/')[2], username, rmt.name)
+                elif ':' in url:
+                    rmt = '{}:{}/{}.git'.format(url.split(':')[0], username, rmt.name)
+                else:
+                    sys.stderr.write("Failed to convert '{}' to '{}' remote\n".format(url, username))
+                    return None
+                if run(
+                    [self.executable(), 'remote', 'add', username, rmt],
+                    capture_output=True, cwd=self.root_path,
+                ).returncode:
+                    sys.stderr.write("Failed to add remote '{}' as '{}'\n".format(rmt, username))
+                    return None
+                self.url.clear()
+            branch = match.group('branch')
+            rc = run(
+                [self.executable(), 'checkout'] + ['-B', branch, '{}/{}'.format(username, branch)] + log_arg,
+                cwd=self.root_path,
+            ).returncode
+            if not rc:
+                return self.commit()
+            if rc == 128:
+                run([self.executable(), 'fetch', username], cwd=self.root_path)
+            return None if run(
+                [self.executable(), 'checkout'] + ['-B', branch, '{}/{}'.format(username, branch)] + log_arg,
+                cwd=self.root_path,
+            ).returncode else self.commit()
+
         return None if run(
             [self.executable(), 'checkout'] + [self._to_git_ref(argument)] + log_arg,
             cwd=self.root_path,

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -352,6 +352,11 @@
                 generator=lambda *args, **kwargs:
                     mocks.ProcessCompletion(returncode=0) if self.checkout(args[3], create=True) else mocks.ProcessCompletion(returncode=1)
             ), mocks.Subprocess.Route(
+                self.executable, 'checkout', '-B', re.compile(r'.+'),
+                cwd=self.path,
+                generator=lambda *args, **kwargs:
+                    mocks.ProcessCompletion(returncode=0) if self.checkout(args[3], create=True, force=True) else mocks.ProcessCompletion(returncode=1)
+            ), mocks.Subprocess.Route(
                 self.executable, 'checkout', re.compile(r'.+'),
                 cwd=self.path,
                 generator=lambda *args, **kwargs:
@@ -588,10 +593,14 @@
                     result.add(branch)
         return result
 
-    def checkout(self, something, create=False):
+    def checkout(self, something, create=False, force=False):
         commit = self.find(something)
         if create:
             if commit:
+                if force:
+                    self.head = commit
+                    self.detached = something not in self.commits.keys()
+                    return True
                 return False
             if self.head.branch == self.default_branch:
                 self.commits[something] = [self.head]

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -21,9 +21,9 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import os
-import json
 import time
 
+import json as jsonlib
 from webkitcorepy import mocks
 from webkitscmpy import Commit, remote as scmremote
 
@@ -50,7 +50,7 @@
         super(GitHub, self).__init__(hostname, 'api.{}'.format(hostname))
 
         with open(datafile or os.path.join(os.path.dirname(os.path.dirname(__file__)), 'git-repo.json')) as file:
-            self.commits = json.load(file)
+            self.commits = jsonlib.load(file)
         for key, commits in self.commits.items():
             self.commits[key] = [Commit(**kwargs) for kwargs in commits]
             if not git_svn:
@@ -145,7 +145,7 @@
             return mocks.Response(
                 status_code=404,
                 url=""
-                text=json.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
+                text=jsonlib.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
             )
 
         response = []
@@ -191,7 +191,7 @@
             return mocks.Response(
                 status_code=404,
                 url=""
-                text=json.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
+                text=jsonlib.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
             )
         return mocks.Response.fromJson({
             'sha': commit.hash,
@@ -219,7 +219,7 @@
             return mocks.Response(
                 status_code=404,
                 url=""
-                text=json.dumps(dict(message='Not found')),
+                text=jsonlib.dumps(dict(message='Not found')),
             )
 
         if commit_a.branch != self.default_branch or commit_b.branch == self.default_branch:
@@ -331,7 +331,7 @@
         if stripped_url.startswith('{}/tree/'.format(self.remote)):
             return self._parents_of_request(url="" ref=stripped_url.split('/')[-1])
 
-        # Check for existance of forked repo
+        # Check for existence of forked repo
         if stripped_url.startswith('{}/repos'.format(self.api_remote.split('/')[0])) and stripped_url.split('/')[-1] == self.remote.split('/')[-1]:
             username = stripped_url.split('/')[-2]
             if username in self.forks or username == self.remote.split('/')[-2]:
@@ -381,7 +381,11 @@
                     return mocks.Response.fromJson({
                         key: value for key, value in candidate.items() if key not in ('requested_reviews', 'reviews')
                     }, url=""
-            return mocks.Response.create404(url)
+            return mocks.Response(
+                status_code=404,
+                text=jsonlib.dumps(dict(message='Not found')),
+                url=""
+            )
 
         # Create/update pull-request
         pr = dict()

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -20,17 +20,20 @@
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
+import re
 import sys
 
 from .command import Command
-from webkitcorepy import arguments
-from webkitscmpy import local
+from webkitscmpy import local, log, remote
 
 
 class Checkout(Command):
     name = 'checkout'
-    help = 'Given an identifier, revision or hash, normalize and checkout that commit'
+    help = "Given an identifier, revision, hash or pull-request, normalize and checkout that commit." \
+           " Pull requests expected in the form 'PR-#'"
 
+    PR_RE = re.compile(r'^\[?[Pp][Rr][ -](?P<number>\d+)]?$')
+
     @classmethod
     def parser(cls, parser, loggers=None):
         parser.add_argument(
@@ -42,11 +45,31 @@
     @classmethod
     def main(cls, args, repository, **kwargs):
         if not repository.path:
-            sys.stderr.write("Cannot checkout on remote repository")
+            sys.stderr.write('Cannot checkout on remote repository\n')
             return 1
 
+        target = args.argument[0]
+        match = cls.PR_RE.match(target)
+        if match:
+            rmt = repository.remote()
+            if not rmt:
+                sys.stderr.write('Repository does not have associated remote\n')
+                return 1
+            if not rmt.pull_requests:
+                sys.stderr.write('No pull-requests associated with repository\n')
+                return 1
+            pr = rmt.pull_requests.get(number=int(match.group('number')))
+            if not pr:
+                sys.stderr.write("Failed to find 'PR-{}' associated with this repository\n".format(match.group('number')))
+                return 1
+            if isinstance(rmt, remote.GitHub) and pr.author.github:
+                target = '{}:{}'.format(pr.author.github, pr.head)
+            else:
+                target = pr.head
+            log.warning("Found associated branch '{}' for '{}'".format(target, pr))
+
         try:
-            commit = repository.checkout(args.argument[0])
+            commit = repository.checkout(target)
         except (local.Scm.Exception, ValueError) as exception:
             # ValueErrors and Scm exceptions usually contain enough information to be displayed
             # to the user as an error
@@ -54,6 +77,6 @@
             return 1
 
         if not commit:
-            sys.stderr.write("Failed to map '{}'\n".format(args.argument[0]))
+            sys.stderr.write("Failed to checkout '{}'\n".format(args.argument[0]))
             return 1
         return 0

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -34,7 +34,7 @@
     @classmethod
     def main(cls, args, repository, **kwargs):
         if not repository.path:
-            sys.stderr.write('Cannot clean on remote repository')
+            sys.stderr.write('Cannot clean on remote repository\n')
             return 1
 
         return repository.clean()

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py (285764 => 285765)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py	2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py	2021-11-13 01:16:24 UTC (rev 285765)
@@ -21,10 +21,11 @@
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
 import os
+import time
 
 from webkitcorepy import OutputCapture, testing
 from webkitcorepy.mocks import Time as MockTime
-from webkitscmpy import program, mocks, local
+from webkitscmpy import program, mocks, local, Contributor, Commit
 
 
 class TestCheckout(testing.PathTestCase):
@@ -57,6 +58,132 @@
 
             self.assertEqual('621652add7fc416099bd2063366cc38ff61afe36', local.Git(self.path).commit().hash)
 
+    def test_no_pr_github(self):
+        with OutputCapture() as captured, mocks.remote.GitHub() as remote, \
+                mocks.local.Git(self.path, remote='https://{}'.format(remote.remote)), mocks.local.Svn():
+            self.assertEqual(1, program.main(
+                args=('checkout', 'PR-1'),
+                path=self.path,
+            ))
+
+        self.assertEqual(
+            "Request to 'https://api.github.example.com/repos/WebKit/WebKit/pulls/1' returned status code '404'\n"
+            "Message: Not found\n"
+            "Failed to find 'PR-1' associated with this repository\n",
+            captured.stderr.getvalue(),
+        )
+
+    def test_no_pr_bitbucket(self):
+        with OutputCapture() as captured, mocks.remote.BitBucket() as remote, mocks.local.Git(self.path, remote='ssh://git@{}/{}/{}.git'.format(
+            remote.hosts[0], remote.project.split('/')[1], remote.project.split('/')[3],
+        )), mocks.local.Svn():
+            self.assertEqual(1, program.main(
+                args=('checkout', 'PR-1'),
+                path=self.path,
+            ))
+
+        self.assertEqual(
+            "Request to 'https://bitbucket.example.com/rest/api/1.0/projects/WEBKIT/repos/webkit/pull-requests/1' returned status code '404'\n"
+            "Failed to find 'PR-1' associated with this repository\n",
+            captured.stderr.getvalue(),
+        )
+
+    def test_pr_github(self):
+        with OutputCapture(), mocks.remote.GitHub() as remote, \
+                mocks.local.Git(self.path, remote='https://{}'.format(remote.remote)) as repo, mocks.local.Svn():
+            remote.users = dict(
+                rreviewer=Contributor('Ricky Reviewer', ['rrevie...@webkit.org'], github='rreviewer'),
+                tcontributor=Contributor('Tim Contributor', ['tcontribu...@webkit.org'], github='tcontributor'),
+            )
+            remote.issues = {
+                1: dict(
+                    comments=[],
+                    assignees=[],
+                )
+            }
+            remote.pull_requests = [dict(
+                number=1,
+                state='open',
+                title='Example Change',
+                user=dict(login='tcontributor'),
+                body='''#### a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd
+<pre>
+To Be Committed
+
+Reviewed by NOBODY (OOPS!).
+</pre>
+''',
+                head=dict(ref='tcontributor:eng/example'),
+                base=dict(ref='main'),
+                requested_reviews=[dict(login='rreviewer')],
+                reviews=[dict(user=dict(login='rreviewer'), state='CHANGES_REQUESTED')],
+            )]
+            repo.commits['eng/example'] = [Commit(
+                hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+                identifier='3.1@eng/example',
+                timestamp=int(time.time()) - 60,
+                author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+                message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
+            )]
+
+            self.assertEqual(0, program.main(
+                args=('checkout', 'PR-1'),
+                path=self.path,
+            ))
+
+            self.assertEqual('a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd', local.Git(self.path).commit().hash)
+
+    def test_pr_bitbucket(self):
+        with OutputCapture(), mocks.remote.BitBucket() as remote, mocks.local.Git(self.path, remote='ssh://git@{}/{}/{}.git'.format(
+            remote.hosts[0], remote.project.split('/')[1], remote.project.split('/')[3],
+        )) as repo, mocks.local.Svn():
+            remote.pull_requests = [dict(
+                id=1,
+                state='OPEN',
+                open=True,
+                closed=False,
+                activities=[],
+                title='Example Change',
+                author=dict(
+                    user=dict(
+                        name='tcontributor',
+                        emailAddress='tcontribu...@apple.com',
+                        displayName='Tim Contributor',
+                    ),
+                ), body='''#### a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd
+```
+To Be Committed
+
+Reviewed by NOBODY (OOPS!).
+```
+''',
+                fromRef=dict(displayId='eng/example', id='refs/heads/eng/example'),
+                toRef=dict(displayId='main', id='refs/heads/main'),
+                reviewers=[
+                    dict(
+                        user=dict(
+                            displayName='Ricky Reviewer',
+                            emailAddress='rrevie...@webkit.org',
+                        ), approved=False,
+                        status='NEEDS_WORK',
+                    ),
+                ],
+            )]
+            repo.commits['eng/example'] = [Commit(
+                hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+                identifier='3.1@eng/example',
+                timestamp=int(time.time()) - 60,
+                author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+                message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
+            )]
+
+            self.assertEqual(0, program.main(
+                args=('checkout', 'PR-1'),
+                path=self.path,
+            ))
+
+            self.assertEqual('a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd', local.Git(self.path).commit().hash)
+
     def test_checkout_svn(self):
         with OutputCapture(), mocks.local.Git(), mocks.local.Svn(self.path), MockTime:
             self.assertEqual(6, local.Svn(self.path).commit().revision)
@@ -68,6 +195,18 @@
 
             self.assertEqual(4, local.Svn(self.path).commit().revision)
 
+    def test_svn_pr(self):
+        with OutputCapture() as captured, mocks.local.Git(), mocks.local.Svn(self.path), MockTime:
+            self.assertEqual(1, program.main(
+                args=('checkout', 'PR-1'),
+                path=self.path,
+            ))
+
+            self.assertEqual(
+                'No pull-requests associated with repository\n',
+                captured.stderr.getvalue(),
+            )
+
     def test_checkout_remote(self):
         with OutputCapture(), mocks.remote.Svn():
             self.assertEqual(1, program.main(
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to