Title: [273333] trunk/Tools
Revision
273333
Author
jbed...@apple.com
Date
2021-02-23 14:22:25 -0800 (Tue, 23 Feb 2021)

Log Message

[webkitscmpy] Add remote BitBucket
https://bugs.webkit.org/show_bug.cgi?id=222213
<rdar://problem/74542626>

Rubber-stamped by Aakash Jain.

* Scripts/libraries/webkitscmpy/setup.py: Increment version.
* Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/__init__.py: Export mock BitBucket.
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py: Added.
(BitBucket): Mock limited set of BitBucket REST APIs.
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/__init__.py: Export BitBucket class.
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py: Added.
(BitBucket): Repository object interacting with BitBucket via REST API.
(BitBucket.is_webserver): Check if the provided URL is a bitbucket URL.
(BitBucket.__init__):
(BitBucket.is_git):
(BitBucket.request): Combine paginated requests into a single API call.
(BitBucket._distance): Preform binary search 
(BitBucket._branches_for): Return branches for reference.
(BitBucket.default_branch): Return the default branch.
(BitBucket.branches): Return all branches for repository.
(BitBucket.tags): Return all tags for repository.
(BitBucket.commit): Convert hash, identifier or git ref to Commit object.
(BitBucket.find): Use git to match branches and tags to a hash instead of trying to do it ourselves.
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py:
(Scm.from_url): Add BitBucket.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
(TestGitHub.test_detection): Add bitbucket url.
(TestBitBucket): Added.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py:
(TestRemoteSvn.test_detection): Add bitbucket url.

Modified Paths

Added Paths

Diff

Modified: trunk/Tools/ChangeLog (273332 => 273333)


--- trunk/Tools/ChangeLog	2021-02-23 22:05:31 UTC (rev 273332)
+++ trunk/Tools/ChangeLog	2021-02-23 22:22:25 UTC (rev 273333)
@@ -1,3 +1,38 @@
+2021-02-23  Jonathan Bedard  <jbed...@apple.com>
+
+        [webkitscmpy] Add remote BitBucket
+        https://bugs.webkit.org/show_bug.cgi?id=222213
+        <rdar://problem/74542626>
+
+        Rubber-stamped by Aakash Jain.
+
+        * Scripts/libraries/webkitscmpy/setup.py: Increment version.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/__init__.py: Export mock BitBucket.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py: Added.
+        (BitBucket): Mock limited set of BitBucket REST APIs.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/__init__.py: Export BitBucket class.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py: Added.
+        (BitBucket): Repository object interacting with BitBucket via REST API.
+        (BitBucket.is_webserver): Check if the provided URL is a bitbucket URL.
+        (BitBucket.__init__):
+        (BitBucket.is_git):
+        (BitBucket.request): Combine paginated requests into a single API call.
+        (BitBucket._distance): Preform binary search 
+        (BitBucket._branches_for): Return branches for reference.
+        (BitBucket.default_branch): Return the default branch.
+        (BitBucket.branches): Return all branches for repository.
+        (BitBucket.tags): Return all tags for repository.
+        (BitBucket.commit): Convert hash, identifier or git ref to Commit object.
+        (BitBucket.find): Use git to match branches and tags to a hash instead of trying to do it ourselves.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py:
+        (Scm.from_url): Add BitBucket.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
+        (TestGitHub.test_detection): Add bitbucket url.
+        (TestBitBucket): Added.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py:
+        (TestRemoteSvn.test_detection): Add bitbucket url.
+
 2021-02-23  Don Olmstead  <don.olmst...@sony.com>
 
         Pass full environment when auto installing a Python module on Windows

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/__init__.py (273332 => 273333)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/__init__.py	2021-02-23 22:05:31 UTC (rev 273332)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/__init__.py	2021-02-23 22:22:25 UTC (rev 273333)
@@ -20,5 +20,6 @@
 # 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.
 
