Diff
Modified: trunk/Tools/ChangeLog (281694 => 281695)
--- trunk/Tools/ChangeLog 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/ChangeLog 2021-08-27 15:16:27 UTC (rev 281695)
@@ -1,5 +1,60 @@
2021-08-27 Jonathan Bedard <jbed...@apple.com>
+ [git-webkit] Add pull-request command (Part 6)
+ https://bugs.webkit.org/show_bug.cgi?id=229089
+ <rdar://problem/81908751>
+
+ Reviewed by Dewei Zhu.
+
+ * 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-27 Jonathan Bedard <jbed...@apple.com>
+
[run-webkit-tests] Use Python 3 (Part 3)
https://bugs.webkit.org/show_bug.cgi?id=226658
<rdar://problem/78882016>
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -29,7 +29,7 @@
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=[
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -46,7 +46,7 @@
"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)))
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -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 @@
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
@@ -412,6 +413,22 @@
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,
completion=mocks.ProcessCompletion(
@@ -519,6 +536,9 @@
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 @@
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)
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/bitbucket.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -53,6 +53,7 @@
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 @@
],
), url=""
- def request(self, method, url, data="" params=None, **kwargs):
+ def request(self, method, url, data="" params=None, json=None, **kwargs):
if not url.startswith('http://') and not url.startswith('https://'):
return mocks.Response.create404(url)
@@ -194,4 +195,47 @@
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 or {}).get('state', [])
+ states = states if isinstance(states, list) else [states]
+ if states and candidate.get('state') not in states:
+ continue
+ at = (params or {}).get('at', None)
+ 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='commit...@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)
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -58,6 +58,7 @@
self.head = self.commits[self.default_branch][-1]
self.tags = {}
+ self.pull_requests = []
self._environment = None
def __enter__(self):
@@ -280,7 +281,7 @@
), url=""
)
- def request(self, method, url, data="" params=None, **kwargs):
+ def request(self, method, url, data="" 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 @@
# 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)
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -34,6 +34,7 @@
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 @@
)
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)
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py (0 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -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=""
+ )
+
+ @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
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -25,8 +25,10 @@
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
@@ -33,6 +35,150 @@
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 = "" 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 = ""
+ 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 = ""
+ 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 = ""
+
+ 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 @@
id=id or self.name.lower(),
)
+ self.pull_requests = self.PRGenerator(self)
+
@property
def is_git(self):
return True
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -29,7 +29,7 @@
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 @@
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 = "" 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 = ""
+ 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 = ""
+
+ 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 @@
id=id or self.name.lower(),
)
+ self.pull_requests = self.PRGenerator(self)
+
def credentials(self, required=True):
username, token = credentials(
url=""
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -26,6 +26,20 @@
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 @@
if not isinstance(url, six.string_types):
raise ValueError("Expected 'url' to be a string type, not '{}'".format(type(url)))
self.url = ""
+ self.pull_requests = None
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py (281694 => 281695)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py 2021-08-27 14:51:58 UTC (rev 281694)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py 2021-08-27 15:16:27 UTC (rev 281695)
@@ -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 @@
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'!",
+ ],
+ )