Diff
Modified: trunk/Tools/ChangeLog (285764 => 285765)
--- trunk/Tools/ChangeLog 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/ChangeLog 2021-11-13 01:16:24 UTC (rev 285765)
@@ -1,3 +1,35 @@
+2021-11-12 Jonathan Bedard <jbed...@apple.com>
+
+ [git-webkit] Checkout pull-requests
+ https://bugs.webkit.org/show_bug.cgi?id=233042
+ <rdar://problem/85343364>
+
+ Reviewed by Dewei Zhu.
+
+ In GitHub, pull-requests are somewhat difficult to checkout because they're
+ attached to a specific user's mirror of WebKit. Automate this process.
+
+ * Scripts/libraries/webkitscmpy/setup.py: Bump version.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/__init.py__: Ditto.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py:
+ (Git): Add username:branch regex.
+ (Git.checkout): Allow checking out of branches by username:branch.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py:
+ (Git.__init__): Add `git checkout -B`.
+ (Git.checkout): `-B` will force checkout a branch, even if one already exists.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
+ (GitHub): Embed error message in 404 response.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py:
+ (Checkout.main): Allow direct checkout of pull-request instead of relying
+ exclusively on branches.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py:
+ (Clean.main): Add newline.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py:
+ (TestCheckout.test_no_pr_github):
+ (TestCheckout.test_no_pr_bitbucket):
+ (TestCheckout.test_pr_github):
+ (TestCheckout.test_pr_bitbucket):
+
2021-11-12 Sihui Liu <sihui_...@apple.com>
Set default general storage directory to websiteDataDirectory
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -29,7 +29,7 @@
setup(
name='webkitscmpy',
- version='3.0.0',
+ version='3.0.1',
description='Library designed to interact with git and svn repositories.',
long_description=readme(),
classifiers=[
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -46,7 +46,7 @@
"Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
)
-version = Version(3, 0, 0)
+version = Version(3, 0, 1)
AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
AutoInstall.register(Package('jinja2', Version(2, 11, 3)))
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -299,6 +299,7 @@
SSH_REMOTE = re.compile('(ssh://)?git@(?P<host>[^:/]+)[:/](?P<path>.+).git')
HTTP_REMOTE = re.compile(r'(?P<protocol>https?)://(?P<host>[^\/]+)/(?P<path>.+).git')
REMOTE_BRANCH = re.compile(r'remotes\/(?P<remote>[^\/]+)\/(?P<branch>.+)')
+ USER_REMOTE = re.compile(r'(?P<username>[^:/]+):(?P<branch>.+)')
@classmethod
@decorators.Memoize()
@@ -820,6 +821,40 @@
else:
log_arg = []
+ match = self.USER_REMOTE.match(argument)
+ rmt = self.remote()
+ if match and isinstance(rmt, remote.GitHub):
+ username = match.group('username')
+ if not self.url(match.group('username')):
+ url = ""
+ if '://' in url:
+ rmt = '{}://{}/{}/{}.git'.format(url.split(':')[0], url.split('/')[2], username, rmt.name)
+ elif ':' in url:
+ rmt = '{}:{}/{}.git'.format(url.split(':')[0], username, rmt.name)
+ else:
+ sys.stderr.write("Failed to convert '{}' to '{}' remote\n".format(url, username))
+ return None
+ if run(
+ [self.executable(), 'remote', 'add', username, rmt],
+ capture_output=True, cwd=self.root_path,
+ ).returncode:
+ sys.stderr.write("Failed to add remote '{}' as '{}'\n".format(rmt, username))
+ return None
+ self.url.clear()
+ branch = match.group('branch')
+ rc = run(
+ [self.executable(), 'checkout'] + ['-B', branch, '{}/{}'.format(username, branch)] + log_arg,
+ cwd=self.root_path,
+ ).returncode
+ if not rc:
+ return self.commit()
+ if rc == 128:
+ run([self.executable(), 'fetch', username], cwd=self.root_path)
+ return None if run(
+ [self.executable(), 'checkout'] + ['-B', branch, '{}/{}'.format(username, branch)] + log_arg,
+ cwd=self.root_path,
+ ).returncode else self.commit()
+
return None if run(
[self.executable(), 'checkout'] + [self._to_git_ref(argument)] + log_arg,
cwd=self.root_path,
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -352,6 +352,11 @@
generator=lambda *args, **kwargs:
mocks.ProcessCompletion(returncode=0) if self.checkout(args[3], create=True) else mocks.ProcessCompletion(returncode=1)
), mocks.Subprocess.Route(
+ self.executable, 'checkout', '-B', re.compile(r'.+'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs:
+ mocks.ProcessCompletion(returncode=0) if self.checkout(args[3], create=True, force=True) else mocks.ProcessCompletion(returncode=1)
+ ), mocks.Subprocess.Route(
self.executable, 'checkout', re.compile(r'.+'),
cwd=self.path,
generator=lambda *args, **kwargs:
@@ -588,10 +593,14 @@
result.add(branch)
return result
- def checkout(self, something, create=False):
+ def checkout(self, something, create=False, force=False):
commit = self.find(something)
if create:
if commit:
+ if force:
+ self.head = commit
+ self.detached = something not in self.commits.keys()
+ return True
return False
if self.head.branch == self.default_branch:
self.commits[something] = [self.head]
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -21,9 +21,9 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
-import json
import time
+import json as jsonlib
from webkitcorepy import mocks
from webkitscmpy import Commit, remote as scmremote
@@ -50,7 +50,7 @@
super(GitHub, self).__init__(hostname, 'api.{}'.format(hostname))
with open(datafile or os.path.join(os.path.dirname(os.path.dirname(__file__)), 'git-repo.json')) as file:
- self.commits = json.load(file)
+ self.commits = jsonlib.load(file)
for key, commits in self.commits.items():
self.commits[key] = [Commit(**kwargs) for kwargs in commits]
if not git_svn:
@@ -145,7 +145,7 @@
return mocks.Response(
status_code=404,
url=""
- text=json.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
+ text=jsonlib.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
)
response = []
@@ -191,7 +191,7 @@
return mocks.Response(
status_code=404,
url=""
- text=json.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
+ text=jsonlib.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
)
return mocks.Response.fromJson({
'sha': commit.hash,
@@ -219,7 +219,7 @@
return mocks.Response(
status_code=404,
url=""
- text=json.dumps(dict(message='Not found')),
+ text=jsonlib.dumps(dict(message='Not found')),
)
if commit_a.branch != self.default_branch or commit_b.branch == self.default_branch:
@@ -331,7 +331,7 @@
if stripped_url.startswith('{}/tree/'.format(self.remote)):
return self._parents_of_request(url="" ref=stripped_url.split('/')[-1])
- # Check for existance of forked repo
+ # Check for existence of forked repo
if stripped_url.startswith('{}/repos'.format(self.api_remote.split('/')[0])) and stripped_url.split('/')[-1] == self.remote.split('/')[-1]:
username = stripped_url.split('/')[-2]
if username in self.forks or username == self.remote.split('/')[-2]:
@@ -381,7 +381,11 @@
return mocks.Response.fromJson({
key: value for key, value in candidate.items() if key not in ('requested_reviews', 'reviews')
}, url=""
- return mocks.Response.create404(url)
+ return mocks.Response(
+ status_code=404,
+ text=jsonlib.dumps(dict(message='Not found')),
+ url=""
+ )
# Create/update pull-request
pr = dict()
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/checkout.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -20,17 +20,20 @@
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+import re
import sys
from .command import Command
-from webkitcorepy import arguments
-from webkitscmpy import local
+from webkitscmpy import local, log, remote
class Checkout(Command):
name = 'checkout'
- help = 'Given an identifier, revision or hash, normalize and checkout that commit'
+ help = "Given an identifier, revision, hash or pull-request, normalize and checkout that commit." \
+ " Pull requests expected in the form 'PR-#'"
+ PR_RE = re.compile(r'^\[?[Pp][Rr][ -](?P<number>\d+)]?$')
+
@classmethod
def parser(cls, parser, loggers=None):
parser.add_argument(
@@ -42,11 +45,31 @@
@classmethod
def main(cls, args, repository, **kwargs):
if not repository.path:
- sys.stderr.write("Cannot checkout on remote repository")
+ sys.stderr.write('Cannot checkout on remote repository\n')
return 1
+ target = args.argument[0]
+ match = cls.PR_RE.match(target)
+ if match:
+ rmt = repository.remote()
+ if not rmt:
+ sys.stderr.write('Repository does not have associated remote\n')
+ return 1
+ if not rmt.pull_requests:
+ sys.stderr.write('No pull-requests associated with repository\n')
+ return 1
+ pr = rmt.pull_requests.get(number=int(match.group('number')))
+ if not pr:
+ sys.stderr.write("Failed to find 'PR-{}' associated with this repository\n".format(match.group('number')))
+ return 1
+ if isinstance(rmt, remote.GitHub) and pr.author.github:
+ target = '{}:{}'.format(pr.author.github, pr.head)
+ else:
+ target = pr.head
+ log.warning("Found associated branch '{}' for '{}'".format(target, pr))
+
try:
- commit = repository.checkout(args.argument[0])
+ commit = repository.checkout(target)
except (local.Scm.Exception, ValueError) as exception:
# ValueErrors and Scm exceptions usually contain enough information to be displayed
# to the user as an error
@@ -54,6 +77,6 @@
return 1
if not commit:
- sys.stderr.write("Failed to map '{}'\n".format(args.argument[0]))
+ sys.stderr.write("Failed to checkout '{}'\n".format(args.argument[0]))
return 1
return 0
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/clean.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -34,7 +34,7 @@
@classmethod
def main(cls, args, repository, **kwargs):
if not repository.path:
- sys.stderr.write('Cannot clean on remote repository')
+ sys.stderr.write('Cannot clean on remote repository\n')
return 1
return repository.clean()
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py (285764 => 285765)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py 2021-11-13 01:16:04 UTC (rev 285764)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py 2021-11-13 01:16:24 UTC (rev 285765)
@@ -21,10 +21,11 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
import os
+import time
from webkitcorepy import OutputCapture, testing
from webkitcorepy.mocks import Time as MockTime
-from webkitscmpy import program, mocks, local
+from webkitscmpy import program, mocks, local, Contributor, Commit
class TestCheckout(testing.PathTestCase):
@@ -57,6 +58,132 @@
self.assertEqual('621652add7fc416099bd2063366cc38ff61afe36', local.Git(self.path).commit().hash)
+ def test_no_pr_github(self):
+ with OutputCapture() as captured, mocks.remote.GitHub() as remote, \
+ mocks.local.Git(self.path, remote='https://{}'.format(remote.remote)), mocks.local.Svn():
+ self.assertEqual(1, program.main(
+ args=('checkout', 'PR-1'),
+ path=self.path,
+ ))
+
+ self.assertEqual(
+ "Request to 'https://api.github.example.com/repos/WebKit/WebKit/pulls/1' returned status code '404'\n"
+ "Message: Not found\n"
+ "Failed to find 'PR-1' associated with this repository\n",
+ captured.stderr.getvalue(),
+ )
+
+ def test_no_pr_bitbucket(self):
+ with OutputCapture() as captured, mocks.remote.BitBucket() as remote, mocks.local.Git(self.path, remote='ssh://git@{}/{}/{}.git'.format(
+ remote.hosts[0], remote.project.split('/')[1], remote.project.split('/')[3],
+ )), mocks.local.Svn():
+ self.assertEqual(1, program.main(
+ args=('checkout', 'PR-1'),
+ path=self.path,
+ ))
+
+ self.assertEqual(
+ "Request to 'https://bitbucket.example.com/rest/api/1.0/projects/WEBKIT/repos/webkit/pull-requests/1' returned status code '404'\n"
+ "Failed to find 'PR-1' associated with this repository\n",
+ captured.stderr.getvalue(),
+ )
+
+ def test_pr_github(self):
+ with OutputCapture(), mocks.remote.GitHub() as remote, \
+ mocks.local.Git(self.path, remote='https://{}'.format(remote.remote)) as repo, mocks.local.Svn():
+ remote.users = dict(
+ rreviewer=Contributor('Ricky Reviewer', ['rrevie...@webkit.org'], github='rreviewer'),
+ tcontributor=Contributor('Tim Contributor', ['tcontribu...@webkit.org'], github='tcontributor'),
+ )
+ remote.issues = {
+ 1: dict(
+ comments=[],
+ assignees=[],
+ )
+ }
+ remote.pull_requests = [dict(
+ number=1,
+ state='open',
+ title='Example Change',
+ user=dict(login='tcontributor'),
+ body='''#### a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd
+<pre>
+To Be Committed
+
+Reviewed by NOBODY (OOPS!).
+</pre>
+''',
+ head=dict(ref='tcontributor:eng/example'),
+ base=dict(ref='main'),
+ requested_reviews=[dict(login='rreviewer')],
+ reviews=[dict(user=dict(login='rreviewer'), state='CHANGES_REQUESTED')],
+ )]
+ repo.commits['eng/example'] = [Commit(
+ hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+ identifier='3.1@eng/example',
+ timestamp=int(time.time()) - 60,
+ author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+ message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
+ )]
+
+ self.assertEqual(0, program.main(
+ args=('checkout', 'PR-1'),
+ path=self.path,
+ ))
+
+ self.assertEqual('a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd', local.Git(self.path).commit().hash)
+
+ def test_pr_bitbucket(self):
+ with OutputCapture(), mocks.remote.BitBucket() as remote, mocks.local.Git(self.path, remote='ssh://git@{}/{}/{}.git'.format(
+ remote.hosts[0], remote.project.split('/')[1], remote.project.split('/')[3],
+ )) as repo, mocks.local.Svn():
+ remote.pull_requests = [dict(
+ id=1,
+ state='OPEN',
+ open=True,
+ closed=False,
+ activities=[],
+ title='Example Change',
+ author=dict(
+ user=dict(
+ name='tcontributor',
+ emailAddress='tcontribu...@apple.com',
+ displayName='Tim Contributor',
+ ),
+ ), body='''#### a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd
+```
+To Be Committed
+
+Reviewed by NOBODY (OOPS!).
+```
+''',
+ fromRef=dict(displayId='eng/example', id='refs/heads/eng/example'),
+ toRef=dict(displayId='main', id='refs/heads/main'),
+ reviewers=[
+ dict(
+ user=dict(
+ displayName='Ricky Reviewer',
+ emailAddress='rrevie...@webkit.org',
+ ), approved=False,
+ status='NEEDS_WORK',
+ ),
+ ],
+ )]
+ repo.commits['eng/example'] = [Commit(
+ hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+ identifier='3.1@eng/example',
+ timestamp=int(time.time()) - 60,
+ author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+ message='To Be Committed\n\nReviewed by NOBODY (OOPS!).\n',
+ )]
+
+ self.assertEqual(0, program.main(
+ args=('checkout', 'PR-1'),
+ path=self.path,
+ ))
+
+ self.assertEqual('a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd', local.Git(self.path).commit().hash)
+
def test_checkout_svn(self):
with OutputCapture(), mocks.local.Git(), mocks.local.Svn(self.path), MockTime:
self.assertEqual(6, local.Svn(self.path).commit().revision)
@@ -68,6 +195,18 @@
self.assertEqual(4, local.Svn(self.path).commit().revision)
+ def test_svn_pr(self):
+ with OutputCapture() as captured, mocks.local.Git(), mocks.local.Svn(self.path), MockTime:
+ self.assertEqual(1, program.main(
+ args=('checkout', 'PR-1'),
+ path=self.path,
+ ))
+
+ self.assertEqual(
+ 'No pull-requests associated with repository\n',
+ captured.stderr.getvalue(),
+ )
+
def test_checkout_remote(self):
with OutputCapture(), mocks.remote.Svn():
self.assertEqual(1, program.main(