⚠ Archived content — this site is no longer maintained.   Current WebKit documentation is at docs.webkit.org.

Changeset 284892 in webkit


Ignore:
Timestamp:
Oct 26, 2021, 1:10:33 PM (5 years ago)
Author:
Jonathan Bedard
Message:

[webkitscmpy] Comment and close pull-requests
https://bugs.webkit.org/show_bug.cgi?id=232095
<rdar://problem/84515738>

Reviewed by Dewei Zhu.

  • Scripts/libraries/webkitscmpy/setup.py: Bump version.
  • Scripts/libraries/webkitscmpy/webkitscmpy/init.py: Ditto.
  • Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py:

(BitBucket.request): Add support for activities, opening and closing pull-request.

  • Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:

(GitHub.init): Add issues.
(GitHub.request): Access issue underlying pull-request (which includes global comments).

  • Scripts/libraries/webkitscmpy/webkitscmpy/pull_request.py:

(PullRequest.Exception): Added.
(PullRequest.Comment): Added.
(PullRequest.init): Add list of pull-request comments, metadata used by generator.
(PullRequest.open): Re-open the pull-request.
(PullRequest.close): Close the pull-request.
(PullRequest.comment): Make a comment on the pull-request.
(PullRequest.comments): List all comments on a pull-request.

  • Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py:

(BitBucket.PRGenerator.update): Handle closing and opening of the pull-request.
(BitBucket.PRGenerator.comment): Make a comment on a pull-request.
(BitBucket.PRGenerator.comments): List all comments on a pull-request.

  • Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py:

(GitHub.PRGenerator.update): Handle closing and opening of the pull-request.
(GitHub.PRGenerator.comment): Make a comment on a issue underpinning a pull-request.
(GitHub.PRGenerator.comments): List all comments on the pull-request.

  • Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py:

(Scm.PRGenerator.update): Add support for opening and closing a pull-request.
(Scm.PRGenerator.comment): Make a comment on a pull-request.
(Scm.PRGenerator.comments): List all comments for a pull-request

  • Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py:
Location:
trunk/Tools
Files:
10 edited

Legend:

