Diff
Modified: trunk/Tools/ChangeLog (285271 => 285272)
--- trunk/Tools/ChangeLog 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/ChangeLog 2021-11-04 18:44:52 UTC (rev 285272)
@@ -1,3 +1,45 @@
+2021-11-03 Jonathan Bedard <jbed...@apple.com>
+
+ [git-webkit] Add land command
+ https://bugs.webkit.org/show_bug.cgi?id=231821
+ <rdar://problem/84309339>
+
+ Reviewed by Dewei Zhu.
+
+ * Scripts/git-webkit: WebKit still uses Svn as it's canonical source of truth.
+ * Scripts/libraries/webkitscmpy/setup.py: Bump version.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py:
+ (Git._to_git_ref): Convert identifiers or revisions to git refs.
+ (Git.checkout): Use shared _to_git_ref function.
+ (Git.rebase): Support rebasing identifiers.
+ (Git.pull): Generalize computation of the gmtoffset.
+ (Git.diff_lines): Output diff between two refs.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py:
+ (Git.__init__): Add branch, push and diff commands.
+ (Git.filter_branch): Support sed.
+ (Git.pull): Mock `git pull`
+ (Git.move_branch): Mock `git rebase`.
+ (Git.push): Mock `git push`.
+ (Git.reset): Mock `git reset HEAD~#`.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py:
+ (main): Add land command.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/program/land.py: Added.
+ (Land.parser):
+ (Land.main): Rebase the current branch against it's root, assign the reviewer, check for 'oops' messages,
+ rebase against target branch, update target branch ref, optionally canonicalize, push (or svn dcommit) and
+ update and close pull-request with information on the landed commit.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py:
+ (ScmBase.gmtoffset): Add shared computation of GMT offset.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
+ (TestGit.test_commits): `git log` should exclude base ref.
+ (TestGit.test_commits_branch): Ditto.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py: Added.
+ (repository):
+ (TestLand):
+ (TestLandGitHub):
+ (TestLandBitBucket):
+
2021-11-04 Jonathan Bedard <jbed...@apple.com>
[generate-webkit-css-docs] Change shebang to Python 3
Modified: trunk/Tools/Scripts/git-webkit (285271 => 285272)
--- trunk/Tools/Scripts/git-webkit 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/git-webkit 2021-11-04 18:44:52 UTC (rev 285272)
@@ -72,5 +72,6 @@
subversion=is_webkit_filter('https://svn.webkit.org/repository/webkit'),
additional_setup=is_webkit_filter(additional_setup),
hooks=os.path.join(os.path.abspath(os.path.join(os.path.dirname(__file__))), 'hooks'),
+ canonical_svn=is_webkit_filter(True),
))
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (285271 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -29,7 +29,7 @@
setup(
name='webkitscmpy',
- version='2.2.23',
+ version='3.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 (285271 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -46,7 +46,7 @@
"Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
)
-version = Version(2, 2, 23)
+version = Version(3, 0, 0)
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 (285271 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -35,8 +35,7 @@
from webkitcorepy import run, decorators, NestedFuzzyDict
from webkitscmpy.local import Scm
-from webkitscmpy import remote
-from webkitscmpy import Commit, Contributor, log
+from webkitscmpy import remote, Commit, Contributor, log
class Git(Scm):
@@ -127,7 +126,7 @@
log = None
try:
kwargs = dict()
- if sys.version_info >= (3, 0):
+ if sys.version_info >= (3, 6):
kwargs = dict(encoding='utf-8')
self._last_populated[branch] = time.time()
log = subprocess.Popen(
@@ -135,7 +134,7 @@
cwd=self.repo.root_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
- ** kwargs
+ **kwargs
)
if log.poll():
raise self.repo.Exception("Failed to construct branch history for '{}'".format(branch))
@@ -701,7 +700,7 @@
cwd=self.root_path,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
- **(dict(encoding='utf-8') if sys.version_info > (3, 0) else dict())
+ **(dict(encoding='utf-8') if sys.version_info > (3, 6) else dict())
)
if log.poll():
raise self.Exception("Failed to construct history for '{}'".format(end.branch))
@@ -794,10 +793,24 @@
raise ValueError("'{}' is not an argument recognized by git".format(argument))
return self.commit(hash=output.stdout.rstrip(), include_log=include_log, include_identifier=include_identifier)
- def checkout(self, argument):
+ def _to_git_ref(self, argument):
+ if not argument:
+ return None
if not isinstance(argument, six.string_types):
raise ValueError("Expected 'argument' to be a string, not '{}'".format(type(argument)))
+ parsed_commit = Commit.parse(argument, do_assert=False)
+ if parsed_commit and not parsed_commit.hash:
+ return self.commit(
+ hash=parsed_commit.hash,
+ revision=parsed_commit.revision,
+ identifier=parsed_commit.identifier,
+ branch=parsed_commit.branch,
+ include_log=False,
+ include_identifier=False,
+ ).hash
+ return argument
+ def checkout(self, argument):
self._branch = None
if log.level > logging.WARNING:
@@ -807,21 +820,8 @@
else:
log_arg = []
- parsed_commit = Commit.parse(argument, do_assert=False)
- if parsed_commit:
- commit = self.commit(
- hash=parsed_commit.hash,
- revision=parsed_commit.revision,
- identifier=parsed_commit.identifier,
- branch=parsed_commit.branch,
- )
- return None if run(
- [self.executable(), 'checkout'] + [commit.hash] + log_arg,
- cwd=self.root_path,
- ).returncode else commit
-
return None if run(
- [self.executable(), 'checkout'] + [argument] + log_arg,
+ [self.executable(), 'checkout'] + [self._to_git_ref(argument)] + log_arg,
cwd=self.root_path,
).returncode else self.commit()
@@ -829,6 +829,10 @@
if head == self.default_branch or self.prod_branches.match(head):
raise RuntimeError("Rebasing production branch '{}' banned in tooling!".format(head))
+ target = self._to_git_ref(target)
+ base = self._to_git_ref(base)
+ head = self._to_git_ref(head)
+
code = run([self.executable(), 'rebase', '--onto', target, base or target, head], cwd=self.root_path).returncode
if self.cache:
self.cache.clear(head if head != 'HEAD' else self.branch)
@@ -837,7 +841,7 @@
return run([
self.executable(), 'filter-branch', '-f',
'--env-filter', "GIT_AUTHOR_DATE='{date}';GIT_COMMITTER_DATE='{date}'".format(
- date='{} -{}'.format(int(time.time()), int(time.localtime().tm_gmtoff * 100 / (60 * 60)))
+ date='{} -{}'.format(int(time.time()), self.gmtoffset())
), '{}...{}'.format(target, head),
], cwd=self.root_path, env={'FILTER_BRANCH_SQUELCH_WARNING': '1'}, capture_output=True).returncode
@@ -869,7 +873,7 @@
self.executable(),
'filter-branch', '-f',
'--env-filter', "GIT_AUTHOR_DATE='{date}';GIT_COMMITTER_DATE='{date}'".format(
- date='{} -{}'.format(int(time.time()), int(time.localtime().tm_gmtoff * 100 / (60 * 60)))
+ date='{} -{}'.format(int(time.time()), self.gmtoffset())
), 'HEAD...{}'.format('{}/{}'.format(remote, branch)),
], cwd=self.root_path, env={'FILTER_BRANCH_SQUELCH_WARNING': '1'}).returncode
@@ -912,3 +916,27 @@
if set(staged) - added:
return staged
return staged + self.modified(staged=False)
+
+ def diff_lines(self, base, head=None):
+ base = self._to_git_ref(base)
+ head = self._to_git_ref(head)
+
+ kwargs = dict()
+ if sys.version_info >= (3, 6):
+ kwargs = dict(encoding='utf-8')
+ target = '{}..{}'.format(base, head) if head else base
+ proc = subprocess.Popen(
+ [self.executable(), 'diff', target],
+ cwd=self.root_path,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ **kwargs
+ )
+
+ if proc.poll():
+ sys.stderr.write("Failed to generate diff for '{}'\n".format(target))
+
+ line = proc.stdout.readline()
+ while line:
+ yield line.rstrip()
+ line = proc.stdout.readline()
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (285271 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -153,6 +153,14 @@
date=datetime.fromtimestamp(self.head.timestamp).strftime('%Y-%m-%d %H:%M:%S'),
),
),
+ ), mocks.Subprocess.Route(
+ self.executable, 'svn', 'fetch',
+ cwd=self.path,
+ completion=mocks.ProcessCompletion(returncode=0)
+ ), mocks.Subprocess.Route(
+ self.executable, 'svn', 'dcommit',
+ cwd=self.path,
+ completion=mocks.ProcessCompletion(returncode=0)
),
]
@@ -287,7 +295,7 @@
os.path.basename(path),
commit.revision,
)] if git_svn else []),
- )) for commit in self.commits_in_range(args[3].split('...')[-1], args[3].split('...')[0])
+ )) for commit in list(self.commits_in_range(args[3].split('...')[-1], args[3].split('...')[0]))[:-1]
])
)
), mocks.Subprocess.Route(
@@ -349,7 +357,7 @@
generator=lambda *args, **kwargs:
mocks.ProcessCompletion(returncode=0) if self.checkout(args[2], create=False) else mocks.ProcessCompletion(returncode=1)
), mocks.Subprocess.Route(
- self.executable, 'filter-branch', '-f',
+ self.executable, 'filter-branch', '-f', '--env-filter', re.compile(r'.*'), '--msg-filter',
cwd=self.path,
generator=lambda *args, **kwargs: self.filter_branch(
args[-1],
@@ -357,6 +365,18 @@
environment_shell=args[4] if args[3] == '--env-filter' and args[4] else None,
)
), mocks.Subprocess.Route(
+ self.executable, 'filter-branch', '-f', '--env-filter', re.compile(r'.*'), '--msg-filter', re.compile(r'sed .*'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs: self.filter_branch(
+ args[-1],
+ environment_shell=args[4] if args[3] == '--env-filter' and args[4] else None,
+ sed=args[6].split('sed ')[-1] if args[5] == '--msg-filter' else None,
+ )
+ ), mocks.Subprocess.Route(
+ self.executable, 'filter-branch', '-f',
+ cwd=self.path,
+ completion=mocks.ProcessCompletion(returncode=0),
+ ), mocks.Subprocess.Route(
self.executable, 'svn', 'fetch', '--log-window-size=5000', '-r', re.compile(r'\d+:HEAD'),
cwd=self.path,
generator=lambda *args, **kwargs:
@@ -364,7 +384,7 @@
), mocks.Subprocess.Route(
self.executable, 'pull',
cwd=self.path,
- completion=mocks.ProcessCompletion(returncode=0),
+ generator=lambda *args, **kwargs: self.pull(),
), mocks.Subprocess.Route(
self.executable, 'config', '-l',
cwd=self.path,
@@ -439,6 +459,32 @@
cwd=self.path,
generator=lambda *args, **kwargs: self.rebase(args[3], args[4], args[5]),
), mocks.Subprocess.Route(
+ self.executable, 'branch', '-f', re.compile(r'.+'), re.compile(r'.+'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs: self.move_branch(args[3], args[4]),
+ ), mocks.Subprocess.Route(
+ self.executable, 'push', 'origin', re.compile(r'.+'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs: self.push(args[2], args[3]),
+ ), mocks.Subprocess.Route(
+ self.executable, 'diff', re.compile(r'.+'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs: mocks.ProcessCompletion(
+ returncode=0,
+ stdout='\n'.join([
+ '--- a/ChangeLog\n+++ b/ChangeLog\n@@ -1,0 +1,0 @@\n{}'.format(
+ '\n'.join(['+ {}'.format(line) for line in commit.message.splitlines()])
+ ) for commit in list(self.commits_in_range(
+ args[2].split('..')[0],
+ args[2].split('..')[-1] if '..' in args[2] else self.commits[self.default_branch][-1].hash,
+ ))[:-1]
+ ])
+ )
+ ), mocks.Subprocess.Route(
+ self.executable, 'reset', re.compile(r'HEAD~\d+'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs: self.reset(int(args[2].split('~')[-1])),
+ ), mocks.Subprocess.Route(
self.executable,
cwd=self.path,
completion=mocks.ProcessCompletion(
@@ -567,7 +613,7 @@
self.detached = something not in self.commits.keys()
return True if commit else False
- def filter_branch(self, range, identifier_template=None, environment_shell=None):
+ def filter_branch(self, range, identifier_template=None, environment_shell=None, sed=None):
# We can't effectively mock the bash script in the command, but we can mock the python code that
# script calls, which is where the program logic is.
head, start = range.split('...')
@@ -621,6 +667,14 @@
lines.pop(-1)
commit.message = '\n'.join(lines)
+ if sed:
+ match = re.match(r'"s/(?P<re>.+)/(?P<value>.+)/g"', sed)
+ if match:
+ commit.message = re.sub(
+ match.group('re').replace('(', '\\(').replace(')', '\\)'),
+ match.group('value'), commit.message,
+ )
+
if not environment_shell:
continue
if re.search(r'echo "Overwriting', environment_shell):
@@ -791,3 +845,34 @@
if self.commits[target][-1].branch_point:
commit.identifier += self.commits[target][-1].identifier
return mocks.ProcessCompletion(returncode=0)
+
+ def pull(self):
+ self.head = self.commits[self.head.branch][-1]
+ return mocks.ProcessCompletion(returncode=0)
+
+ def move_branch(self, to_be_moved, moved_to):
+ if moved_to == self.default_branch:
+ return mocks.ProcessCompletion(returncode=0)
+ if to_be_moved != self.default_branch:
+ self.commits[to_be_moved] = self.commits[moved_to]
+ self.head = self.commits[to_be_moved][-1]
+ return mocks.ProcessCompletion(returncode=0)
+ self.commits[to_be_moved] += [
+ Commit(
+ branch=to_be_moved, repository_id=commit.repository_id,
+ timestamp=commit.timestamp,
+ identifier=commit.identifier + (commit.branch_point or 0), branch_point=None,
+ hash=commit.hash, revision=commit.revision,
+ author=commit.author, message=commit.message,
+ ) for commit in self.commits[moved_to]
+ ]
+ self.head = self.commits[to_be_moved][-1]
+ return mocks.ProcessCompletion(returncode=0)
+
+ def push(self, remote, branch):
+ self.remotes['{}/{}'.format(remote, branch)] = self.commits[branch][-1]
+ return mocks.ProcessCompletion(returncode=0)
+
+ def reset(self, index):
+ self.head = self.commits[self.head.branch][-(index + 1)]
+ return mocks.ProcessCompletion(returncode=0)
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py (285271 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -32,6 +32,7 @@
from .command import Command
from .checkout import Checkout
from .find import Find, Info
+from .land import Land
from .log import Log
from .pull import Pull
from .pull_request import PullRequest
@@ -45,6 +46,7 @@
def main(
args=None, path=None, loggers=None, contributors=None,
identifier_template=None, subversion=None, additional_setup=None, hooks=None,
+ canonical_svn=False,
):
logging.basicConfig(level=logging.WARNING)
@@ -69,7 +71,7 @@
)
subparsers = parser.add_subparsers(help='sub-command help')
- programs = [Blame, Branch, Canonicalize, Checkout, Clean, Find, Info, Log, Pull, PullRequest, Setup]
+ programs = [Blame, Branch, Canonicalize, Checkout, Clean, Find, Info, Land, Log, Pull, PullRequest, Setup]
if subversion:
programs.append(SetupGitSvn)
@@ -122,6 +124,9 @@
if callable(additional_setup) and list(inspect.signature(additional_setup).parameters.keys()) == ['repository']:
additional_setup = additional_setup(repository)
+ if callable(canonical_svn):
+ canonical_svn = canonical_svn(repository)
+
if not getattr(parsed, 'main', None):
parser.print_help()
return -1
@@ -133,4 +138,5 @@
subversion=subversion,
additional_setup=additional_setup,
hooks=hooks,
+ canonical_svn=canonical_svn,
)
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/land.py (0 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/land.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/land.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -0,0 +1,207 @@
+# 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 re
+import sys
+import time
+
+from .canonicalize import Canonicalize
+from .command import Command
+from .branch import Branch
+from argparse import Namespace
+from webkitcorepy import arguments, run, string_utils, Terminal
+from webkitscmpy import local, log
+
+
+class Land(Command):
+ name = 'land'
+ help = 'If on a pull-request or commit-queue branch, rebase the ' \
+ 'current branch onto the target production branch and push.'
+
+ OOPS_RE = re.compile(r'\(O+P+S!*\)')
+ REVIEWED_BY_RE = re.compile('Reviewed by (?P<approver>.+)')
+ REMOTE = 'origin'
+ MIRROR_TIMEOUT = 60
+
+ @classmethod
+ def parser(cls, parser, loggers=None):
+ parser.add_argument(
+ '--no-force-review', '--force-review', '--no-review',
+ dest='review', default=True,
+ help='Check if the change has been approved or blocked by reviewers',
+ action=""
+ )
+ parser.add_argument(
+ '--no-oops', '--no-allow-oops', '--allow-oops',
+ dest='oops', default=False,
+ help="Allow (OOPS!) in commit messages",
+ action=""
+ )
+ parser.add_argument(
+ '--defaults', '--no-defaults', action="" default=None,
+ help='Do not prompt the user for defaults, always use (or do not use) them',
+ )
+
+ @classmethod
+ def main(cls, args, repository, identifier_template=None, canonical_svn=False, **kwargs):
+ if not repository.path:
+ sys.stderr.write("Cannot 'land' change in remote repository\n")
+ return 1
+
+ if not isinstance(repository, local.Git):
+ sys.stderr.write("'land' only supported by local git repositories\n")
+ return 1
+
+ if canonical_svn and not repository.is_svn:
+ sys.stderr.write("Cannot 'land' on a canonical SVN repository that is not configured as git-svn\n")
+ return 1
+
+ if not Branch.editable(repository.branch, repository=repository):
+ sys.stderr.write("Can only 'land' editable branches\n")
+ return 1
+ branch_point = Branch.branch_point(repository)
+ commits = list(repository.commits(begin=dict(hash=branch_point.hash), end=dict(branch=repository.branch)))
+ if not commits:
+ sys.stderr.write('Failed to find commits to land\n')
+ return 1
+
+ pull_request = None
+ rmt = repository.remote()
+ if rmt and rmt.pull_requests:
+ candidates = list(rmt.pull_requests.find(opened=True, head=repository.branch))
+ if len(candidates) == 1:
+ pull_request = candidates[0]
+ elif candidates:
+ sys.stderr.write("Multiple pull-request match '{}'\n".format(repository.branch))
+
+ if pull_request and args.review:
+ if pull_request.blockers:
+ sys.stderr.write("{} {} blocking landing '{}'\n".format(
+ string_utils.join([p.name for p in pull_request.blockers]),
+ 'are' if len(pull_request.blockers) > 1 else 'is',
+ pull_request,
+ ))
+ return 1
+ need_review = False
+ if pull_request.approvers:
+ review_lines = [cls.REVIEWED_BY_RE.search(commit.message) for commit in commits]
+ need_review = any([cls.OOPS_RE.search(match.group('approver')) for match in review_lines if match])
+ if need_review and (args.defaults or Terminal.choose("Set '{}' as your reviewer{}?".format(
+ string_utils.join([p.name for p in pull_request.approvers]),
+ 's' if len(pull_request.approvers) > 1 else '',
+ ), default='Yes') == 'Yes'):
+ log.warning("Setting {} as reviewer{}".format(
+ string_utils.join([p.name for p in pull_request.approvers]),
+ 's' if len(pull_request.approvers) > 1 else '',
+ ))
+ if run([
+ repository.executable(), 'filter-branch', '-f',
+ '--env-filter', "GIT_AUTHOR_DATE='{date}';GIT_COMMITTER_DATE='{date}'".format(
+ date='{} -{}'.format(int(time.time()), repository.gmtoffset())
+ ), '--msg-filter', 'sed "s/NOBODY (OO*PP*S!*)/{}/g"'.format(string_utils.join([p.name for p in pull_request.approvers])),
+ '{}...{}'.format(repository.branch, branch_point.hash),
+ ], cwd=repository.root_path, env={'FILTER_BRANCH_SQUELCH_WARNING': '1'}, capture_output=True).returncode:
+ sys.stderr.write('Failed to set reviewers\n')
+ return 1
+ commits = list(repository.commits(begin=dict(hash=branch_point.hash), end=dict(branch=repository.branch)))
+ if not commits:
+ sys.stderr.write('Failed to find commits after setting reviewers\n')
+ return 1
+
+ elif not pull_request:
+ sys.stderr.write("Failed to find pull-request associated with '{}'\n".format(repository.branch))
+
+ if not args.oops and any([cls.OOPS_RE.search(commit.message) for commit in commits]):
+ sys.stderr.write("Found '(OOPS!)' message in commit messages, please resolve before committing\n")
+ return 1
+
+ if not args.oops:
+ for line in repository.diff_lines(branch_point.hash, repository.branch):
+ if cls.OOPS_RE.search(line):
+ sys.stderr.write("Found '(OOPS!)' in commit diff, please resolve before committing\n")
+ return 1
+
+ target = pull_request.base if pull_request else branch_point.branch
+ log.warning("Rebasing '{}' from '{}' to '{}'...".format(repository.branch, branch_point.branch, target))
+ if repository.fetch(branch=target, remote=cls.REMOTE):
+ sys.stderr.write("Failed to fetch '{}' from '{}'\n".format(target, cls.REMOTE))
+ return 1
+ if repository.rebase(target=target, base=branch_point.branch, head=repository.branch):
+ sys.stderr.write("Failed to rebase '{}' onto '{}', please resolve conflicts\n".format(repository.branch, target))
+ return 1
+ log.warning("Rebased '{}' from '{}' to '{}'!".format(repository.branch, branch_point.branch, target))
+
+ if run([repository.executable(), 'branch', '-f', target, repository.branch], cwd=repository.root_path).returncode:
+ sys.stderr.write("Failed to move '{}' ref\n".format(target))
+ return 1
+
+ if identifier_template:
+ source = repository.branch
+ repository.checkout(target)
+ if Canonicalize.main(Namespace(
+ identifier=True, remote=cls.REMOTE, number=len(commits),
+ ), repository, identifier_template=identifier_template):
+ sys.stderr.write("Failed to embed identifiers to '{}'\n".format(target))
+ return 1
+ if run([repository.executable(), 'branch', '-f', source, target], cwd=repository.root_path).returncode:
+ sys.stderr.write("Failed to move '{}' ref to the canonicalized head of '{}'\n".format(source, target))
+ return -1
+
+ if canonical_svn:
+ if run([repository.executable(), 'svn', 'fetch'], cwd=repository.root_path).returncode:
+ sys.stderr.write("Failed to update subversion refs\n".format(target))
+ return 1
+ if run([repository.executable(), 'svn', 'dcommit'], cwd=repository.root_path).returncode:
+ sys.stderr.write("Failed to commit '{}' to Subversion remote\n".format(target))
+ return 1
+ run([repository.executable(), 'reset', 'HEAD~{}'.format(len(commits)), '--hard'], cwd=repository.root_path)
+
+ # Verify the mirror processed our change
+ started = time.time()
+ original = repository.find('HEAD', include_log=False, include_identifier=False)
+ latest = original
+ while original.hash == latest.hash:
+ if time.time() - started > cls.MIRROR_TIMEOUT:
+ sys.stderr.write("Timed out waiting for the git-svn mirror, '{}' landed but not closed\n".format(pull_request or repository.branch))
+ return 1
+ log.warning(' Verifying mirror processesed change')
+ time.sleep(5)
+ run([repository.executable(), 'pull'], cwd=repository.root_path)
+ original = repository.find('HEAD', include_log=False, include_identifier=False)
+
+ else:
+ if run([repository.executable(), 'push', cls.REMOTE, target], cwd=repository.root_path).returncode:
+ sys.stderr.write("Failed to push '{}' to '{}'\n".format(target, cls.REMOTE))
+ return 1
+
+ commit = repository.commit(branch=target, include_log=False)
+ if identifier_template and commit.identifier:
+ land_message = 'Landed {} ({})!'.format(identifier_template.format(commit).split(': ')[-1], commit.hash)
+ else:
+ land_message = 'Landed {}!'.format(commit.hash)
+ print(land_message)
+
+ if pull_request:
+ pull_request.comment(land_message)
+ pull_request.close()
+
+ return 0
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py (285271 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -24,7 +24,9 @@
import re
import six
import sys
+import time
+from datetime import datetime
from logging import NullHandler
from webkitscmpy import Commit, Contributor, log
@@ -40,6 +42,14 @@
GIT_SVN_REVISION = re.compile(r'^git-svn-id: \S+:\/\/.+@(?P<revision>\d+) .+-.+-.+-.+', flags=re.MULTILINE)
DEFAULT_BRANCHES = ['main', 'master', 'trunk']
+ @classmethod
+ def gmtoffset(cls):
+ if sys.version_info >= (3, 0):
+ return int(time.localtime().tm_gmtoff * 100 / (60 * 60))
+
+ ts = time.time()
+ return int((datetime.fromtimestamp(ts) - datetime.utcfromtimestamp(ts)).total_seconds() * 100 / (60 * 60))
+
def __init__(self, dev_branches=None, prod_branches=None, contributors=None, id=None):
self.dev_branches = dev_branches or self.DEV_BRANCHES
self.prod_branches = prod_branches or self.PROD_BRANCHES
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py (285271 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py 2021-11-04 18:15:20 UTC (rev 285271)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -296,7 +296,6 @@
git.commit(hash='bae5d1e9'),
git.commit(hash='1abe25b4'),
git.commit(hash='fff83bb2'),
- git.commit(hash='9b8311f2'),
]), Commit.Encoder().default(list(git.commits(begin=dict(hash='9b8311f2'), end=dict(hash='bae5d1e9')))))
def test_commits_branch(self):
@@ -307,7 +306,6 @@
git.commit(hash='621652ad'),
git.commit(hash='a30ce849'),
git.commit(hash='fff83bb2'),
- git.commit(hash='9b8311f2'),
]), Commit.Encoder().default(list(git.commits(begin=dict(argument='9b8311f2'), end=dict(argument='621652ad')))))
def test_log(self):
@@ -333,15 +331,6 @@
8th commit
git-svn-id: https://svn.example.org/repository/repository/trunk@8 268f45cc-cd09-0410-ab3c-d52691b4dbfc
-
-commit 1abe25b443e985f93b90d830e4a7e3731336af4d
-Author: Jonathan Bedard <jbed...@apple.com>
-AuthorDate: {time_b}
-Commit: Jonathan Bedard <jbed...@apple.com>
-CommitDate: {time_b}
-
- 4th commit
- git-svn-id: https://svn.example.org/repository/repository/trunk@4 268f45cc-cd09-0410-ab3c-d52691b4dbfc
'''.format(
time_a=datetime.utcfromtimestamp(1601668000 + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'),
time_b=datetime.utcfromtimestamp(1601663000 + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'),
@@ -372,15 +361,6 @@
Cherry pick
git-svn-id: https://svn.webkit.org/repository/webkit/trunk@6 268f45cc-cd09-0410-ab3c-d52691b4dbfc
git-svn-id: https://svn.example.org/repository/repository/trunk@5 268f45cc-cd09-0410-ab3c-d52691b4dbfc
-
-commit a30ce8494bf1ac2807a69844f726be4a9843ca55
-Author: Jonathan Bedard <jbed...@apple.com>
-AuthorDate: {time_c}
-Commit: Jonathan Bedard <jbed...@apple.com>
-CommitDate: {time_c}
-
- 3rd commit
- git-svn-id: https://svn.example.org/repository/repository/trunk@3 268f45cc-cd09-0410-ab3c-d52691b4dbfc
'''.format(
time_a=datetime.utcfromtimestamp(1601667000 + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'),
time_b=datetime.utcfromtimestamp(1601664000 + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'),
@@ -477,13 +457,29 @@
self.assertEqual(repo.modified(staged=True), ['added.txt', 'modified.txt'])
def test_rebase(self):
- with mocks.local.Git(self.path):
+ with mocks.local.Git(self.path), OutputCapture():
repo = local.Git(self.path)
self.assertEqual(str(repo.commit(branch='branch-a')), '2.2@branch-a')
self.assertEqual(repo.rebase(target='main', base='main', head='branch-a', recommit=False), 0)
self.assertEqual(str(repo.commit(branch='branch-a')), '5.2@branch-a')
+ def test_diff_lines(self):
+ with mocks.local.Git(self.path), OutputCapture():
+ repo = local.Git(self.path)
+ self.assertEqual(
+ ['--- a/ChangeLog', '+++ b/ChangeLog', '@@ -1,0 +1,0 @@', '+ Patch Series'],
+ list(repo.diff_lines(base='bae5d1e90999d4f916a8a15810ccfa43f37a2fd6'))
+ )
+ def test_diff_lines_identifier(self):
+ with mocks.local.Git(self.path), OutputCapture():
+ repo = local.Git(self.path)
+ self.assertEqual(
+ ['--- a/ChangeLog', '+++ b/ChangeLog', '@@ -1,0 +1,0 @@', '+ 8th commit'],
+ list(repo.diff_lines(base='3@main', head='4@main'))
+ )
+
+
class TestGitHub(testing.TestCase):
remote = 'https://github.example.com/WebKit/WebKit'
Added: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py (0 => 285272)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py 2021-11-04 18:44:52 UTC (rev 285272)
@@ -0,0 +1,439 @@
+# 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 os
+import time
+
+from webkitcorepy import OutputCapture, testing
+from webkitcorepy.mocks import Terminal as MockTerminal, Time as MockTime
+from webkitscmpy import Contributor, Commit, local, program, mocks
+
+
+def repository(path, has_oops=True, remote=None, git_svn=False):
+ branch = 'eng/example'
+ result = mocks.local.Git(path, remote=remote, git_svn=git_svn)
+ result.commits[branch] = [Commit(
+ hash='a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd',
+ identifier='3.1@{}'.format(branch),
+ timestamp=int(time.time()) - 60,
+ author=Contributor('Tim Committer', ['tcommit...@webkit.org']),
+ message='To Be Committed\n\nReviewed by {}.\n'.format(
+ 'NOBODY (OOPS!)' if has_oops else 'Ricky Reviewer',
+ ),
+ )]
+ result.head = result.commits[branch][0]
+ return result
+
+
+class TestLand(testing.PathTestCase):
+ basepath = 'mock/repository'
+
+ def setUp(self):
+ super(TestLand, self).setUp()
+ os.mkdir(os.path.join(self.path, '.git'))
+ os.mkdir(os.path.join(self.path, '.svn'))
+
+ def test_non_editable(self):
+ with OutputCapture() as captured, mocks.local.Git(self.path), mocks.local.Svn():
+ self.assertEqual(1, program.main(
+ args=('land',),
+ path=self.path,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '5@main')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual([line for line in log if 'Mock process' not in line], [])
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Can only 'land' editable branches\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), '')
+
+ def test_with_oops(self):
+ with OutputCapture() as captured, repository(self.path), mocks.local.Svn():
+ self.assertEqual(1, program.main(
+ args=('land',),
+ path=self.path,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '3.1@eng/example')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Failed to find pull-request associated with 'eng/example'\n"
+ "Found '(OOPS!)' message in commit messages, please resolve before committing\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), '')
+
+ def test_default(self):
+ with OutputCapture() as captured, repository(self.path, has_oops=False), mocks.local.Svn():
+ self.assertEqual(0, program.main(
+ args=('land',),
+ path=self.path,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '6@main')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ "Rebasing 'eng/example' from 'main' to 'main'...",
+ "Rebased 'eng/example' from 'main' to 'main'!",
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Failed to find pull-request associated with 'eng/example'\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), 'Landed a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd!\n')
+
+ def test_canonicalize(self):
+ with OutputCapture() as captured, repository(self.path, has_oops=False), mocks.local.Svn():
+ self.assertEqual(0, program.main(
+ args=('land',),
+ path=self.path,
+ identifier_template='Canonical link: https://commits.webkit.org/{}',
+ ))
+
+ commit = local.Git(self.path).commit(branch='main')
+ self.assertEqual(str(commit), '6@main')
+ self.assertEqual(
+ commit.message,
+ 'To Be Committed\n\n'
+ 'Reviewed by Ricky Reviewer.\n\n'
+ 'Canonical link: https://commits.webkit.org/6@main',
+ )
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ "Rebasing 'eng/example' from 'main' to 'main'...",
+ "Rebased 'eng/example' from 'main' to 'main'!",
+ '1 commit to be editted...',
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Failed to find pull-request associated with 'eng/example'\n",
+ )
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ 'Rewrite a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd (1/1) (--- seconds passed, remaining --- predicted)\n'
+ '1 commit successfully canonicalized!\n'
+ 'Landed https://commits.webkit.org/6@main (a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd)!\n',
+ )
+
+ def test_no_svn_canonical_svn(self):
+ with OutputCapture() as captured, repository(self.path, has_oops=False), mocks.local.Svn():
+ self.assertEqual(1, program.main(
+ args=('land',),
+ path=self.path, canonical_svn=True,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '3.1@eng/example')
+
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Cannot 'land' on a canonical SVN repository that is not configured as git-svn\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), '')
+
+ def test_svn(self):
+ self.maxDiff = None
+ with MockTime, OutputCapture() as captured, repository(self.path, has_oops=False, git_svn=True), mocks.local.Svn():
+ self.assertEqual(0, program.main(
+ args=('land',),
+ path=self.path, canonical_svn=True,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '6@main')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ "Rebasing 'eng/example' from 'main' to 'main'...",
+ "Rebased 'eng/example' from 'main' to 'main'!",
+ ' Verifying mirror processesed change',
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Failed to find pull-request associated with 'eng/example'\n",
+ )
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ 'Landed a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd!\n',
+ )
+
+
+class TestLandGitHub(testing.PathTestCase):
+ basepath = 'mock/repository'
+
+ def setUp(self):
+ super(TestLandGitHub, self).setUp()
+ os.mkdir(os.path.join(self.path, '.git'))
+ os.mkdir(os.path.join(self.path, '.svn'))
+
+ @classmethod
+ def webserver(cls, approved=None):
+ result = mocks.remote.GitHub()
+ result.users = dict(
+ rreviewer=Contributor('Ricky Reviewer', ['rrevie...@webkit.org'], github='rreviewer'),
+ tcontributor=Contributor('Tim Contributor', ['tcontribu...@webkit.org'], github='tcontributor'),
+ )
+ result.issues = {
+ 1: dict(
+ comments=[],
+ assignees=[],
+ )
+ }
+ result.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='username:eng/example'),
+ base=dict(ref='main'),
+ requested_reviews=[dict(login='rreviewer')],
+ reviews=[
+ dict(user=dict(login='rreviewer'), state='APPROVED')
+ ] if approved else [] + [
+ dict(user=dict(login='rreviewer'), state='CHANGES_REQUESTED')
+ ] if approved is not None else [], _links=dict(
+ issue=dict(href=''.format(result.api_remote)),
+ ),
+ )]
+ return result
+
+ def test_no_reviewer(self):
+ with OutputCapture() as captured, self.webserver() as remote, \
+ repository(self.path, remote='https://{}'.format(remote.remote)), mocks.local.Svn():
+
+ self.assertEqual(1, program.main(
+ args=('land',),
+ path=self.path,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '3.1@eng/example')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Found '(OOPS!)' message in commit messages, please resolve before committing\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), '')
+
+ def test_blocking_reviewer(self):
+ with OutputCapture() as captured, self.webserver(approved=False) as remote, \
+ repository(self.path, has_oops=False, remote='https://{}'.format(remote.remote)), mocks.local.Svn():
+
+ self.assertEqual(1, program.main(
+ args=('land',),
+ path=self.path,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '3.1@eng/example')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Ricky Reviewer is blocking landing 'PR 1 | Example Change'\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), '')
+
+ def test_insert_review(self):
+ with OutputCapture() as captured, MockTerminal.input('y'), self.webserver(approved=True) as remote, \
+ repository(self.path, has_oops=True, remote='https://{}'.format(remote.remote)), mocks.local.Svn():
+ self.assertEqual(0, program.main(
+ args=('land',),
+ path=self.path,
+ ))
+
+ repo = local.Git(self.path)
+ self.assertEqual(str(repo.commit()), '6@main')
+ self.assertEqual(
+ [comment.content for comment in repo.remote().pull_requests.get(1).comments],
+ ['Landed a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd!'],
+ )
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ 'Setting Ricky Reviewer as reviewer',
+ "Rebasing 'eng/example' from 'main' to 'main'...",
+ "Rebased 'eng/example' from 'main' to 'main'!",
+ ],
+ )
+ self.assertEqual(captured.stderr.getvalue(), '')
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ "Set 'Ricky Reviewer' as your reviewer? (Yes/No): \n"
+ 'Landed a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd!\n',
+ )
+
+
+class TestLandBitBucket(testing.PathTestCase):
+ basepath = 'mock/repository'
+
+ def setUp(self):
+ super(TestLandBitBucket, self).setUp()
+ os.mkdir(os.path.join(self.path, '.git'))
+ os.mkdir(os.path.join(self.path, '.svn'))
+
+ @classmethod
+ def webserver(cls, approved=None):
+ result = mocks.remote.BitBucket()
+ result.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=True if approved else False,
+ status='NEEDS_WORK' if approved is False else None,
+ ),
+ ],
+ )]
+ return result
+
+ def test_no_reviewer(self):
+ with OutputCapture() as captured, self.webserver() as remote, repository(
+ 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=('land',),
+ path=self.path,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '3.1@eng/example')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Found '(OOPS!)' message in commit messages, please resolve before committing\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), '')
+
+ def test_blocking_reviewer(self):
+ with OutputCapture() as captured, self.webserver(approved=False) as remote, repository(
+ self.path, has_oops=False, 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=('land',),
+ path=self.path,
+ ))
+ self.assertEqual(str(local.Git(self.path).commit()), '3.1@eng/example')
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ ],
+ )
+ self.assertEqual(
+ captured.stderr.getvalue(),
+ "Ricky Reviewer is blocking landing 'PR 1 | Example Change'\n",
+ )
+ self.assertEqual(captured.stdout.getvalue(), '')
+
+ def test_insert_review(self):
+ with OutputCapture() as captured, MockTerminal.input('y'), self.webserver(approved=True) as remote, repository(
+ self.path, has_oops=True, remote='ssh://git@{}/{}/{}.git'.format(
+ remote.hosts[0], remote.project.split('/')[1], remote.project.split('/')[3],
+ )), mocks.local.Svn():
+
+ self.assertEqual(0, program.main(
+ args=('land',),
+ path=self.path,
+ ))
+ repo = local.Git(self.path)
+ self.assertEqual(str(repo.commit()), '6@main')
+ self.assertEqual(
+ [comment.content for comment in repo.remote().pull_requests.get(1).comments],
+ ['Landed a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd!'],
+ )
+
+ log = captured.root.log.getvalue().splitlines()
+ self.assertEqual(
+ [line for line in log if 'Mock process' not in line], [
+ ' Found 1 commit...',
+ 'Setting Ricky Reviewer as reviewer',
+ "Rebasing 'eng/example' from 'main' to 'main'...",
+ "Rebased 'eng/example' from 'main' to 'main'!",
+ ],
+ )
+ self.assertEqual(captured.stderr.getvalue(), '')
+ self.assertEqual(
+ captured.stdout.getvalue(),
+ "Set 'Ricky Reviewer' as your reviewer? (Yes/No): \n"
+ 'Landed a5fe8afe9bf7d07158fcd9e9732ff02a712db2fd!\n',
+ )