Skip to content
Permalink
Browse files
[webkitscmpy] Support draft pull-requests
https://bugs.webkit.org/show_bug.cgi?id=235721
<rdar://problem/88139678>

Rubber-stamped by Aakash Jain.

GitHub has the concept of a "draft" pull request. Our tooling should allow users
to request that the pull request they are updating or creating be converted to a draft.

* Tools/Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
(GitHub.request): Handle "draft" in upload.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py:
(PullRequest.parser): Add --draft option.
(PullRequest.main): When creating or uploading a pull-request, set draft state.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/pull_request.py:
(PullRequest.__init__): Pass draft state.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py:
(BitBucket.PRGenerator.PullRequest): Pass draft state to PullRequest object.
(BitBucket.PRGenerator.create): Accept draft flag.
(BitBucket.PRGenerator.update): Ditto.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py:
(GitHub.PRGenerator): Draft pull requests are a GitHub idea.
(GitHub.PRGenerator.PullRequest): Pass draft state to PullRequest object.
(GitHub.PRGenerator.create): Accept draft flag.
(GitHub.PRGenerator.update): Ditto.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py:
(Scm.PRGenerator): Draft pull requests are a GitHub idea.
(Scm.PRGenerator.create): Accept draft flag.
(Scm.PRGenerator.update): Ditto.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py:
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py:
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py:


Canonical link: https://commits.webkit.org/247298@main
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@289856 268f45cc-cd09-0410-ab3c-d52691b4dbfc
  • Loading branch information
JonWBedard committed Feb 15, 2022
1 parent 6f560a5 commit 5f19dc5efe078526461f86d87d75d2cb872bcdc0
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 12 deletions.
@@ -1,3 +1,40 @@
2022-02-15 Jonathan Bedard <jbedard@apple.com>

[webkitscmpy] Support draft pull-requests
https://bugs.webkit.org/show_bug.cgi?id=235721
<rdar://problem/88139678>

Rubber-stamped by Aakash Jain.

GitHub has the concept of a "draft" pull request. Our tooling should allow users
to request that the pull request they are updating or creating be converted to a draft.

* Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
(GitHub.request): Handle "draft" in upload.
* Scripts/libraries/webkitscmpy/webkitscmpy/program/pull_request.py:
(PullRequest.parser): Add --draft option.
(PullRequest.main): When creating or uploading a pull-request, set draft state.
* Scripts/libraries/webkitscmpy/webkitscmpy/pull_request.py:
(PullRequest.__init__): Pass draft state.
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/bitbucket.py:
(BitBucket.PRGenerator.PullRequest): Pass draft state to PullRequest object.
(BitBucket.PRGenerator.create): Accept draft flag.
(BitBucket.PRGenerator.update): Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py:
(GitHub.PRGenerator): Draft pull requests are a GitHub idea.
(GitHub.PRGenerator.PullRequest): Pass draft state to PullRequest object.
(GitHub.PRGenerator.create): Accept draft flag.
(GitHub.PRGenerator.update): Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/scm.py:
(Scm.PRGenerator): Draft pull requests are a GitHub idea.
(Scm.PRGenerator.create): Accept draft flag.
(Scm.PRGenerator.update): Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/checkout_unittest.py:
* Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py:
* Scripts/libraries/webkitscmpy/webkitscmpy/test/pull_request_unittest.py:

2022-02-15 Jonathan Bedard <jbedard@apple.com>

[EWS] Support PRs when sending build failure emails (Follow-up fix)
@@ -29,7 +29,7 @@ def readme():

