WebKit Bugzilla
New
Browse
Search+
Log In
×
Sign in with GitHub
or
Remember my login
Create Account
·
Forgot Password
Forgotten password account recovery
[patch]
Patch
bug-229089-20210824082056.patch (text/plain), 47.73 KB, created by
Jonathan Bedard
on 2021-08-24 08:20:58 PDT
(
hide
)
Description:
Patch
Filename:
MIME Type:
Creator:
Jonathan Bedard
Created:
2021-08-24 08:20:58 PDT
Size:
47.73 KB
patch
obsolete
>Subversion Revision: 281493 >diff --git a/Tools/ChangeLog b/Tools/ChangeLog >index e6f9e75a9a03597b293c3aa49383dbefe89cdfdc..909180b2538b2ee8596e1e6b017a5ffa1306dbb0 100644 >--- a/Tools/ChangeLog >+++ b/Tools/ChangeLog >@@ -1,3 +1,58 @@ >+2021-08-24 Jonathan Bedard <jbedard@apple.com> >+ >+ [git-webkit] Add pull-request command (Part 6) >+ https://bugs.webkit.org/show_bug.cgi?id=229089 >+ <rdar://problem/81908751> >+ >+ Reviewed by NOBODY (OOPS!). >+ >+ * Scripts/libraries/webkitscmpy/setup.py: Bump version. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py: >+ (Git.__init__): Add commit, add and push commands. >+ (Git.commit): Create new commit from staged files. >+ (Git.add): Stage modified files. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py: >+ (BitBucket.__init__): Add pull_requests. >+ (BitBucket.request): Add ability to list and edit pull requets. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py: >+ (GitHub.__init__): Add pull_requests. >+ (GitHub.request): Add ability to list and edit pull requets. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py: Add PullRequest. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py: >+ (PullRequest.parser): Add '--add' and '--no-add' to allow user to specify how modified files >+ are incorperated into the pull-request. >+ (PullRequest.create_commit): Based on currently modified files, either create a new commit or >+ add those files to an existing commit. >+ (PullRequest.branch_point): Determine when this branch diverged from a production branch. >+ (PullRequest.main): Create branch, create commit on branch, push branch and either create or >+ update a pull-request. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py: >+ (BitBucket.PRGenerator.find): >+ (BitBucket.PRGenerator.create): >+ (BitBucket.PRGenerator.update): >+ (BitBucket.__init__): Add pull_request generator. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py: >+ (GitHub.PRGenerator.find): >+ (GitHub.PRGenerator.create): >+ (GitHub.PRGenerator.update): >+ (GitHub.__init__): Add pull_request generator. >+ * Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py: >+ (Scm.PRGenerator.__init__): >+ (Scm.PRGenerator.find): >+ (Scm.PRGenerator.create): >+ (Scm.PRGenerator.update): >+ * Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py: >+ (TestDoPullRequest.setUp): >+ (TestDoPullRequest.test_svn): >+ (TestDoPullRequest.test_no_modified): >+ (TestDoPullRequest.test_staged): >+ (TestDoPullRequest.test_modified): >+ (TestDoPullRequest.test_github): >+ (TestDoPullRequest.test_github_update): >+ (TestDoPullRequest.test_stash): >+ (TestDoPullRequest.test_stash_update): >+ > 2021-08-23 John Wilander <wilander@apple.com> > > PCM: Support ephemeral measurement with non-persistent WebCore::PrivateClickMeasurement >diff --git a/Tools/Scripts/libraries/webkitscmpy/setup.py b/Tools/Scripts/libraries/webkitscmpy/setup.py >index 7f96391ae37946fbf4e148a146d977e730179e8b..71bcae2f70c707040fd4dbd4269e5bbe12faebf0 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/setup.py >+++ b/Tools/Scripts/libraries/webkitscmpy/setup.py >@@ -29,7 +29,7 @@ def readme(): > > setup( > name='webkitscmpy', >- version='1.1.9', >+ version='2.0.0', > description='Library designed to interact with git and svn repositories.', > long_description=readme(), > classifiers=[ >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py >index 3c7439614d257ea9a1eb3a09415a2015d1bba5fe..97c10b31ccfea92ca717a5c69eb9b3b64970f8c7 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py >@@ -46,7 +46,7 @@ except ImportError: > "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`" > ) > >-version = Version(1, 1, 9) >+version = Version(2, 0, 0) > > AutoInstall.register(Package('fasteners', Version(0, 15, 0))) > AutoInstall.register(Package('monotonic', Version(1, 5))) >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py >index a11985f4e1a3a56232f4f24d06bc1a76c4698eaf..26f56839cd3780e70e32e7b444bf3c405992dd0b 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py >@@ -20,6 +20,7 @@ > # 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 hashlib > import json > import os > import re >@@ -28,7 +29,7 @@ import time > from datetime import datetime > from mock import patch > >-from webkitcorepy import decorators, mocks, OutputCapture, StringIO >+from webkitcorepy import decorators, mocks, string_utils, OutputCapture, StringIO > from webkitscmpy import local, Commit, Contributor > from webkitscmpy.program.canonicalize.committer import main as committer_main > from webkitscmpy.program.canonicalize.message import main as message_main >@@ -411,6 +412,22 @@ nothing to commit, working tree clean > self.executable, 'check-ref-format', re.compile(r'.+'), > generator=lambda *args, **kwargs: > mocks.ProcessCompletion(returncode=0) if re.match(r'^[A-Za-z0-9-]+/[A-Za-z0-9/-]+$', args[2]) else mocks.ProcessCompletion(), >+ ), mocks.Subprocess.Route( >+ self.executable, 'commit', >+ cwd=self.path, >+ generator=lambda *args, **kwargs: self.commit(amend=False), >+ ), mocks.Subprocess.Route( >+ self.executable, 'commit', '--amend', >+ cwd=self.path, >+ generator=lambda *args, **kwargs: self.commit(amend=True), >+ ), mocks.Subprocess.Route( >+ self.executable, 'add', re.compile(r'.+'), >+ cwd=self.path, >+ generator=lambda *args, **kwargs: self.add(args[2]), >+ ), mocks.Subprocess.Route( >+ self.executable, 'push', '-f', >+ cwd=self.path, >+ generator=lambda *args, **kwargs: mocks.ProcessCompletion(returncode=0), > ), mocks.Subprocess.Route( > self.executable, > cwd=self.path, >@@ -519,6 +536,9 @@ nothing to commit, working tree clean > self.commits[something][-1] = Commit.from_json(Commit.Encoder().default(self.head)) > self.head = self.commits[something][-1] > self.head.branch = something >+ if not self.head.branch_point: >+ self.head.branch_point = self.head.identifier >+ self.head.identifier = 0 > return True > > if commit: >@@ -709,3 +729,35 @@ nothing to commit, working tree clean > configfile.write('\t{}={}\n'.format(key_b, value)) > > return mocks.ProcessCompletion(returncode=0) >+ >+ def commit(self, amend=False): >+ if not self.head: >+ return mocks.ProcessCompletion(returncode=1, stdout='Allowed in git, but disallowed by reasonable workflows') >+ if not self.staged and not amend: >+ return mocks.ProcessCompletion(returncode=1, stdout='no changes added to commit (use "git add" and/or "git commit -a")\n') >+ >+ if not amend: >+ self.head = Commit( >+ branch=self.branch, repository_id=self.head.repository_id, >+ timestamp=int(time.time()), >+ identifier=self.head.identifier + 1 if self.head.branch_point else 1, >+ branch_point=self.head.branch_point or self.head.identifier, >+ ) >+ self.commits[self.branch].append(self.head) >+ >+ self.head.author = Contributor(self.config()['user.name'], [self.config()['user.email']]) >+ self.head.message = '{} commit\nReviewed by Jonathan Bedard\n\n * {}\n'.format( >+ 'Amended' if amend else 'Created', >+ '\n * '.join(self.staged.keys()), >+ ) >+ self.head.hash = hashlib.sha256(string_utils.encode(self.head.message)).hexdigest()[:40] >+ self.staged = {} >+ return mocks.ProcessCompletion(returncode=0) >+ >+ def add(self, file): >+ if file not in self.modified: >+ return mocks.ProcessCompletion(returncode=128, stdout="fatal: pathspec '{}' did not match any files\n".format(file)) >+ for key, value in self.modified.items(): >+ self.staged[key] = value >+ self.modified = {} >+ return mocks.ProcessCompletion(returncode=0) >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py >index a1d17071d07bf6d068ea886ffce149d14a2f0547..38637c5fac3e6f3006de7d09f202a2719b692379 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py >@@ -53,6 +53,7 @@ class BitBucket(mocks.Requests): > > self.head = self.commits[self.default_branch][-1] > self.tags = {} >+ self.pull_requests = [] > > def commit(self, ref): > if ref in self.commits: >@@ -157,7 +158,7 @@ class BitBucket(mocks.Requests): > ], > ), url=url) > >- def request(self, method, url, data=None, params=None, **kwargs): >+ def request(self, method, url, data=None, params=None, json=None, **kwargs): > if not url.startswith('http://') and not url.startswith('https://'): > return mocks.Response.create404(url) > >@@ -194,4 +195,47 @@ class BitBucket(mocks.Requests): > 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 {}) > >+ # All pull-requests >+ pr_base = '{}/rest/api/1.0/{}/pull-requests'.format(self.hosts[0], self.project) >+ if method == 'GET' and stripped_url == pr_base: >+ prs = [] >+ for candidate in self.pull_requests: >+ states = params.get('state', []) >+ states = states if isinstance(states, list) else [states] >+ if states and candidate.get('state') not in states: >+ continue >+ at = params.get('at') >+ if at and candidate.get('fromRef', {}).get('id') != at: >+ continue >+ prs.append(candidate) >+ >+ return mocks.Response.fromJson(dict( >+ size=len(prs), >+ isLastPage=True, >+ values=prs, >+ )) >+ >+ # Create pull-request >+ if method == 'POST' and stripped_url == pr_base: >+ json['author'] = dict(user=dict(displayName='Tim Committer', emailAddress='committer@webkit.org')) >+ json['participants'] = [json['author']] >+ json['id'] = 1 + max([0] + [pr.get('id', 0) for pr in self.pull_requests]) >+ json['fromRef']['displayId'] = json['fromRef']['id'].split('/')[-2:] >+ json['toRef']['displayId'] = json['toRef']['id'].split('/')[-2:] >+ self.pull_requests.append(json) >+ return mocks.Response.fromJson(json) >+ >+ # Update or access pull-request >+ if stripped_url.startswith(pr_base): >+ number = int(stripped_url.split('/')[-1]) >+ existing = None >+ for i in range(len(self.pull_requests)): >+ if self.pull_requests[i].get('id') == number: >+ existing = i >+ if existing is None: >+ return mocks.Response.create404(url) >+ if method == 'PUT': >+ self.pull_requests[existing].update(json) >+ return mocks.Response.fromJson(self.pull_requests[existing]) >+ > return mocks.Response.create404(url) >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py >index 6c6804d9040241da3e6a1def8389f6ed7e32e425..a551452263d29dae8601875d0c89a90960cea2bc 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py >@@ -58,6 +58,7 @@ class GitHub(mocks.Requests): > > self.head = self.commits[self.default_branch][-1] > self.tags = {} >+ self.pull_requests = [] > self._environment = None > > def __enter__(self): >@@ -280,7 +281,7 @@ class GitHub(mocks.Requests): > ), url=url > ) > >- def request(self, method, url, data=None, params=None, **kwargs): >+ def request(self, method, url, data=None, params=None, auth=None, json=None, **kwargs): > if not url.startswith('http://') and not url.startswith('https://'): > return mocks.Response.create404(url) > >@@ -331,9 +332,65 @@ class GitHub(mocks.Requests): > > # Add fork > if stripped_url.startswith('{}/forks'.format(self.api_remote)) and method == 'POST': >- username = kwargs.get('json', {}).get('owner', None) >+ username = (json or {}).get('owner', None) > if username: > self.forks.append(username) > return mocks.Response.fromJson({}) if username else mocks.Response.create404(url) > >+ # All pull-requests >+ pr_base = '{}/pulls'.format(self.api_remote) >+ if method == 'GET' and stripped_url == pr_base: >+ prs = [] >+ for candidate in self.pull_requests: >+ state = params.get('state', 'all') >+ if state != 'all' and candidate.get('state', 'closed') != state: >+ continue >+ base = params.get('base') >+ if base and candidate.get('base', {}).get('ref') != base: >+ continue >+ head = params.get('head') >+ if head and head not in [candidate.get('head', {}).get('ref'), candidate.get('head', {}).get('label')]: >+ continue >+ prs.append(candidate) >+ return mocks.Response.fromJson(prs) >+ >+ # Create/update pull-request >+ pr = dict() >+ if method == 'POST' and auth and stripped_url.startswith(pr_base): >+ if json.get('title'): >+ pr['title'] = json['title'] >+ if json.get('body'): >+ pr['body'] = json['body'] >+ if json.get('head'): >+ pr['head'] = dict( >+ label=json['head'], >+ ref=json['head'].split(':')[-1], >+ user=dict(login=auth.username), >+ ) >+ if json.get('base'): >+ pr['base'] = dict( >+ label='{}:{}'.format(self.remote.split('/')[-2], json['base']), >+ ref=json['base'], >+ user=dict(login=self.remote.split('/')[-2]), >+ ) >+ >+ # Create specifically >+ if method == 'POST' and auth and stripped_url == pr_base: >+ pr['number'] = 1 + max([0] + [pr.get('number', 0) for pr in self.pull_requests]) >+ pr['user'] = dict(login=auth.username) >+ self.pull_requests.append(pr) >+ return mocks.Response.fromJson(pr) >+ >+ # Update specifically >+ if method == 'POST' and auth and stripped_url.startswith(pr_base): >+ number = int(stripped_url.split('/')[-1]) >+ existing = None >+ for i in range(len(self.pull_requests)): >+ if self.pull_requests[i].get('number') == number: >+ existing = i >+ if existing is None: >+ return mocks.Response.create404(url) >+ self.pull_requests[existing].update(pr) >+ return mocks.Response.fromJson(self.pull_requests[i]) >+ > return mocks.Response.create404(url) >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py >index c938bd96902eaf67554595f48752b6f31e4542a9..2a9a9600af63ff159fe4bbc140aa74a79ee6672d 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py >@@ -34,6 +34,7 @@ from .checkout import Checkout > from .find import Find, Info > from .log import Log > from .pull import Pull >+from .pull_request import PullRequest > from .setup_git_svn import SetupGitSvn > from .setup import Setup > >@@ -65,8 +66,7 @@ def main(args=None, path=None, loggers=None, contributors=None, identifier_templ > ) > > subparsers = parser.add_subparsers(help='sub-command help') >- >- programs = [Branch, Blame, Canonicalize, Checkout, Clean, Find, Info, Log, Pull, Setup] >+ programs = [Blame, Branch, Canonicalize, Checkout, Clean, Find, Info, Log, Pull, PullRequest, Setup] > if subversion: > programs.append(SetupGitSvn) > >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py >new file mode 100644 >index 0000000000000000000000000000000000000000..67475bf909e8f502f8ad628618496f98a6928094 >--- /dev/null >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py >@@ -0,0 +1,163 @@ >+# 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 sys >+ >+from .command import Command >+from .branch import Branch >+ >+from webkitcorepy import arguments, run, string_utils >+from webkitscmpy import local, log, remote >+ >+ >+class PullRequest(Command): >+ name = 'pull-request' >+ aliases = ['pr', 'pfr', 'upload'] >+ help = 'Push the current checkout state as a pull-request' >+ >+ @classmethod >+ def parser(cls, parser, loggers=None): >+ Branch.parser(parser, loggers=loggers) >+ parser.add_argument( >+ '--no-add', '--add', >+ dest='will_add', default=None, >+ help='When drafting a change, add (or never add) modified files to set of staged changes to be committed', >+ action=arguments.NoAction, >+ ) >+ >+ @classmethod >+ def create_commit(cls, args, repository, **kwargs): >+ # First, find the set of files to be modified >+ modified = [] if args.will_add is False else repository.modified() >+ if args.will_add: >+ modified = list(set(modified).union(set(repository.modified(staged=False)))) >+ >+ # Next, add all modified file >+ for file in set(modified) - set(repository.modified(staged=True)): >+ log.warning(' Adding {}...'.format(file)) >+ if run([repository.executable(), 'add', file], cwd=repository.root_path).returncode: >+ sys.stderr.write("Failed to add '{}'\n".format(file)) >+ return 1 >+ >+ # Then, see if we already have a commit associated with this branch we need to modify >+ has_commit = repository.commit(include_log=False, include_identifier=False).branch == repository.branch and repository.branch != repository.default_branch >+ if not modified and has_commit: >+ log.warning('Using committed changes...') >+ return 0 >+ >+ # Otherwise, we need to create a commit >+ if not modified: >+ sys.stderr.write('No modified files\n') >+ return 1 >+ log.warning('Amending commit...' if has_commit else 'Creating commit...') >+ if run([repository.executable(), 'commit'] + (['--amend'] if has_commit else []), cwd=repository.root_path).returncode: >+ sys.stderr.write('Failed to generate commit\n') >+ return 1 >+ >+ return 0 >+ >+ @classmethod >+ def branch_point(cls, args, repository, **kwargs): >+ cnt = 0 >+ commit = None >+ while not commit or commit.branch.startswith(Branch.PREFIX): >+ cnt += 1 >+ commit = repository.find(argument='HEAD~{}'.format(cnt), include_log=False, include_identifier=False) >+ log.warning(' Found {}...'.format(string_utils.pluralize(cnt, 'commit'))) >+ >+ return commit >+ >+ @classmethod >+ def main(cls, args, repository, **kwargs): >+ if not isinstance(repository, local.Git): >+ sys.stderr.write("Can only '{}' on a native Git repository\n".format(cls.name)) >+ return 1 >+ >+ if not repository.branch.startswith(Branch.PREFIX): >+ if Branch.main(args, repository, **kwargs): >+ sys.stderr.write("Abandoning pushing pull-request because '{}' could not be created\n".format(args.issue)) >+ return 1 >+ elif args.issue and repository.branch != args.issue: >+ sys.stderr.write("Creating a pull-request for '{}' but we're on '{}'\n".format(args.issue, repository.branch)) >+ return 1 >+ >+ result = cls.create_commit(args, repository, **kwargs) >+ if result: >+ return result >+ >+ branch_point = cls.branch_point(args, repository, **kwargs) >+ >+ rmt = repository.remote() >+ if not rmt: >+ sys.stderr.write("'{}' doesn't have a recognized remote\n".format(repository.root_path)) >+ return 1 >+ target = 'fork' if isinstance(rmt, remote.GitHub) else 'origin' >+ log.warning("Pushing '{}' to '{}'...".format(repository.branch, target)) >+ if run([repository.executable(), 'push', '-f', target, repository.branch], cwd=repository.root_path).returncode: >+ sys.stderr.write("Failed to push '{}' to '{}'\n".format(repository.branch, target)) >+ return 1 >+ >+ if not rmt.pull_requests: >+ sys.stderr.write("'{}' cannot generate pull-requests\n".format(rmt.url)) >+ return 1 >+ user, _ = rmt.credentials(required=True) if isinstance(rmt, remote.GitHub) else (repository.config()['user.email'], None) >+ candidates = list(rmt.pull_requests.find(head=repository.branch)) >+ commits = list(repository.commits(begin=dict(hash=branch_point.hash), end=dict(branch=repository.branch))) >+ >+ title = commits[0].message.splitlines()[0] >+ for commit in commits[1:]: >+ title_candidate = commit.message.splitlines()[0] >+ while title and not title_candidate.startswith(title): >+ title = title[:-1] >+ if not title: >+ title = commits[0].message.splitlines()[0] >+ title = title.rstrip().lstrip() >+ if title.endswith('(Part'): >+ title = title[:-5].rstrip() >+ >+ if candidates: >+ log.warning("Updating pull-request for '{}'...".format(repository.branch)) >+ pr = rmt.pull_requests.update( >+ pull_request=candidates[0], >+ title=title, >+ commits=commits, >+ base=branch_point.branch, >+ head=repository.branch, >+ ) >+ if not pr: >+ sys.stderr.write("Failed to update pull-request '{}'\n".format(candidates[0])) >+ return 1 >+ log.warning("Updated '{}'!".format(pr)) >+ else: >+ log.warning("Creating pull-request for '{}'...".format(repository.branch)) >+ pr = rmt.pull_requests.create( >+ title=title, >+ commits=commits, >+ base=branch_point.branch, >+ head=repository.branch, >+ ) >+ if not pr: >+ sys.stderr.write("Failed to create pull-request for '{}'\n".format(repository.branch)) >+ return 1 >+ log.warning("Created '{}'!".format(pr)) >+ >+ return 0 >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py >index ea2ee7d25e39d518c354e73453fe1c80558a66b5..b6fe11dc57e88f5460d7e94f38c244cea75f73cc 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py >@@ -25,14 +25,160 @@ import requests > import six > import sys > >+import json >+ > from webkitcorepy import decorators >-from webkitscmpy import Commit >+from webkitscmpy import Commit, PullRequest > 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') > >+ class PRGenerator(Scm.PRGenerator): >+ TITLE_CHAR_LIMIT = 254 >+ BODY_CHAR_LIMIT = 32766 >+ >+ def find(self, state=None, head=None, base=None): >+ params = dict( >+ limit=100, >+ withProperties='false', >+ withAttributes='false', >+ ) >+ if state == PullRequest.State.OPENED: >+ params['state'] = 'OPEN' >+ if state == PullRequest.State.CLOSED: >+ params['state'] = ['DECLINED', 'MERGED', 'SUPERSEDED'] >+ if head: >+ params['direction'] = 'OUTGOING' >+ params['at'] = 'refs/heads/{}'.format(head) >+ data = self.repository.request('pull-requests', params=params) >+ for datum in data or []: >+ if base and not datum['toRef']['id'].endswith(base): >+ continue >+ yield PullRequest( >+ number=datum['id'], >+ title=datum.get('title'), >+ body=datum.get('description'), >+ author=self.repository.contributors.create( >+ datum['author']['user']['displayName'], >+ datum['author']['user']['emailAddress'], >+ ), head=datum['fromRef']['displayId'], >+ base=datum['toRef']['displayId'], >+ ) >+ >+ def create(self, head, title, body=None, commits=None, base=None): >+ for key, value in dict(head=head, title=title).items(): >+ if not value: >+ raise ValueError("Must define '{}' when creating pull-request".format(key)) >+ >+ if len(title) > self.TITLE_CHAR_LIMIT: >+ raise ValueError('Title length too long. Limit is: {}'.format(self.TITLE_CHAR_LIMIT)) >+ description = PullRequest.create_body(body, commits) >+ if description and len(description) > self.BODY_CHAR_LIMIT: >+ raise ValueError('Body length too long. Limit is: {}'.format(self.BODY_CHAR_LIMIT)) >+ response = requests.post( >+ 'https://{domain}/rest/api/1.0/projects/{project}/repos/{name}/pull-requests'.format( >+ domain=self.repository.domain, >+ project=self.repository.project, >+ name=self.repository.name, >+ ), json=dict( >+ title=title, >+ description=PullRequest.create_body(body, commits), >+ fromRef=dict( >+ id='refs/heads/{}'.format(head), >+ repository=dict( >+ slug=self.repository.name, >+ project=dict(key=self.repository.project), >+ ), >+ ), toRef=dict( >+ id='refs/heads/{}'.format(base or self.repository.default_branch), >+ repository=dict( >+ slug=self.repository.name, >+ project=dict(key=self.repository.project), >+ ), >+ ), >+ ), >+ ) >+ if response.status_code // 100 != 2: >+ return None >+ data = response.json() >+ return PullRequest( >+ number=data['id'], >+ title=data.get('title'), >+ body=data.get('description'), >+ author=self.repository.contributors.create( >+ data['author']['user']['displayName'], >+ data['author']['user']['emailAddress'], >+ ), head=data['fromRef']['displayId'], >+ base=data['toRef']['displayId'], >+ ) >+ >+ def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None): >+ if not isinstance(pull_request, PullRequest): >+ raise ValueError( >+ "Expected 'pull_request' to be of type '{}' not '{}'".format(PullRequest, type(pull_request))) >+ if not any((head, title, body, commits, base)): >+ raise ValueError('No arguments to update pull-request provided') >+ >+ to_change = dict() >+ if title: >+ to_change['title'] = title >+ if body or commits: >+ to_change['description'] = PullRequest.create_body(body, commits) >+ if head: >+ to_change['fromRef'] = dict( >+ id='refs/heads/{}'.format(head), >+ repository=dict( >+ slug=self.repository.name, >+ project=dict(key=self.repository.project), >+ ), >+ ) >+ if commits: >+ if to_change.get('fromRef'): >+ to_change['fromRef']['latestCommit'] = commits[0].hash >+ else: >+ to_change['fromRef'] = dict(latestCommit=commits[0].hash) >+ if base: >+ to_change['toRef'] = dict( >+ id='refs/heads/{}'.format(base or self.repository.default_branch), >+ repository=dict( >+ slug=self.repository.name, >+ project=dict(key=self.repository.project), >+ ), >+ ) >+ >+ pr_url = 'https://{domain}/rest/api/1.0/projects/{project}/repos/{name}/pull-requests/{id}'.format( >+ domain=self.repository.domain, >+ project=self.repository.project, >+ name=self.repository.name, >+ id=pull_request.number, >+ ) >+ response = requests.get(pr_url) >+ if response.status_code // 100 != 2: >+ return None >+ data = response.json() >+ del data['author'] >+ del data['participants'] >+ data.update(to_change) >+ >+ response = requests.put(pr_url, json=data) >+ if response.status_code // 100 != 2: >+ return None >+ data = response.json() >+ >+ pull_request.title = data.get('title', pull_request.title) >+ if data.get('description'): >+ pull_request.body, pull_request.commits = pull_request.parse_body(data.get('description')) >+ user = data.get('author', {}).get('user', {}) >+ if user.get('displayName') and user.get('emailAddress'): >+ pull_request.author = self.repository.contributors.create(user['displayName'], user['emailAddress']) >+ pull_request.head = data.get('fromRef', {}).get('displayId', pull_request.base) >+ pull_request.base = data.get('toRef', {}).get('displayId', pull_request.base) >+ >+ return pull_request >+ >+ > @classmethod > def is_webserver(cls, url): > return True if cls.URL_RE.match(url) else False >@@ -52,6 +198,8 @@ class BitBucket(Scm): > id=id or self.name.lower(), > ) > >+ self.pull_requests = self.PRGenerator(self) >+ > @property > def is_git(self): > return True >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py >index b0529f2cb1580ab9a55bbe0aa0fc801e6bbf1578..64edfc2d76d37f131fbb48ad2b129f2e57d8209a 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py >@@ -29,7 +29,7 @@ import sys > from datetime import datetime > from requests.auth import HTTPBasicAuth > from webkitcorepy import credentials, decorators >-from webkitscmpy import Commit >+from webkitscmpy import Commit, PullRequest > from webkitscmpy.remote.scm import Scm > from xml.dom import minidom > >@@ -38,6 +38,101 @@ class GitHub(Scm): > URL_RE = re.compile(r'\Ahttps?://github.(?P<domain>\S+)/(?P<owner>\S+)/(?P<repository>\S+)\Z') > EMAIL_RE = re.compile(r'(?P<email>[^@]+@[^@]+)(@.*)?') > >+ class PRGenerator(Scm.PRGenerator): >+ def find(self, state=None, head=None, base=None): >+ if not state: >+ state = 'all' >+ user, _ = self.repository.credentials() >+ data = self.repository.request('pulls', params=dict( >+ state=state, >+ base=base, >+ head='{}:{}'.format(user, head) if user and head else head, >+ )) >+ for datum in data or []: >+ if base and datum['base']['ref'] != base: >+ continue >+ if head and not datum['head']['ref'].endswith(head): >+ continue >+ yield PullRequest( >+ number=datum['number'], >+ title=datum.get('title'), >+ body=datum.get('body'), >+ author=self.repository.contributors.create(datum['user']['login']), >+ head=datum['head']['ref'], >+ base=datum['base']['ref'], >+ ) >+ >+ def create(self, head, title, body=None, commits=None, base=None): >+ for key, value in dict(head=head, title=title).items(): >+ if not value: >+ raise ValueError("Must define '{}' when creating pull-request".format(key)) >+ >+ user, _ = self.repository.credentials(required=True) >+ response = requests.post( >+ '{api_url}/repos/{owner}/{name}/pulls'.format( >+ api_url=self.repository.api_url, >+ owner=self.repository.owner, >+ name=self.repository.name, >+ ), auth=HTTPBasicAuth(*self.repository.credentials(required=True)), >+ headers=dict(Accept='application/vnd.github.v3+json'), >+ json=dict( >+ title=title, >+ body=PullRequest.create_body(body, commits), >+ base=base or self.repository.default_branch, >+ head='{}:{}'.format(user, head), >+ ), >+ ) >+ if response.status_code // 100 != 2: >+ return None >+ data = response.json() >+ return PullRequest( >+ number=data['number'], >+ title=data.get('title'), >+ body=data.get('body'), >+ author=self.repository.contributors.create(data['user']['login']), >+ head=data['head']['ref'], >+ base=data['base']['ref'], >+ ) >+ >+ def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None): >+ if not isinstance(pull_request, PullRequest): >+ raise ValueError("Expected 'pull_request' to be of type '{}' not '{}'".format(PullRequest, type(pull_request))) >+ if not any((head, title, body, commits, base)): >+ raise ValueError('No arguments to update pull-request provided') >+ >+ user, _ = self.repository.credentials(required=True) >+ updates = dict( >+ title=title or pull_request.title, >+ base=base or pull_request.base, >+ head='{}:{}'.format(user, head) if head else pull_request.head, >+ ) >+ if body or commits: >+ updates['body'] = PullRequest.create_body(body, commits) >+ response = requests.post( >+ '{api_url}/repos/{owner}/{name}/pulls/{number}'.format( >+ api_url=self.repository.api_url, >+ owner=self.repository.owner, >+ name=self.repository.name, >+ number=pull_request.number, >+ ), auth=HTTPBasicAuth(*self.repository.credentials(required=True)), >+ headers=dict(Accept='application/vnd.github.v3+json'), >+ json=updates, >+ ) >+ if response.status_code // 100 != 2: >+ return None >+ data = response.json() >+ >+ pull_request.title = data.get('title', pull_request.title) >+ if data.get('body'): >+ pull_request.body, pull_request.commits = pull_request.parse_body(data.get('body')) >+ if data.get('user', {}).get('login'): >+ pull_request.author = self.repository.contributors.create(data['user']['login']) >+ pull_request.head = data.get('head', {}).get('displayId', pull_request.base) >+ pull_request.base = data.get('base', {}).get('displayId', pull_request.base) >+ >+ return pull_request >+ >+ > @classmethod > def is_webserver(cls, url): > return True if cls.URL_RE.match(url) else False >@@ -62,6 +157,8 @@ class GitHub(Scm): > id=id or self.name.lower(), > ) > >+ self.pull_requests = self.PRGenerator(self) >+ > def credentials(self, required=True): > username, token = credentials( > url=self.api_url, >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py >index 9f83f52ad9eafda6c37b9d60c1c9b3e0979d4d83..fbebfe88c9ae8c19b79782ab5b0a1b0df6973293 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py >@@ -26,6 +26,20 @@ from webkitscmpy.scm_base import ScmBase > > > class Scm(ScmBase): >+ class PRGenerator(object): >+ def __init__(self, repository): >+ self.repository = repository >+ >+ def find(self, state=None, head=None, base=None): >+ raise NotImplementedError() >+ >+ def create(self, head, title, body=None, commits=None, base=None): >+ raise NotImplementedError() >+ >+ def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None): >+ raise NotImplementedError() >+ >+ > @classmethod > def from_url(cls, url, contributors=None): > from webkitscmpy import remote >@@ -42,3 +56,4 @@ class Scm(ScmBase): > if not isinstance(url, six.string_types): > raise ValueError("Expected 'url' to be a string type, not '{}'".format(type(url))) > self.url = url >+ self.pull_requests = None >diff --git a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py >index 2fdbe745994d4f834dd8e0da9d9fd89d7530fae0..3b06b1ce519eab17648f7a6dd5d9ac5b607ca2d0 100644 >--- a/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py >+++ b/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py >@@ -20,9 +20,12 @@ > # (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 sys > import unittest > >-from webkitscmpy import Commit, PullRequest >+from webkitcorepy import OutputCapture, testing >+from webkitscmpy import Commit, PullRequest, program, mocks > > > class TestPullRequest(unittest.TestCase): >@@ -150,3 +153,169 @@ Reviewed by Tim Contributor. > self.assertEqual(len(commits), 1) > self.assertEqual(commits[0].hash, '11aa76f9fc380e9fe06157154f32b304e8dc4749') > self.assertEqual(commits[0].message, '[scoping] Bug to fix\n\nReviewed by Tim Contributor.') >+ >+ >+class TestDoPullRequest(testing.PathTestCase): >+ basepath = 'mock/repository' >+ >+ def setUp(self): >+ super(TestDoPullRequest, self).setUp() >+ os.mkdir(os.path.join(self.path, '.git')) >+ os.mkdir(os.path.join(self.path, '.svn')) >+ >+ def test_svn(self): >+ with OutputCapture() as captured, mocks.local.Git(), mocks.local.Svn(self.path): >+ self.assertEqual(1, program.main( >+ args=('pull-request',), >+ path=self.path, >+ )) >+ self.assertEqual(captured.root.log.getvalue(), '') >+ self.assertEqual(captured.stderr.getvalue(), "Can only 'pull-request' on a native Git repository\n") >+ >+ def test_no_modified(self): >+ with OutputCapture() as captured, mocks.local.Git(self.path), mocks.local.Svn(): >+ self.assertEqual(1, program.main( >+ args=('pull-request', '-i', 'pr-branch'), >+ path=self.path, >+ )) >+ self.assertEqual(captured.root.log.getvalue(), "Creating the local development branch 'eng/pr-branch'...\n") >+ self.assertEqual(captured.stderr.getvalue(), 'No modified files\n') >+ >+ def test_staged(self): >+ with OutputCapture() as captured, mocks.local.Git(self.path) as repo, mocks.local.Svn(): >+ repo.staged['added.txt'] = 'added' >+ self.assertEqual(1, program.main( >+ args=('pull-request', '-i', 'pr-branch'), >+ path=self.path, >+ )) >+ self.assertDictEqual(repo.staged, {}) >+ self.assertEqual(repo.head.hash, 'e4390abc95a2026370b8c9813b7e55c61c5d6ebb') >+ >+ self.assertEqual(captured.root.log.getvalue(), '''Creating the local development branch 'eng/pr-branch'... >+Creating commit... >+ Found 1 commit... >+''') >+ self.assertEqual(captured.stderr.getvalue(), "'{}' doesn't have a recognized remote\n".format(self.path)) >+ >+ def test_modified(self): >+ with OutputCapture() as captured, mocks.local.Git(self.path) as repo, mocks.local.Svn(): >+ repo.modified['modified.txt'] = 'diff' >+ self.assertEqual(1, program.main( >+ args=('pull-request', '-i', 'pr-branch'), >+ path=self.path, >+ )) >+ self.assertDictEqual(repo.modified, dict()) >+ self.assertDictEqual(repo.staged, dict()) >+ self.assertEqual(repo.head.hash, 'd05082bf6707252aef3472692598a587ed3fb213') >+ >+ self.assertEqual(captured.stderr.getvalue(), "'{}' doesn't have a recognized remote\n".format(self.path)) >+ self.assertEqual(captured.root.log.getvalue(), '''Creating the local development branch 'eng/pr-branch'... >+ Adding modified.txt... >+Creating commit... >+ Found 1 commit... >+''') >+ >+ def test_github(self): >+ with OutputCapture() as captured, mocks.remote.GitHub() as remote, \ >+ mocks.local.Git(self.path, remote='https://{}'.format(remote.remote)) as repo, mocks.local.Svn(): >+ >+ repo.staged['added.txt'] = 'added' >+ self.assertEqual(0, program.main( >+ args=('pull-request', '-i', 'pr-branch'), >+ path=self.path, >+ )) >+ >+ self.assertEqual(captured.stderr.getvalue(), '') >+ log = captured.root.log.getvalue().splitlines() >+ self.assertEqual( >+ log[:4] + log[7 if sys.version_info > (3, 0) else 5:], [ >+ "Creating the local development branch 'eng/pr-branch'...", >+ 'Creating commit...', >+ ' Found 1 commit...', >+ "Pushing 'eng/pr-branch' to 'fork'...", >+ "Creating pull-request for 'eng/pr-branch'...", >+ "Created 'PR 1 | Created commit'!", >+ ], >+ ) >+ >+ def test_github_update(self): >+ with mocks.remote.GitHub() as remote, mocks.local.Git(self.path, remote='https://{}'.format(remote.remote)) as repo, mocks.local.Svn(): >+ with OutputCapture(): >+ repo.staged['added.txt'] = 'added' >+ self.assertEqual(0, program.main( >+ args=('pull-request', '-i', 'pr-branch'), >+ path=self.path, >+ )) >+ >+ with OutputCapture() as captured: >+ repo.staged['added.txt'] = 'diff' >+ self.assertEqual(0, program.main( >+ args=('pull-request',), >+ path=self.path, >+ )) >+ >+ self.assertEqual(captured.stderr.getvalue(), '') >+ log = captured.root.log.getvalue().splitlines() >+ self.assertEqual( >+ log[:3] + log[6 if sys.version_info > (3, 0) else 4:], [ >+ "Amending commit...", >+ ' Found 1 commit...', >+ "Pushing 'eng/pr-branch' to 'fork'...", >+ "Updating pull-request for 'eng/pr-branch'...", >+ "Updated 'PR 1 | Amended commit'!", >+ ], >+ ) >+ >+ def test_stash(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], >+ )) as repo, mocks.local.Svn(): >+ >+ repo.staged['added.txt'] = 'added' >+ self.assertEqual(0, program.main( >+ args=('pull-request', '-i', 'pr-branch'), >+ path=self.path, >+ )) >+ >+ self.assertEqual(captured.stderr.getvalue(), '') >+ log = captured.root.log.getvalue().splitlines() >+ self.assertEqual( >+ log[:4] + log[7 if sys.version_info > (3, 0) else 5:], [ >+ "Creating the local development branch 'eng/pr-branch'...", >+ 'Creating commit...', >+ ' Found 1 commit...', >+ "Pushing 'eng/pr-branch' to 'origin'...", >+ "Creating pull-request for 'eng/pr-branch'...", >+ "Created 'PR 1 | Created commit'!", >+ ], >+ ) >+ >+ def test_stash_update(self): >+ with 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(): >+ with OutputCapture(): >+ repo.staged['added.txt'] = 'added' >+ self.assertEqual(0, program.main( >+ args=('pull-request', '-i', 'pr-branch'), >+ path=self.path, >+ )) >+ >+ with OutputCapture() as captured: >+ repo.staged['added.txt'] = 'diff' >+ self.assertEqual(0, program.main( >+ args=('pull-request',), >+ path=self.path, >+ )) >+ >+ self.assertEqual(captured.stderr.getvalue(), '') >+ log = captured.root.log.getvalue().splitlines() >+ self.assertEqual( >+ log[:3] + log[6 if sys.version_info > (3, 0) else 4:], [ >+ "Amending commit...", >+ ' Found 1 commit...', >+ "Pushing 'eng/pr-branch' to 'origin'...", >+ "Updating pull-request for 'eng/pr-branch'...", >+ "Updated 'PR 1 | Amended commit'!", >+ ], >+ )
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Formatted Diff
|
Diff
Attachments on
bug 229089
:
435504
|
435505
|
435507
|
435508
|
435510
|
435513
|
435914
|
435917
|
435921
|
435922
|
435927
|
436036
|
436040
|
436074
|
436116
|
436122
|
436250
|
436288
|
436623