Unmodified
Added
Removed
  • trunk/Tools/ChangeLog

    r284891 r284892  
     12021-10-26  Jonathan Bedard  <jbedard@apple.com>
     2
     3        [webkitscmpy] Comment and close pull-requests
     4        https://bugs.webkit.org/show_bug.cgi?id=232095
     5        <rdar://problem/84515738>
     6
     7        Reviewed by Dewei Zhu.
     8
     9        * Scripts/libraries/webkitscmpy/setup.py: Bump version.
     10        * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
     11        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py:
     12        (BitBucket.request): Add support for activities, opening and closing pull-request.
     13        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
     14        (GitHub.__init__): Add issues.
     15        (GitHub.request): Access issue underlying pull-request (which includes global comments).
     16        * Scripts/libraries/webkitscmpy/webkitscmpy/pull_request.py:
     17        (PullRequest.Exception): Added.
     18        (PullRequest.Comment): Added.
     19        (PullRequest.__init__): Add list of pull-request comments, metadata used by generator.
     20        (PullRequest.open): Re-open the pull-request.
     21        (PullRequest.close): Close the pull-request.
     22        (PullRequest.comment): Make a comment on the pull-request.
     23        (PullRequest.comments): List all comments on a pull-request.
     24        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py:
     25        (BitBucket.PRGenerator.update): Handle closing and opening of the pull-request.
     26        (BitBucket.PRGenerator.comment): Make a comment on a pull-request.
     27        (BitBucket.PRGenerator.comments): List all comments on a pull-request.
     28        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py:
     29        (GitHub.PRGenerator.update): Handle closing and opening of the pull-request.
     30        (GitHub.PRGenerator.comment): Make a comment on a issue underpinning a pull-request.
     31        (GitHub.PRGenerator.comments): List all comments on the pull-request.
     32        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py:
     33        (Scm.PRGenerator.update): Add support for opening and closing a pull-request.
     34        (Scm.PRGenerator.comment): Make a comment on a pull-request.
     35        (Scm.PRGenerator.comments): List all comments for a pull-request
     36        * Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py:
     37
    1382021-10-26  Simon Fraser  <simon.fraser@apple.com>
    239
  • trunk/Tools/Scripts/libraries/webkitscmpy/setup.py

    r284498 r284892  
    3030setup(
    3131    name='webkitscmpy',
    32     version='2.2.15',
     32    version='2.2.16',
    3333    description='Library designed to interact with git and svn repositories.',
    3434    long_description=readme(),
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py

    r284498 r284892  
    4747    )
    4848
    49 version = Version(2, 2, 15)
     49version = Version(2, 2, 16)
    5050
    5151AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py

    r284128 r284892  
    2323import os
    2424import json
     25import time
    2526
    2627from webkitcorepy import mocks
     
    208209                if at and candidate.get('fromRef', {}).get('id') != at:
    209210                    continue
    210                 prs.append(candidate)
     211                prs.append({key: value for key, value in candidate.items() if key != 'activities'})
    211212
    212213            return mocks.Response.fromJson(dict(
     
    224225            json['toRef']['displayId'] = json['toRef']['id'].split('/')[-2:]
    225226            json['state'] = 'OPEN'
     227            json['activities'] = []
    226228            self.pull_requests.append(json)
    227229            return mocks.Response.fromJson(json)
     
    229231        # Update or access pull-request
    230232        if stripped_url.startswith(pr_base):
    231             number = int(stripped_url.split('/')[-1])
     233            split_url = stripped_url.split('/')
     234            number = int(split_url[9])
    232235            existing = None
    233236            for i in range(len(self.pull_requests)):
     
    238241            if method == 'PUT':
    239242                self.pull_requests[existing].update(json)
    240             return mocks.Response.fromJson(self.pull_requests[existing])
     243            if len(split_url) < 11:
     244                return mocks.Response.fromJson({key: value for key, value in self.pull_requests[existing].items() if key != 'activities'})
     245
     246            if method == 'GET' and split_url[-1] == 'activities':
     247                return mocks.Response.fromJson(dict(
     248                    size=len(self.pull_requests[existing].get('activities', [])),
     249                    isLastPage=True,
     250                    values=self.pull_requests[existing].get('activities', []),
     251                ))
     252            if method == 'POST' and split_url[-1] == 'comments':
     253                self.pull_requests[existing]['activities'].append(dict(comment=dict(
     254                    author=dict(displayName='Tim Committer', emailAddress='committer@webkit.org'),
     255                    createdDate=int(time.time() * 1000),
     256                    updatedDate=int(time.time() * 1000),
     257                    text=json.get('text', ''),
     258                )))
     259                return mocks.Response.fromJson({})
     260            if method == 'POST' and split_url[-1] == 'decline':
     261                self.pull_requests[existing]['open'] = False
     262                self.pull_requests[existing]['closed'] = True
     263                self.pull_requests[existing]['state'] = 'DECLINED'
     264                return mocks.Response.fromJson({})
     265            if method == 'POST' and split_url[-1] == 'reopen':
     266                self.pull_requests[existing]['open'] = True
     267                self.pull_requests[existing]['closed'] = False
     268                self.pull_requests[existing]['state'] = 'OPEN'
     269                return mocks.Response.fromJson({})
     270            return mocks.Response.create404(url)
    241271
    242272        return mocks.Response.create404(url)
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py

    r284128 r284892  
    2323import os
    2424import json
     25import time
    2526
    2627from webkitcorepy import mocks
     
    6061        self.tags = {}
    6162        self.pull_requests = []
     63        self.issues = dict()
    6264        self.users = dict()
    6365        self._environment = None
     
    293295
    294296    def request(self, method, url, data=None, params=None, auth=None, json=None, **kwargs):
     297        from datetime import datetime, timedelta
     298
    295299        if not url.startswith('http://') and not url.startswith('https://'):
    296300            return mocks.Response.create404(url)
     
    399403                    user=dict(login=self.remote.split('/')[-2]),
    400404                )
     405            if json.get('state'):
     406                pr['state'] = json.get('state')
    401407
    402408        # Create specifically
     
    404410            pr['number'] = 1 + max([0] + [pr.get('number', 0) for pr in self.pull_requests])
    405411            pr['user'] = dict(login=auth.username)
     412            pr['_links'] = dict(issue=dict(href='https://{}/issues/{}'.format(self.api_remote, pr['number'])))
    406413            self.pull_requests.append(pr)
    407414            return mocks.Response.fromJson(pr, url=url)
     
    423430            return self._users(url, stripped_url.split('/')[-1])
    424431
     432        # Access underlying issue
     433        if stripped_url.startswith('{}/issues/'.format(self.api_remote)):
     434            number = int(stripped_url.split('/')[5])
     435            issue = self.issues.get(number, dict(comments=[]))
     436            if method == 'GET' and stripped_url.split('/')[6] == 'comments':
     437                return mocks.Response.fromJson(issue['comments'], url=url)
     438            if method == 'POST' and stripped_url.split('/')[6] == 'comments':
     439                self.issues[number] = issue
     440                now = datetime.utcfromtimestamp(int(time.time()) - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ')
     441                self.issues[number]['comments'].append(dict(
     442                    user=dict(login=auth.username),
     443                    created_at=now, updated_at=now,
     444                    body=json.get('body', ''),
     445                ))
     446                return mocks.Response.fromJson(issue['comments'], url=url)
     447            return mocks.Response.create404(url)
     448
    425449        return mocks.Response.create404(url)
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/pull_request.py

    r284128 r284892  
    2222
    2323import re
     24import six
    2425
    2526from .commit import Commit
     27from datetime import datetime
     28from webkitscmpy import Contributor
    2629
    2730
    2831class PullRequest(object):
     32    class Exception(RuntimeError):
     33        pass
     34
     35    class Comment(object):
     36        def __init__(self, author, timestamp, content):
     37            if author and isinstance(author, dict) and author.get('name'):
     38                self.author = Contributor(author.get('name'), author.get('emails'))
     39            elif author and isinstance(author, six.string_types) and '@' in author:
     40                self.author = Contributor(author, [author])
     41            elif author and not isinstance(author, Contributor):
     42                raise TypeError("Expected 'author' to be of type {}, got '{}'".format(Contributor, author))
     43            else:
     44                self.author = author
     45
     46            if isinstance(timestamp, six.string_types) and timestamp.isdigit():
     47                timestamp = int(timestamp)
     48            if timestamp and not isinstance(timestamp, int):
     49                raise TypeError("Expected 'timestamp' to be of type int, got '{}'".format(timestamp))
     50            self.timestamp = timestamp
     51
     52            if content and not isinstance(content, six.string_types):
     53                raise ValueError("Expected 'content' to be a string, got '{}'".format(content))
     54            self.content = content
     55
     56        def __repr__(self):
     57            return '({} @ {}) {}'.format(
     58                self.author,
     59                datetime.utcfromtimestamp(self.timestamp) if self.timestamp else '-',
     60                self.content,
     61            )
     62
     63
    2964    COMMIT_BODY_RES = [
    3065        dict(
     
    100135        return body or None, commits
    101136
    102     def __init__(self, number, title=None, body=None, author=None, head=None, base=None, opened=None, generator=None):
     137    def __init__(
     138        self, number, title=None,
     139        body=None, author=None,
     140        head=None, base=None,
     141        opened=None, generator=None, metadata=None,
     142    ):
    103143        self.number = number
    104144        self.title = title
     
    111151        self._approvers = None
    112152        self._blockers = None
     153        self._metadata = metadata
     154        self._comments = None
    113155        self.generator = generator
    114156
     
    137179        return self._opened
    138180
     181    def open(self):
     182        if self.opened is True:
     183            return self
     184        if not self.generator:
     185            raise self.Exception('No associated pull-request generator')
     186        return self.generator.update(self, opened=True)
     187
     188    def close(self):
     189        if self.opened is False:
     190            return self
     191        if not self.generator:
     192            raise self.Exception('No associated pull-request generator')
     193        return self.generator.update(self, opened=False)
     194
     195    def comment(self, content):
     196        if not self.generator:
     197            raise self.Exception('No associated pull-request generator')
     198        self.generator.comment(self, content)
     199        self._comments = None
     200        return self
     201
     202    @property
     203    def comments(self):
     204        if self._comments is None and self.generator:
     205            self._comments = list(self.generator.comments(self))
     206        return self._comments
     207
    139208    def __repr__(self):
    140209        return 'PR {}{}'.format(self.number, ' | {}'.format(self.title) if self.title else '')
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py

    r284479 r284892  
    143143            return self.PullRequest(response.json())
    144144
    145         def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None):
     145        def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None):
    146146            if not isinstance(pull_request, PullRequest):
    147                 raise ValueError(
    148                     "Expected 'pull_request' to be of type '{}' not '{}'".format(PullRequest, type(pull_request)))
     147                raise ValueError("Expected 'pull_request' to be of type '{}' not '{}'".format(PullRequest, type(pull_request)))
     148
     149            pr_url = 'https://{domain}/rest/api/1.0/projects/{project}/repos/{name}/pull-requests/{id}'.format(
     150                domain=self.repository.domain,
     151                project=self.repository.project,
     152                name=self.repository.name,
     153                id=pull_request.number,
     154            )
     155
     156            if opened is not None:
     157                response = requests.get(pr_url)
     158                if response.status_code // 100 != 2:
     159                    return None
     160                response = requests.post(
     161                    '{}/{}'.format(pr_url, 'reopen' if opened else 'decline'),
     162                    json=dict(version=response.json().get('version', 0)),
     163                )
     164                if response.status_code // 100 != 2:
     165                    return None
     166
     167                pull_request._opened = opened
     168                if not any((head, title, body, commits, base)):
     169                    return pull_request
     170
    149171            if not any((head, title, body, commits, base)):
    150172                raise ValueError('No arguments to update pull-request provided')
     
    177199                )
    178200
    179             pr_url = 'https://{domain}/rest/api/1.0/projects/{project}/repos/{name}/pull-requests/{id}'.format(
    180                 domain=self.repository.domain,
    181                 project=self.repository.project,
    182                 name=self.repository.name,
    183                 id=pull_request.number,
    184             )
    185201            response = requests.get(pr_url)
    186202            if response.status_code // 100 != 2:
     
    213229            pull_request._approvers = got._approvers if got else []
    214230            return pull_request
     231
     232        def comment(self, pull_request, content):
     233            response = requests.post(
     234                'https://{domain}/rest/api/1.0/projects/{project}/repos/{name}/pull-requests/{id}/comments'.format(
     235                    domain=self.repository.domain,
     236                    project=self.repository.project,
     237                    name=self.repository.name,
     238                    id=pull_request.number,
     239                ), json=dict(text=content),
     240            )
     241            if response.status_code // 100 != 2:
     242                sys.stderr.write("Failed to add comment to '{}'\n".format(pull_request))
     243
     244        def comments(self, pull_request):
     245            for action in reversed(self.repository.request('pull-requests/{}/activities'.format(pull_request.number)) or []):
     246                comment = action.get('comment', {})
     247                user = comment.get('author', {})
     248                if not comment or not user or not comment.get('text'):
     249                    continue
     250
     251                yield PullRequest.Comment(
     252                    author=self.repository.contributors.create(user['displayName'], user['emailAddress']),
     253                    timestamp=comment.get('updatedDate', comment.get('createdDate')) // 1000,
     254                    content=comment.get('text'),
     255                )
     256
    215257
    216258    @classmethod
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py

    r284479 r284892  
    5555                ).get(data.get('state'), None),
    5656                generator=self,
     57                metadata=dict(
     58                    issue=data.get('_links', {}).get('issue', {}).get('href'),
     59                ),
    5760            )
    5861
     
    104107            return self.PullRequest(response.json())
    105108
    106         def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None):
     109        def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None):
    107110            if not isinstance(pull_request, PullRequest):
    108111                raise ValueError("Expected 'pull_request' to be of type '{}' not '{}'".format(PullRequest, type(pull_request)))
    109             if not any((head, title, body, commits, base)):
     112            if not any((head, title, body, commits, base)) and opened is None:
    110113                raise ValueError('No arguments to update pull-request provided')
    111114
     
    118121            if body or commits:
    119122                updates['body'] = PullRequest.create_body(body, commits)
     123            if opened is not None:
     124                updates['state'] = 'open' if opened else 'closed'
    120125            response = requests.post(
    121126                '{api_url}/repos/{owner}/{name}/pulls/{number}'.format(
     
    128133                json=updates,
    129134            )
     135            if response.status_code == 422:
     136                pull_request._opened = False
     137                return pull_request
    130138            if response.status_code // 100 != 2:
    131139                return None
     
    142150                open=True,
    143151                closed=False,
    144             ).get(data.get('state'), None),
     152            ).get(data.get('state'), None)
    145153            pull_request.generator = self
     154            pull_request._metadata = dict(
     155                issue=data.get('_links', {}).get('issue', {}).get('href'),
     156            )
    146157
    147158            return pull_request
     
    184195            pull_request._reviewers = sorted(pull_request._reviewers)
    185196            return pull_request
     197
     198        def comment(self, pull_request, content):
     199            issue = pull_request._metadata.get('issue')
     200            if not issue:
     201                old = pull_request
     202                pull_request = self.get(old.number)
     203                pull_request._reviewers = old._reviewers
     204                pull_request._approvers = old._approvers
     205                pull_request._blockers = old._blockers
     206                issue = pull_request._metadata.get('issue')
     207            if not issue:
     208                raise self.repository.Exception('Failed to find issue underlying pull-request')
     209            response = requests.post(
     210                '{}/comments'.format(issue),
     211                auth=HTTPBasicAuth(*self.repository.credentials(required=True)),
     212                headers=dict(Accept='application/vnd.github.v3+json'),
     213                json=dict(body=content),
     214            )
     215            if response.status_code // 100 != 2:
     216                sys.stderr.write("Failed to add comment to '{}'\n".format(pull_request))
     217
     218        def comments(self, pull_request):
     219            issue = pull_request._metadata.get('issue')
     220            if not issue:
     221                old = pull_request
     222                pull_request = self.get(old.number)
     223                pull_request._reviewers = old._reviewers
     224                pull_request._approvers = old._approvers
     225                pull_request._blockers = old._blockers
     226                issue = pull_request._metadata.get('issue')
     227            if not issue:
     228                raise self.repository.Exception('Failed to find issue underlying pull-request')
     229            response = requests.get(
     230                '{}/comments'.format(issue),
     231                auth=HTTPBasicAuth(*self.repository.credentials()),
     232                headers=dict(Accept='application/vnd.github.v3+json'),
     233            )
     234            for node in response.json() if response.status_code // 100 == 2 else []:
     235                user = node.get('user', {}).get('login')
     236                if not user:
     237                    continue
     238                tm = node.get('updated_at', node.get('created_at'))
     239                if tm:
     240                    tm = int(calendar.timegm(datetime.strptime(tm, '%Y-%m-%dT%H:%M:%SZ').timetuple()))
     241
     242                yield PullRequest.Comment(
     243                    author=self._contributor(user),
     244                    timestamp=tm,
     245                    content=node.get('body'),
     246                )
    186247
    187248
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py

    r284128 r284892  
    4040            raise NotImplementedError()
    4141
    42         def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None):
     42        def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None):
    4343            raise NotImplementedError()
    4444
    4545        def reviewers(self, pull_request):
     46            raise NotImplementedError()
     47
     48        def comment(self, pull_request, content):
     49            raise NotImplementedError()
     50
     51        def comments(self, pull_request):
    4652            raise NotImplementedError()
    4753
  • trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py

    r284479 r284892  
    461461                dict(user=dict(login='ereviewer'), state='APPROVED'),
    462462                dict(user=dict(login='sreviewer'), state='CHANGES_REQUESTED'),
    463             ],
     463            ], _links=dict(
     464                issue=dict(href='https://{}/issues/1'.format(result.api_remote)),
     465            ),
    464466        )]
    465467        return result
     
    510512            self.assertEqual(pr.blockers, [Contributor('Suspicious Reviewer', ['sreviewer@webkit.org'])])
    511513
     514    def test_comments(self):
     515        with self.webserver():
     516            repo = remote.GitHub(self.remote)
     517            pr = repo.pull_requests.get(1)
     518            self.assertEqual(pr.comments, [])
     519            pr.comment('Commenting!')
     520            self.assertEqual([c.content for c in pr.comments], ['Commenting!'])
     521
     522    def test_open_close(self):
     523        with self.webserver():
     524            repo = remote.GitHub(self.remote)
     525            pr = repo.pull_requests.get(1)
     526            self.assertTrue(pr.opened)
     527            pr.close()
     528            self.assertFalse(pr.opened)
     529            pr.open()
     530            self.assertTrue(pr.opened)
     531
    512532
    513533class TestNetworkPullRequestBitBucket(unittest.TestCase):
     
    522542            open=True,
    523543            closed=False,
     544            activities=[],
    524545            title='Example Change',
    525546            author=dict(
     
    607628            self.assertEqual(pr.approvers, [])
    608629            self.assertEqual(pr.blockers, [Contributor('Suspicious Reviewer', ['sreviewer@webkit.org'])])
     630
     631    def test_comments(self):
     632        with self.webserver():
     633            repo = remote.BitBucket(self.remote)
     634            pr = repo.pull_requests.get(1)
     635            self.assertEqual(pr.comments, [])
     636            pr.comment('Commenting!')
     637            self.assertEqual([c.content for c in pr.comments], ['Commenting!'])
     638
     639    def test_open_close(self):
     640        with self.webserver():
     641            repo = remote.BitBucket(self.remote)
     642            pr = repo.pull_requests.get(1)
     643            self.assertTrue(pr.opened)
     644            pr.close()
     645            self.assertFalse(pr.opened)
     646            pr.open()
     647            self.assertTrue(pr.opened)
Note: See TracChangeset for help on using the changeset viewer.