setup(
name='webkitscmpy',
version='4.1.5',
version='4.2.0',
description='Library designed to interact with git and svn repositories.',
long_description=readme(),
classifiers=[
@@ -46,7 +46,7 @@ def _maybe_add_webkitcorepy_path():
"Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
)

version = Version(4, 1, 5)
version = Version(4, 2, 0)

AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
AutoInstall.register(Package('jinja2', Version(2, 11, 3)))
@@ -370,13 +370,16 @@ def request(self, method, url, data=None, params=None, auth=None, json=None, **k
)
if json.get('state'):
pr['state'] = json.get('state')
if json.get('draft'):
pr['draft'] = json.get('draft')

# 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['state'] = 'open'
pr['user'] = dict(login=auth.username)
pr['_links'] = dict(issue=dict(href='https://{}/issues/{}'.format(self.api_remote, pr['number'])))
pr['draft'] = pr.get('draft', False)
self.pull_requests.append(pr)
if pr['number'] not in self.issues:
self.issues[pr['number']] = dict(
@@ -72,6 +72,10 @@ def parser(cls, parser, loggers=None):
help='Create numbered branches to track the history of a change',
action=arguments.NoAction,
)
parser.add_argument(
'--draft', dest='draft', action='store_true', default=None,
help='Mark a pull request as a draft when creating it',
)

@classmethod
def create_commit(cls, args, repository, **kwargs):
@@ -195,6 +199,10 @@ def main(cls, args, repository, **kwargs):
if not rmt.pull_requests:
sys.stderr.write("'{}' cannot generate pull-requests\n".format(rmt.url))
return 1
if args.draft and not rmt.pull_requests.SUPPORTS_DRAFTS:
sys.stderr.write("'{}' does not support draft pull requests, aborting\n".format(rmt.url))
return 1

existing_pr = None
for pr in rmt.pull_requests.find(opened=None, head=repository.branch):
existing_pr = pr
@@ -222,7 +230,8 @@ def main(cls, args, repository, **kwargs):
commits=commits,
base=branch_point.branch,
head=repository.branch,
opened=None if existing_pr.opened else True
opened=None if existing_pr.opened else True,
draft=args.draft,
)
if not pr:
sys.stderr.write("Failed to update pull-request '{}'\n".format(existing_pr))
@@ -235,6 +244,7 @@ def main(cls, args, repository, **kwargs):
commits=commits,
base=branch_point.branch,
head=repository.branch,
draft=args.draft,
)
if not pr:
sys.stderr.write("Failed to create pull-request for '{}'\n".format(repository.branch))
@@ -139,14 +139,15 @@ def __init__(
body=None, author=None,
head=None, base=None,
opened=None, generator=None, metadata=None,
url=None,
url=None, draft=None
):
self.number = number
self.title = title
self.body, self.commits = self.parse_body(body)
self.author = author
self.head = head
self.base = base
self.draft = draft
self._opened = opened
self._reviewers = None
self._approvers = None
@@ -52,6 +52,7 @@ def PullRequest(self, data):
opened=True if data.get('open') else (False if data.get('closed') else None),
generator=self,
url='{}/pull-requests/{}/overview'.format(self.repository.url, data['id']),
draft=False,
)

result._reviewers = []
@@ -106,7 +107,10 @@ def find(self, opened=True, head=None, base=None):
continue
yield self.PullRequest(datum)

def create(self, head, title, body=None, commits=None, base=None):
def create(self, head, title, body=None, commits=None, base=None, draft=None):
if draft:
sys.stderr.write('Bitbucket does not support the concept of a "draft" pull request\n')

for key, value in dict(head=head, title=title).items():
if not value:
raise ValueError("Must define '{}' when creating pull-request".format(key))
@@ -143,10 +147,13 @@ def create(self, head, title, body=None, commits=None, base=None):
return None
return self.PullRequest(response.json())

def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None):
def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None, draft=None):
if not isinstance(pull_request, PullRequest):
raise ValueError("Expected 'pull_request' to be of type '{}' not '{}'".format(PullRequest, type(pull_request)))

if draft:
sys.stderr.write('Bitbucket does not support the concept of a "draft" pull request\n')

pr_url = 'https://{domain}/rest/api/1.0/projects/{project}/repos/{name}/pull-requests/{id}'.format(
domain=self.repository.domain,
project=self.repository.project,
@@ -42,6 +42,8 @@ class GitHub(Scm):
EMAIL_RE = re.compile(r'(?P<email>[^@]+@[^@]+)(@.*)?')

class PRGenerator(Scm.PRGenerator):
SUPPORTS_DRAFTS = True

def PullRequest(self, data):
if not data:
return None
@@ -61,6 +63,7 @@ def PullRequest(self, data):
metadata=dict(
issue=self.repository.tracker.from_string(issue_ref) if issue_ref else None,
), url='{}/pull/{}'.format(self.repository.url, data['number']),
draft=data['draft'],
)

def get(self, number):
@@ -86,7 +89,8 @@ def find(self, opened=True, head=None, base=None):
continue
yield self.PullRequest(datum)

def create(self, head, title, body=None, commits=None, base=None):
def create(self, head, title, body=None, commits=None, base=None, draft=None):
draft = False if draft is None else draft
for key, value in dict(head=head, title=title).items():
if not value:
raise ValueError("Must define '{}' when creating pull-request".format(key))
@@ -104,6 +108,7 @@ def create(self, head, title, body=None, commits=None, base=None):
body=PullRequest.create_body(body, commits),
base=base or self.repository.default_branch,
head='{}:{}'.format(user, head),
draft=draft,
),
)
if response.status_code // 100 != 2:
@@ -115,11 +120,13 @@ def create(self, head, title, body=None, commits=None, base=None):
sys.stderr.write("Failed to assign '{}' to '{}'\n".format(result, user))
return result

def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None):
def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None, draft=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)) and opened is None:
raise ValueError('No arguments to update pull-request provided')
if draft is not None:
sys.stderr.write('GitHub does not allow editing draft state via API\n')

