Title: [285272] trunk/Tools
Revision
285272
Author
jbed...@apple.com
Date
2021-11-04 11:44:52 -0700 (Thu, 04 Nov 2021)

Log Message

[git-webkit] Add land command
https://bugs.webkit.org/show_bug.cgi?id=231821
<rdar://problem/84309339>

Reviewed by Dewei Zhu.

* Tools/Scripts/git-webkit: WebKit still uses Svn as it's canonical source of truth.
* Tools/Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Tools/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.
* Tools/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~#`.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/program/__init__.py:
(main): Add land command.
* Tools/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.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py:
(ScmBase.gmtoffset): Add shared computation of GMT offset.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
(TestGit.test_commits): `git log` should exclude base ref.
(TestGit.test_commits_branch): Ditto.
* Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/land_unittest.py: Added.
(repository):
(TestLand):
(TestLandGitHub):
(TestLandBitBucket):

Canonical link: https://commits.webkit.org/243883@main

Modified Paths

Added Paths

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',
+        )
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to