+from webkitscmpy.mocks.remote.bitbucket import BitBucket
 from webkitscmpy.mocks.remote.git_hub import GitHub
 from webkitscmpy.mocks.remote.svn import Svn

Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py (0 => 273333)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py	                        (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py	2021-02-23 22:22:25 UTC (rev 273333)
@@ -0,0 +1,195 @@
+# Copyright (C) 2020 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# 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 os
+import json
+
+from webkitcorepy import mocks
+from webkitscmpy import Commit, remote as scmremote
+
+
+class BitBucket(mocks.Requests):
+    top = None
+
+    def __init__(
+        self, remote='bitbucket.example.com/projects/WEBKIT/repos/webkit', datafile=None,
+        default_branch='main', git_svn=False,
+    ):
+        if not scmremote.BitBucket.is_webserver('https://{}'.format(remote)):
+            raise ValueError('"{}" is not a valid BitBucket remote'.format(remote))
+
+        self.default_branch = default_branch
+        self.remote = remote
+        self.project = '/'.join(remote.split('/')[1:])
+
+        super(BitBucket, self).__init__(self.remote.split('/')[0])
+
+        with open(datafile or os.path.join(os.path.dirname(os.path.dirname(__file__)), 'git-repo.json')) as file:
+            self.commits = json.load(file)
+        for key, commits in self.commits.items():
+            self.commits[key] = [Commit(**kwargs) for kwargs in commits]
+            if not git_svn:
+                for commit in self.commits[key]:
+                    commit.revision = None
+
+        self.head = self.commits[self.default_branch][-1]
+        self.tags = {}
+
+    def commit(self, ref):
+        if ref in self.commits:
+            return self.commits[ref][-1]
+        if ref in self.tags:
+            return self.tags[ref]
+
+        for branch, commits in self.commits.items():
+            for commit in commits:
+                if commit.hash.startswith(ref):
+                    return commit
+
+        if '~' not in ref:
+            return None
+        ref, delta = ref.split('~')
+        commit = self.commit(ref)
+        if not commit:
+            return None
+        delta = int(delta)
+
+        if delta < commit.identifier:
+            return self.commits[commit.branch][commit.identifier - delta - 1]
+        delta -= commit.identifier
+        if commit.branch_point and delta < commit.branch_point:
+            return self.commits[self.default_branch][commit.branch_point - delta - 1]
+        return None
+
+    def _branches_default(self, url):
+        recent = self.commit(self.default_branch)
+        return mocks.Response.fromJson(dict(
+            id='refs/heads/{}'.format(self.default_branch),
+            displayId=self.default_branch,
+            type='BRANCH',
+            latestCommit=recent.hash,
+            latestChangeset=recent.hash,
+            isDefault=True,
+        ), url=""
+
+    def _branches(self, url, params):
+        limit = params.get('limit', 25)
+        start = params.get('start', 0)
+        branches = [branch for branch in sorted(self.commits.keys())[start * limit: (start + 1) * limit]]
+
+        return mocks.Response.fromJson(dict(
+            size=len(branches),
+            limit=limit,
+            isLastPage=(start + 1) * limit > len(self.commits.keys()),
+            start=start,
+            nextPageStart=start + limit,
+            values=[
+                dict(
+                    id='refs/heads/{}'.format(branch),
+                    displayId=branch,
+                    type='BRANCH',
+                    isDefault=self.default_branch == branch,
+                    latestCommit=self.commits[branch][-1].hash,
+                ) for branch in branches
+            ],
+        ), url=""
+
+    def _tags(self, url, params):
+        limit = params.get('limit', 25)
+        start = params.get('start', 0)
+        tags = [tag for tag in sorted(self.tags.keys())[start * limit: (start + 1) * limit]]
+
+        return mocks.Response.fromJson(dict(
+            size=len(tags),
+            limit=limit,
+            isLastPage=(start + 1) * limit > len(self.tags.keys()),
+            start=start,
+            nextPageStart=start + limit,
+            values=[
+                dict(
+                    id='refs/tags/{}'.format(tag),
+                    displayId=tag,
+                    type='TAG',
+                    latestCommit=self.tags[tag].hash,
+                ) for tag in tags
+            ],
+        ), url=""
+
+    def _branches_for(self, ref, url, params):
+        limit = params.get('limit', 25)
+        start = params.get('start', 0)
+        commit = self.commit(ref)
+        if not commit:
+            return mocks.Response.create404(url)
+
+        branches = [commit.branch][start * limit: (start + 1) * limit]
+        return mocks.Response.fromJson(dict(
+            size=len(branches),
+            limit=limit,
+            isLastPage=(start + 1) * limit > 1,
+            start=start,
+            nextPageStart=start + limit,
+            values=[
+                dict(
+                    id='refs/tags/{}'.format(branch),
+                    displayId=branch,
+                    type='BRANCH',
+                ) for branch in branches
+            ],
+        ), url=""
+
+    def request(self, method, url, data="" params=None, **kwargs):
+        if not url.startswith('http://') and not url.startswith('https://'):
+            return mocks.Response.create404(url)
+
+        stripped_url = url.split('://')[-1]
+        if stripped_url == '{}/rest/api/1.0/{}/branches/default'.format(self.hosts[0], self.project):
+            return self._branches_default(url)
+
+        if stripped_url == '{}/rest/api/1.0/{}/branches'.format(self.hosts[0], self.project):
+            return self._branches(url, params or {})
+
+        if stripped_url == '{}/rest/api/1.0/{}/tags'.format(self.hosts[0], self.project):
+            return self._tags(url, params or {})
+
+        if stripped_url.startswith('{}/rest/api/1.0/{}/commits/'.format(self.hosts[0], self.project)):
+            commit = self.commit(stripped_url.split('/')[-1])
+            if not commit:
+                return mocks.Response.create404(url)
+            return mocks.Response.fromJson(dict(
+                id=commit.hash,
+                displayId=commit.hash[:12],
+                author=dict(
+                    emailAddress=commit.author.email,
+                    displayName=commit.author.name,
+                ), committer=dict(
+                    emailAddress=commit.author.email,
+                    displayName=commit.author.name,
+                ),
+                committerTimestamp=commit.timestamp * 100,
+                message=commit.message,
+            ))
+
+        if stripped_url.startswith('{}/rest/branch-utils/latest/{}/branches/info/'.format(self.hosts[0], self.project)):
+            return self._branches_for(stripped_url.split('/')[-1], url, params or {})
+
+        return mocks.Response.create404(url)

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/__init__.py (273332 => 273333)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/__init__.py	2021-02-23 22:05:31 UTC (rev 273332)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/__init__.py	2021-02-23 22:22:25 UTC (rev 273333)
@@ -23,3 +23,4 @@
 from webkitscmpy.remote.scm import Scm
 from webkitscmpy.remote.svn import Svn
 from webkitscmpy.remote.git_hub import GitHub
+from webkitscmpy.remote.bitbucket import BitBucket

Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py (0 => 273333)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py	                        (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py	2021-02-23 22:22:25 UTC (rev 273333)
@@ -0,0 +1,265 @@
+# Copyright (C) 2021 Apple Inc. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+# 1.  Redistributions of source code must retain the above copyright
+#     notice, this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+#     notice, this list of conditions and the following disclaimer in the
+#     documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# 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 requests
+import six
+import sys
+
+from webkitcorepy import decorators
+from webkitscmpy import Commit
+from webkitscmpy.remote.scm import Scm
+
+
+class BitBucket(Scm):
+    URL_RE = re.compile(r'\Ahttps?://(?P<domain>\S+)/projects/(?P<project>\S+)/repos/(?P<repository>\S+)\Z')
+
+    @classmethod
+    def is_webserver(cls, url):
+        return True if cls.URL_RE.match(url) else False
+
+    def __init__(self, url, dev_branches=None, prod_branches=None, contributors=None):
+        match = self.URL_RE.match(url)
+        if not match:
+            raise self.Exception("'{}' is not a valid BitBucket project".format(url))
+        self.domain = match.group('domain')
+        self.project = match.group('project')
+        self.name = match.group('repository')
+
+        super(BitBucket, self).__init__(url, dev_branches=dev_branches, prod_branches=prod_branches, contributors=contributors)
+
+    @property
+    def is_git(self):
+        return True
+
+    def request(self, path=None, params=None, headers=None, api=None, ignore_errors=False):
+        headers = {key: value for key, value in headers.items()} if headers else dict()
+
+        params = {key: value for key, value in params.items()} if params else dict()
+        params['limit'] = params.get('limit', 500)
+        params['start'] = 0
+        url = ''.format(
+            api=api or 'api/1.0',
+            domain=self.domain,
+            project=self.project,
+            name=self.name,
+            path='/{}'.format(path) if path else '',
+        )
+        response = requests.get(url, params=params, headers=headers)
+        if response.status_code != 200:
+            if not ignore_errors:
+                sys.stderr.write("Request to '{}' returned status code '{}'\n".format(url, response.status_code))
+            return None
+        response = response.json()
+        result = response.get('values', None)
+        if result is None:
+            return response
+
+        while not response.get('isLastPage', True):
+            params['start'] += params['limit']
+            response = requests.get(url, params=params, headers=headers)
+            if response.status_code != 200:
+                if ignore_errors:
+                    break
+                raise self.Exception("Failed to assemble pagination requests for '{}', failed on start {}".format(url, params['start']))
+            response = response.json()
+            result.extend(response.get('values', []))
+        return result
+
+    @decorators.Memoize()
+    def _distance(self, ref, magnitude=None, condition=None):
+        bound = [0, magnitude if magnitude else 65536]
+        condition = condition or (lambda val: val)
+
+        branches = self._branches_for('{}~{}'.format(ref, bound[1]), ignore_errors=True)
+        while branches and condition(branches):
+            bound = [bound[1], bound[1] * 2]
+            branches = self._branches_for('{}~{}'.format(ref, bound[1]), ignore_errors=True)
+
+        while True:
+            current = bound[0] + int((bound[1] - bound[0]) / 2)
+
+            branches = self._branches_for('{}~{}'.format(ref, current), ignore_errors=True)
+            if branches and condition(branches):
+                if bound[1] - bound[0] <= 1:
+                    return current + 1
+                bound = [current, bound[1]]
+            else:
+                if bound[1] - bound[0] <= 1:
+                    return bound[1] + 1 if current == bound[0] else bound[0] + 1
+                bound = [bound[0], current]
+
+    def _branches_for(self, hash, ignore_errors=False):
+        response = self.request('branches/info/{}'.format(hash), api='branch-utils/latest', ignore_errors=ignore_errors)
+        if not response:
+            return []
+        return sorted([details.get('displayId') for details in response if details.get('displayId')])
+
+    @property
+    @decorators.Memoize()
+    def default_branch(self):
+        response = self.request('branches/default')
+        if not response:
+            raise self.Exception("Failed to query {} for {}'s default branch".format(self.domain, self.name))
+        return response.get('displayId')
+
+    @property
+    def branches(self):
+        response = self.request('branches')
+        if not response:
+            return [self.default_branch]
+        return sorted([details.get('displayId') for details in response if details.get('displayId')])
+
+    @property
+    def tags(self):
+        response = self.request('tags')
+        if not response:
+            return []
+        return sorted([details.get('displayId') for details in response if details.get('displayId')])
+
+    def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True):
+        if revision:
+            raise self.Exception('Cannot map revisions to commits on BitBucket')
+
+        if identifier is not None:
+            if revision:
+                raise ValueError('Cannot define both revision and identifier')
+            if hash:
+                raise ValueError('Cannot define both hash and identifier')
+            if tag:
+                raise ValueError('Cannot define both tag and identifier')
+
+            parsed_branch_point, identifier, parsed_branch = Commit._parse_identifier(identifier, do_assert=True)
+            if parsed_branch:
+                if branch and branch != parsed_branch:
+                    raise ValueError(
+                        "Caller passed both 'branch' and 'identifier', but specified different branches ({} and {})".format(
+                            branch, parsed_branch,
+                        ),
+                    )
+                branch = parsed_branch
+
+            branch = branch or self.default_branch
+            is_default = branch == self.default_branch
+
+            if is_default and parsed_branch_point:
+                raise self.Exception('Cannot provide a branch point for a commit on the default branch')
+
+            commit_data = self.request('commits/{}'.format(branch), params=dict(limit=1))
+            if not commit_data:
+                raise self.Exception("Failed to retrieve commit information for '{}'".format(branch))
+            base_ref = commit_data['id']
+
+            if is_default:
+                base_count = self._distance(base_ref)
+            else:
+                base_count = self._distance(base_ref, magnitude=256, condition=lambda val: self.default_branch not in val)
+
+            if identifier > base_count:
+                raise self.Exception('Identifier {} cannot be found on {}'.format(identifier, branch))
+
+            # Negative identifiers are actually commits on the default branch, we will need to re-compute the identifier
+            if identifier < 0 and is_default:
+                raise self.Exception('Illegal negative identifier on the default branch')
+
+            commit_data = self.request('commits/{}~{}'.format(base_ref, base_count - identifier))
+            if not commit_data:
+                raise self.Exception("Failed to retrieve commit information for '{}@{}'".format(identifier, branch or 'HEAD'))
+
+            # If an identifier is negative, unset it so we re-compute before constructing the commit.
+            if identifier <= 0:
+                identifier = None
+
+        elif branch or tag:
+            if hash:
+                raise ValueError('Cannot define both tag/branch and hash')
+            if branch and tag:
+                raise ValueError('Cannot define both tag and branch')
+            commit_data = self.request('commits/{}'.format(branch or tag))
+            if not commit_data:
+                raise self.Exception("Failed to retrieve commit information for '{}'".format(branch or tag))
+
+        else:
+            hash = Commit._parse_hash(hash, do_assert=True)
+            commit_data = self.request('commits/{}'.format(hash or self.default_branch))
+            if not commit_data:
+                raise self.Exception("Failed to retrieve commit information for '{}'".format(hash or 'HEAD'))
+
+        branches = self._branches_for(commit_data['id'])
+        if branches:
+            branch = self.prioritize_branches(branches)
+
+        else:
+            # A commit not on any branches cannot have an identifier
+            identifier = None
+            branch = None
+
+        branch_point = None
+        if branch and branch == self.default_branch:
+            if not identifier:
+                identifier = self._distance(commit_data['id'])
+
+        elif branch:
+            if not identifier:
+                identifier = self._distance(commit_data['id'], magnitude=256, condition=lambda val: self.default_branch not in val)
+            branch_point = self._distance(commit_data['id']) - identifier
+
+        matches = self.GIT_SVN_REVISION.findall(commit_data['message'])
+        revision = int(matches[-1].split('@')[0]) if matches else None
+
+        return Commit(
+            hash=commit_data['id'],
+            revision=revision,
+            branch_point=branch_point,
+            identifier=identifier,
+            branch=branch,
+            timestamp=int(commit_data['committerTimestamp'] / 100),
+            author=self.contributors.create(
+                commit_data.get('committer', {}).get('displayName', None),
+                commit_data.get('committer', {}).get('emailAddress', None),
+            ), message=commit_data['message'] if include_log else None,
+        )
+
+    def find(self, argument, include_log=True):
+        if not isinstance(argument, six.string_types):
+            raise ValueError("Expected 'argument' to be a string, not '{}'".format(type(argument)))
+
+        if argument in self.DEFAULT_BRANCHES:
+            argument = self.default_branch
+
+        parsed_commit = Commit.parse(argument, do_assert=False)
+        if parsed_commit:
+            if parsed_commit.branch in self.DEFAULT_BRANCHES:
+                parsed_commit.branch = self.default_branch
+
+            return self.commit(
+                hash=parsed_commit.hash,
+                revision=parsed_commit.revision,
+                identifier=parsed_commit.identifier,
+                branch=parsed_commit.branch,
+                include_log=include_log,
+            )
+
+        commit_data = self.request('commits/{}'.format(argument))
+        if not commit_data:
+            raise ValueError("'{}' is not an argument recognized by git".format(argument))
+        return self.commit(hash=commit_data['id'], include_log=include_log)

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py (273332 => 273333)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py	2021-02-23 22:05:31 UTC (rev 273332)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py	2021-02-23 22:22:25 UTC (rev 273333)
@@ -30,10 +30,10 @@
     def from_url(cls, url, contributors=None):
         from webkitscmpy import remote
 
-        if remote.Svn.is_webserver(url):
-            return remote.Svn(url, contributors=contributors)
-        if remote.GitHub.is_webserver(url):
-            return remote.GitHub(url, contributors=contributors)
+        for candidate in [remote.Svn, remote.GitHub, remote.BitBucket]:
+            if candidate.is_webserver(url):
+                return candidate(url, contributors=contributors)
+
         raise OSError("'{}' is not a known SCM server".format(url))
 
     def __init__(self, url, dev_branches=None, prod_branches=None, contributors=None):

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py (273332 => 273333)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py	2021-02-23 22:05:31 UTC (rev 273332)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py	2021-02-23 22:22:25 UTC (rev 273333)
@@ -279,6 +279,7 @@
         self.assertEqual(remote.GitHub.is_webserver('https://github.example.com/WebKit/webkit'), True)
         self.assertEqual(remote.GitHub.is_webserver('http://github.example.com/WebKit/webkit'), True)
         self.assertEqual(remote.GitHub.is_webserver('https://svn.example.org/repository/webkit'), False)
+        self.assertEqual(remote.GitHub.is_webserver('https://bitbucket.example.com/projects/WebKit/repos/webkit'), False)
 
     def test_branches(self):
         with mocks.remote.GitHub():
@@ -378,3 +379,112 @@
         with mocks.remote.GitHub():
             self.assertEqual(str(remote.GitHub(self.remote).find('4@trunk')), '4@main')
             self.assertEqual(str(remote.GitHub(self.remote).find('4@master')), '4@main')
+
+
+class TestBitBucket(unittest.TestCase):
+    remote = 'https://bitbucket.example.com/projects/WEBKIT/repos/webkit'
+
+    def test_detection(self):
+        self.assertEqual(remote.BitBucket.is_webserver('https://bitbucket.example.com/projects/WebKit/repos/webkit'), True)
+        self.assertEqual(remote.BitBucket.is_webserver('http://bitbucket.example.com/projects/WebKit/repos/webkit'), True)
+        self.assertEqual(remote.BitBucket.is_webserver('https://svn.example.org/repository/webkit'), False)
+        self.assertEqual(remote.BitBucket.is_webserver('https://github.example.com/WebKit/webkit'), False)
+
+    def test_branches(self):
+        with mocks.remote.BitBucket():
+            self.assertEqual(
+                remote.BitBucket(self.remote).branches,
+                ['branch-a', 'branch-b', 'main'],
+            )
+
+    def test_tags(self):
+        with mocks.remote.BitBucket() as mock:
+            mock.tags['tag-1'] = mock.commits['branch-a'][-1]
+            mock.tags['tag-2'] = mock.commits['branch-b'][-1]
+
+            self.assertEqual(
+                remote.BitBucket(self.remote).tags,
+                ['tag-1', 'tag-2'],
+            )
+
+    def test_scm_type(self):
+        self.assertFalse(remote.BitBucket(self.remote).is_svn)
+        self.assertTrue(remote.BitBucket(self.remote).is_git)
+
+    def test_commit_revision(self):
+        with mocks.remote.BitBucket():
+            self.assertEqual('1@main', str(remote.BitBucket(self.remote).commit(hash='9b8311f2')))
+            self.assertEqual('2@main', str(remote.BitBucket(self.remote).commit(hash='fff83bb2')))
+            self.assertEqual('2.1@branch-a', str(remote.BitBucket(self.remote).commit(hash='a30ce849')))
+            self.assertEqual('3@main', str(remote.BitBucket(self.remote).commit(hash='1abe25b4')))
+            self.assertEqual('2.2@branch-b', str(remote.BitBucket(self.remote).commit(hash='3cd32e35')))
+            self.assertEqual('4@main', str(remote.BitBucket(self.remote).commit(hash='bae5d1e9')))
+            self.assertEqual('2.2@branch-a', str(remote.BitBucket(self.remote).commit(hash='621652ad')))
+            self.assertEqual('2.3@branch-b', str(remote.BitBucket(self.remote).commit(hash='790725a6')))
+
+    def test_commit_from_branch(self):
+        with mocks.remote.BitBucket():
+            self.assertEqual('4@main', str(remote.BitBucket(self.remote).commit(branch='main')))
+            self.assertEqual('2.2@branch-a', str(remote.BitBucket(self.remote).commit(branch='branch-a')))
+            self.assertEqual('2.3@branch-b', str(remote.BitBucket(self.remote).commit(branch='branch-b')))
+
+    def test_identifier(self):
+        with mocks.remote.BitBucket():
+            self.assertEqual(
+                '9b8311f25a77ba14923d9d5a6532103f54abefcb',
+                remote.BitBucket(self.remote).commit(identifier='1@main').hash,
+            )
+            self.assertEqual(
+                'fff83bb2d9171b4d9196e977eb0508fd57e7a08d',
+                remote.BitBucket(self.remote).commit(identifier='2@main').hash,
+            )
+            self.assertEqual(
+                'a30ce8494bf1ac2807a69844f726be4a9843ca55',
+                remote.BitBucket(self.remote).commit(identifier='2.1@branch-a').hash,
+            )
+            self.assertEqual(
+                '1abe25b443e985f93b90d830e4a7e3731336af4d',
+                remote.BitBucket(self.remote).commit(identifier='3@main').hash,
+            )
+            self.assertEqual(
+                '3cd32e352410565bb543821fbf856a6d3caad1c4',
+                remote.BitBucket(self.remote).commit(identifier='2.2@branch-b').hash,
+            )
+            self.assertEqual(
+                'bae5d1e90999d4f916a8a15810ccfa43f37a2fd6',
+                remote.BitBucket(self.remote).commit(identifier='4@main').hash,
+            )
+            self.assertEqual(
+                '621652add7fc416099bd2063366cc38ff61afe36',
+                remote.BitBucket(self.remote).commit(identifier='2.2@branch-a').hash,
+            )
+            self.assertEqual(
+                '790725a6d79e28db2ecdde29548d2262c0bd059d',
+                remote.BitBucket(self.remote).commit(identifier='2.3@branch-b').hash,
+            )
+
+    def test_non_cannonical_identifiers(self):
+        with mocks.remote.BitBucket():
+            self.assertEqual('2@main', str(remote.BitBucket(self.remote).commit(identifier='0@branch-a')))
+            self.assertEqual('1@main', str(remote.BitBucket(self.remote).commit(identifier='-1@branch-a')))
+
+            self.assertEqual('2@main', str(remote.BitBucket(self.remote).commit(identifier='0@branch-b')))
+            self.assertEqual('1@main', str(remote.BitBucket(self.remote).commit(identifier='-1@branch-b')))
+
+    def test_tag(self):
+        with mocks.remote.BitBucket() as mock:
+            mock.tags['tag-1'] = mock.commits['branch-a'][-1]
+
+            self.assertEqual(
+                '621652add7fc416099bd2063366cc38ff61afe36',
+                remote.BitBucket(self.remote).commit(tag='tag-1').hash,
+            )
+
+    def test_no_log(self):
+        with mocks.remote.BitBucket():
+            self.assertIsNone(remote.BitBucket(self.remote).commit(identifier='4@main', include_log=False).message)
+
+    def test_alternative_default_branch(self):
+        with mocks.remote.BitBucket():
+            self.assertEqual(str(remote.BitBucket(self.remote).find('4@trunk')), '4@main')
+            self.assertEqual(str(remote.BitBucket(self.remote).find('4@master')), '4@main')

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py (273332 => 273333)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py	2021-02-23 22:05:31 UTC (rev 273332)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py	2021-02-23 22:22:25 UTC (rev 273333)
@@ -235,6 +235,7 @@
         self.assertEqual(remote.Svn.is_webserver('https://svn.example.org/repository/webkit'), True)
         self.assertEqual(remote.Svn.is_webserver('http://svn.example.org/repository/webkit'), True)
         self.assertEqual(remote.Svn.is_webserver('https://github.example.org/WebKit/webkit'), False)
+        self.assertEqual(remote.GitHub.is_webserver('https://bitbucket.example.com/projects/WebKit/repos/webkit'), False)
 
     def test_branches(self):
         with mocks.remote.Svn():
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to