user, _ = self.repository.credentials(required=True)
updates = dict(
@@ -27,6 +27,8 @@

class Scm(ScmBase):
class PRGenerator(object):
SUPPORTS_DRAFTS = False

def __init__(self, repository):
self.repository = repository

@@ -36,10 +38,10 @@ def get(self, number):
def find(self, opened=True, head=None, base=None):
raise NotImplementedError()

def create(self, head, title, body=None, commits=None, base=None):
def create(self, head, title, body=None, commits=None, base=None, draft=None):
raise NotImplementedError()

def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None):
def update(self, pull_request, head=None, title=None, body=None, commits=None, base=None, opened=None, draft=None):
raise NotImplementedError()

def reviewers(self, pull_request):
@@ -117,6 +117,7 @@ def test_pr_github(self):
base=dict(ref='main'),
requested_reviews=[dict(login='rreviewer')],
reviews=[dict(user=dict(login='rreviewer'), state='CHANGES_REQUESTED')],
draft=False,
)]
repo.commits['eng/example'] = [Commit(
hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
@@ -244,7 +244,7 @@ def webserver(cls, approved=None):
dict(user=dict(login='rreviewer'), state='CHANGES_REQUESTED')
] if approved is not None else [], _links=dict(
issue=dict(href='https://{}/issues/1'.format(result.api_remote)),
),
), draft=False,
)]
return result

@@ -329,6 +329,39 @@ def test_github(self):
args=('pull-request', '-i', 'pr-branch', '-v', '--no-history'),
path=self.path,
))
self.assertEqual(local.Git(self.path).remote().pull_requests.get(1).draft, False)

self.assertEqual(
captured.stdout.getvalue(),
"Created the local development branch 'eng/pr-branch'\n"
"Created 'PR 1 | [Testing] Creating commits'!\n"
"https://github.example.com/WebKit/WebKit/pull/1\n",
)
self.assertEqual(captured.stderr.getvalue(), '')
log = captured.root.log.getvalue().splitlines()
self.assertEqual(
[line for line in log if 'Mock process' not in line], [
"Creating the local development branch 'eng/pr-branch'...",
' Found 1 commit...',
'Creating commit...',
"Rebasing 'eng/pr-branch' on 'main'...",
"Rebased 'eng/pr-branch' on 'main!'",
" Found 1 commit...",
"Pushing 'eng/pr-branch' to 'fork'...",
"Creating pull-request for 'eng/pr-branch'...",
],
)

def test_github_draft(self):
with OutputCapture(level=logging.INFO) 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', '-v', '--no-history', '--draft'),
path=self.path,
))
self.assertEqual(local.Git(self.path).remote().pull_requests.get(1).draft, True)

self.assertEqual(
captured.stdout.getvalue(),
@@ -526,6 +559,7 @@ def test_bitbucket(self):
args=('pull-request', '-i', 'pr-branch', '-v'),
path=self.path,
))
self.assertEqual(local.Git(self.path).remote().pull_requests.get(1).draft, False)

self.assertEqual(
captured.stdout.getvalue(),
@@ -548,6 +582,38 @@ def test_bitbucket(self):
],
)

def test_bitbucket_draft(self):
with OutputCapture(level=logging.INFO) 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(1, program.main(
args=('pull-request', '-i', 'pr-branch', '-v', '--draft'),
path=self.path,
))

self.assertEqual(
captured.stdout.getvalue(),
"Created the local development branch 'eng/pr-branch'\n",
)
self.assertEqual(
captured.stderr.getvalue(),
"'https://bitbucket.example.com/projects/WEBKIT/repos/webkit' does not support draft pull requests, aborting\n",
)
log = captured.root.log.getvalue().splitlines()
self.assertEqual(
[line for line in log if 'Mock process' not in line], [
"Creating the local development branch 'eng/pr-branch'...",
' Found 1 commit...',
'Creating commit...',
"Rebasing 'eng/pr-branch' on 'main'...",
"Rebased 'eng/pr-branch' on 'main!'",
" Found 1 commit...",
"Pushing 'eng/pr-branch' to 'origin'...",
],
)

def test_bitbucket_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],
@@ -759,7 +825,7 @@ def webserver(cls):
dict(user=dict(login='sreviewer'), state='CHANGES_REQUESTED'),
], _links=dict(
issue=dict(href='https://{}/issues/1'.format(result.api_remote)),
),
), draft=False,
)]
return result

@@ -780,6 +846,7 @@ def test_get(self):
self.assertEqual(pr.title, 'Example Change')
self.assertEqual(pr.head, 'eng/pull-request')
self.assertEqual(pr.base, 'main')
self.assertEqual(pr.draft, False)

def test_reviewers(self):
with self.webserver():
@@ -906,6 +973,7 @@ def test_get(self):
self.assertEqual(pr.title, 'Example Change')
self.assertEqual(pr.head, 'eng/pull-request')
self.assertEqual(pr.base, 'main')
self.assertEqual(pr.draft, False)

def test_reviewers(self):
with self.webserver():

0 comments on commit 5f19dc5

Please sign in to comment.