<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[286576] trunk/Tools</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta">
<dt>Revision</dt> <dd><a href="http://trac.webkit.org/projects/webkit/changeset/286576">286576</a></dd>
<dt>Author</dt> <dd>jbedard@apple.com</dd>
<dt>Date</dt> <dd>2021-12-06 16:18:20 -0800 (Mon, 06 Dec 2021)</dd>
</dl>

<h3>Log Message</h3>
<pre>[reporelaypy] Add implementation of commits.webkit.org
https://bugs.webkit.org/show_bug.cgi?id=233734
<rdar://problem/85945101>

Reviewed by Dewei Zhu.

As commits.webkit.org becomes more important to WebKit development, we need to apply
our normal robust testing and review to the code backing it.

* Tools/Scripts/libraries/reporelaypy/reporelaypy/__init__.py: Add entrypoint to library.
* Tools/Scripts/libraries/reporelaypy/reporelaypy/checkout.py: Added.
(Checkout): Abstraction managing local repository checkout.
* Tools/Scripts/libraries/reporelaypy/reporelaypy/checkoutroute.py: Added.
(Redirector): Generates redirects for certain SCM services.
(CheckoutRoute): http routes returning information based on a checkout.
* Tools/Scripts/libraries/reporelaypy/reporelaypy/database.py: Added.
(Database): A connection to a true Redis or TinyRedis database.
* Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkout_unittest.py:
(CheckoutUnittest):
* Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkoutroute_unittest.py: Added.
(RedirectorUnittest):
(CheckoutRouteUnittest):
* Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/database_unittest.py: Added.
(DatabaseUnittest):
* Tools/Scripts/libraries/reporelaypy/reporelaypy/webserver.py: Copied from Tools/Scripts/libraries/webkitflaskpy/setup.py.
(health):
* Tools/Scripts/libraries/reporelaypy/run: Added.
* Tools/Scripts/libraries/reporelaypy/setup.py: Added.
* Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py: Bump version.
* Tools/Scripts/libraries/resultsdbpy/resultsdbpy/flask_support/flask_testcase.py:
(FlaskTestCase.run_with_mock_webserver.decorator.real_method):
(FlaskRequestsResponse): Moved to webkitflaskpy.
* Tools/Scripts/libraries/webkitflaskpy/setup.py: Bump version, update library description.
* Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/__init__.py: Bump version.
* Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/mock_app.py: Added.
(mock_app): Setup a flask app for testing.
* Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/response.py: Added.
(Response): Move FlaskRequestsResponse to more generic location.
* Tools/Scripts/webkitpy/__init__.py: Include reporelaypy for testing.
* Tools/Scripts/webkitpy/test/main.py:
(main): Include reporelaypy for testing.
(Tester._run_tests): Ditto.

Canonical link: https://commits.webkit.org/244901@main</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunkToolsChangeLog">trunk/Tools/ChangeLog</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpy__init__py">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpyresultsdbpyflask_supportflask_testcasepy">trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/flask_support/flask_testcase.py</a></li>
<li><a href="#trunkToolsScriptslibrariesresultsdbpysetuppy">trunk/Tools/Scripts/libraries/resultsdbpy/setup.py</a></li>
<li><a href="#trunkToolsScriptslibrarieswebkitflaskpysetuppy">trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py</a></li>
<li><a href="#trunkToolsScriptslibrarieswebkitflaskpywebkitflaskpy__init__py">trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/__init__.py</a></li>
<li><a href="#trunkToolsScriptswebkitpy__init__py">trunk/Tools/Scripts/webkitpy/__init__.py</a></li>
<li><a href="#trunkToolsScriptswebkitpytestmainpy">trunk/Tools/Scripts/webkitpy/test/main.py</a></li>
</ul>

<h3>Added Paths</h3>
<ul>
<li>trunk/Tools/Scripts/libraries/reporelaypy/</li>
<li>trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/</li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypy__init__py">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/__init__.py</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypycheckoutpy">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkout.py</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypycheckoutroutepy">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkoutroute.py</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypydatabasepy">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/database.py</a></li>
<li>trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/</li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypytestscheckout_unittestpy">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkout_unittest.py</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypytestscheckoutroute_unittestpy">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkoutroute_unittest.py</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypytestsdatabase_unittestpy">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/database_unittest.py</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyreporelaypywebserverpy">trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/webserver.py</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypyrun">trunk/Tools/Scripts/libraries/reporelaypy/run</a></li>
<li><a href="#trunkToolsScriptslibrariesreporelaypysetuppy">trunk/Tools/Scripts/libraries/reporelaypy/setup.py</a></li>
<li><a href="#trunkToolsScriptslibrarieswebkitflaskpywebkitflaskpymock_apppy">trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/mock_app.py</a></li>
<li><a href="#trunkToolsScriptslibrarieswebkitflaskpywebkitflaskpyresponsepy">trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/response.py</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunkToolsChangeLog"></a>
<div class="modfile"><h4>Modified: trunk/Tools/ChangeLog (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/ChangeLog    2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/ChangeLog       2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -1,3 +1,48 @@
</span><ins>+2021-12-04  Jonathan Bedard  <jbedard@apple.com>
+
+        [reporelaypy] Add implementation of commits.webkit.org
+        https://bugs.webkit.org/show_bug.cgi?id=233734
+        <rdar://problem/85945101>
+
+        Reviewed by Dewei Zhu.
+
+        As commits.webkit.org becomes more important to WebKit development, we need to apply
+        our normal robust testing and review to the code backing it.
+
+        * Scripts/libraries/reporelaypy/reporelaypy/__init__.py: Add entrypoint to library.
+        * Scripts/libraries/reporelaypy/reporelaypy/checkout.py: Added.
+        (Checkout): Abstraction managing local repository checkout.
+        * Scripts/libraries/reporelaypy/reporelaypy/checkoutroute.py: Added.
+        (Redirector): Generates redirects for certain SCM services.
+        (CheckoutRoute): http routes returning information based on a checkout.
+        * Scripts/libraries/reporelaypy/reporelaypy/database.py: Added.
+        (Database): A connection to a true Redis or TinyRedis database.
+        * Scripts/libraries/reporelaypy/reporelaypy/tests/checkout_unittest.py:
+        (CheckoutUnittest):
+        * Scripts/libraries/reporelaypy/reporelaypy/tests/checkoutroute_unittest.py: Added.
+        (RedirectorUnittest):
+        (CheckoutRouteUnittest):
+        * Scripts/libraries/reporelaypy/reporelaypy/tests/database_unittest.py: Added.
+        (DatabaseUnittest):
+        * Scripts/libraries/reporelaypy/reporelaypy/webserver.py: Copied from Tools/Scripts/libraries/webkitflaskpy/setup.py.
+        (health):
+        * Scripts/libraries/reporelaypy/run: Added.
+        * Scripts/libraries/reporelaypy/setup.py: Added.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py: Bump version.
+        * Scripts/libraries/resultsdbpy/resultsdbpy/flask_support/flask_testcase.py:
+        (FlaskTestCase.run_with_mock_webserver.decorator.real_method):
+        (FlaskRequestsResponse): Moved to webkitflaskpy.
+        * Scripts/libraries/webkitflaskpy/setup.py: Bump version, update library description.
+        * Scripts/libraries/webkitflaskpy/webkitflaskpy/__init__.py: Bump version.
+        * Scripts/libraries/webkitflaskpy/webkitflaskpy/mock_app.py: Added.
+        (mock_app): Setup a flask app for testing.
+        * Scripts/libraries/webkitflaskpy/webkitflaskpy/response.py: Added.
+        (Response): Move FlaskRequestsResponse to more generic location.
+        * Scripts/webkitpy/__init__.py: Include reporelaypy for testing.
+        * Scripts/webkitpy/test/main.py:
+        (main): Include reporelaypy for testing.
+        (Tester._run_tests): Ditto.
+
</ins><span class="cx"> 2021-12-06  Ryan Haddad  <ryanhaddad@apple.com>
</span><span class="cx"> 
</span><span class="cx">         REGRESSION (r286507): [macOS] Many file system access layout tests became flaky failures
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypy__init__pyfromrev286574trunkToolsScriptslibrarieswebkitflaskpywebkitflaskpy__init__py"></a>
<div class="copfile"><h4>Copied: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/__init__.py (from rev 286574, trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/__init__.py) (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/__init__.py                                (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/__init__.py   2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,59 @@
</span><ins>+# 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 sys
+
+
+def _maybe_add_webkit_python_library_paths():
+    # Hopefully we're beside webkit*py libraries, otherwise webkit*py will need to be installed.
+    libraries_path = os.path.dirname(os.path.dirname(os.path.abspath(os.path.dirname(__file__))))
+    for library in ['webkitcorepy', 'webkitscmpy', 'webkitflaskpy']:
+        library_path = os.path.join(libraries_path, library)
+        if os.path.isdir(library_path) and os.path.isdir(os.path.join(library_path, library)) and library_path not in sys.path:
+            sys.path.insert(0, library_path)
+
+
+_maybe_add_webkit_python_library_paths()
+
+try:
+    from webkitcorepy import AutoInstall, Package, Version
+except ImportError:
+    raise ImportError(
+        "'webkitcorepy' could not be found on your Python path.\n" +
+        "You are not running from a WebKit checkout.\n" +
+        "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
+    )
+
+version = Version(0, 1, 0)
+
+import webkitflaskpy
+
+from reporelaypy.checkout import Checkout
+from reporelaypy.database import Database
+from reporelaypy.checkoutroute import CheckoutRoute, Redirector
+
+AutoInstall.register(Package('fakeredis', Version(1, 5, 2)))
+AutoInstall.register(Package('hiredis', Version(1, 1, 0)))
+AutoInstall.register(Package('redis', Version(3, 5, 3)))
+
+name = 'reporelaypy'
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypycheckoutpy"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkout.py (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkout.py                                (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkout.py   2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,204 @@
</span><ins>+# 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 json
+import multiprocessing
+import os
+import shutil
+import sys
+
+from webkitcorepy import run
+from webkitscmpy import local
+
+
+class Checkout(object):
+    class Exception(RuntimeError):
+        pass
+
+    class Encoder(json.JSONEncoder):
+        def default(self, obj):
+            if isinstance(obj, dict):
+                return {key: self.default(value) for key, value in obj.items()}
+            if isinstance(obj, list):
+                return [self.default(value) for value in obj]
+            if not isinstance(obj, Checkout):
+                return super(Checkout.Encoder, self).default(obj)
+
+            return dict(
+                path=obj.path,
+                url=obj.url,
+                sentinal=obj.sentinal,
+            )
+
+    @classmethod
+    def from_json(cls, data):
+        data = data if isinstance(data, dict) else json.loads(data)
+        if isinstance(data, list):
+            return [cls(**node, primary=False) for node in data]
+        return cls(**data, primary=False)
+
+    @staticmethod
+    def clone(url, path, sentinal_file=None):
+        run([local.Git.executable(), 'clone', url, path], cwd=os.path.dirname(path))
+        run([local.Git.executable(), 'config', 'pull.ff', 'only'], cwd=path)
+
+        if sentinal_file:
+            with open(sentinal_file, 'w') as cloned:
+                cloned.write('yes\n')
+        return 0
+
+    def __init__(self, path, url=None, http_proxy=None, sentinal=True, primary=True):
+        self.sentinal = sentinal
+        self.path = path
+        self.url = url
+        self._repository = None
+        self._child_process = None
+
+        containing_path = os.path.dirname(path)
+        if not os.path.isdir(containing_path):
+            raise self.Exception("Containing path '{}' does not exist".format(containing_path))
+
+        if http_proxy and run([
+            local.Git.executable(), 'config', '--global', 'http.proxy', 'http://{}'.format(http_proxy),
+        ]).returncode:
+            split_proxy = http_proxy.split('@')
+            raise self.Exception("Failed to set https proxy to '{}'".format(
+                '{}@{}'.format('*' * len(split_proxy[0]), split_proxy[1]) if '@' in http_proxy else http_proxy,
+            ))
+
+        try:
+            if self.repository:
+                if not self.url:
+                    self.url = self.repository.url(name='origin')
+                if self.url and self.repository.url(name='origin') != self.url:
+                    sys.stderr.write("Specified '{}' as the URL, but the specified path is from '{}'\n".format(
+                        self.url, self.repository.url(name='origin'),
+                    ))
+                return
+        except FileNotFoundError:
+            pass
+
+        if not primary:
+            return
+
+        if not self.url:
+            raise self.Exception('No url provided to clone from')
+
+        if os.path.isfile(self.sentinal_file):
+            os.remove(self.sentinal_file)
+        if os.path.isdir(path):
+            shutil.rmtree(path, ignore_errors=True)
+
+        if self.sentinal:
+            self._child_process = multiprocessing.Process(
+                target=self.clone,
+                args=(self.url, path, self.sentinal_file),
+            )
+            self._child_process.start()
+        else:
+            self.clone(self.url, path)
+
+    @property
+    def sentinal_file(self):
+        return os.path.join(os.path.dirname(self.path), 'cloned')
+
+    @property
+    def repository(self):
+        if self._repository:
+            return self._repository
+        if os.path.isfile(self.sentinal_file) or not self.sentinal:
+            if self._child_process:
+                self._child_process.join()
+                self._child_process = None
+            self._repository = local.Git(self.path)
+            return self._repository
+        return None
+
+    def is_updated(self, branch, remote='origin'):
+        if not self.repository:
+            sys.stderr.write('Cannot query checkout, clone still pending...\n')
+            return None
+
+        result = run(
+            [self.repository.executable(), 'show-ref', branch],
+            cwd=self.repository.root_path,
+            capture_output=True,
+            encoding='utf-8',
+        )
+        if result.returncode:
+            return False
+        ref = None
+        for line in result.stdout.splitlines():
+            if line.split()[-1].startswith('refs/heads'):
+                ref = line.split()[0]
+                break
+        if not ref:
+            return False
+        for line in result.stdout.splitlines():
+            if line.split()[-1].startswith('refs/remotes/{}'.format(remote)):
+                return ref == line.split()[0]
+        return False
+
+    def update_for(self, branch=None, remote='origin'):
+        if not self.repository:
+            sys.stderr.write("Cannot update '{}', clone still pending...\n".format(branch))
+            return None
+
+        branch = branch or self.repository.default_branch
+        if branch == self.repository.default_branch:
+            self.repository.pull(remote=remote)
+            self.repository.cache.populate(branch=branch)
+            return True
+        if not self.repository.prod_branches.match(branch):
+            return False
+        if self.is_updated(branch, remote=remote):
+            return True
+
+        run(
+            [self.repository.executable(), 'fetch', remote, '{}:{}'.format(branch, branch)],
+            cwd=self.repository.root_path,
+        )
+        self.repository.cache.populate(branch=branch)
+        return True
+
+    def update_all(self, remote='origin'):
+        if not self.repository:
+            sys.stderr.write("Cannot update checkout, clone still pending...\n")
+            return None
+
+        self.repository.pull(remote=remote)
+        self.repository.cache.populate(branch=self.repository.default_branch)
+
+        # First, update all branches we're already tracking
+        all_branches = set(self.repository.branches_for(remote=remote))
+        for branch in self.repository.branches_for(remote=False):
+            if branch in all_branches:
+                all_branches.remove(branch)
+            self.update_for(branch=branch, remote=remote)
+
+        # Then, track all untracked branches
+        for branch in all_branches:
+            run(
+                [self.repository.executable(), 'branch', '--track', branch, 'remotes/{}/{}'.format(remote, branch)],
+                cwd=self.repository.root_path,
+            )
+            self.repository.cache.populate(branch=branch)
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypycheckoutroutepy"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkoutroute.py (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkoutroute.py                           (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/checkoutroute.py      2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,189 @@
</span><ins>+# 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 json
+import re
+
+from flask import abort, current_app, json as fjson, redirect, Flask, Response
+from reporelaypy import Database
+from webkitflaskpy import AuthedBlueprint
+from webkitscmpy import Commit, remote
+
+
+class Redirector(object):
+    class Encoder(json.JSONEncoder):
+        def default(self, obj):
+            if isinstance(obj, dict):
+                return {key: self.default(value) for key, value in obj.items()}
+            if isinstance(obj, list):
+                return [self.default(value) for value in obj]
+            if not isinstance(obj, Redirector):
+                return super(Checkout.Encoder, self).default(obj)
+
+            return dict(
+                name=obj.name,
+                url=obj.url,
+            )
+
+    @classmethod
+    def bitbucket_generator(cls, base):
+        def redirector(commit):
+            if commit and commit.hash:
+                return redirect('{}/commits/{}'.format(base, commit.hash))
+            return redirect('{}/commits'.format(base))
+
+        return redirector
+
+    @classmethod
+    def trac_generator(cls, base):
+        def redirector(commit):
+            if commit and commit.revision:
+                return redirect('{}/changeset/{}/webkit'.format(base, commit.revision))
+            return redirect(base)
+
+        return redirector
+
+    @classmethod
+    def github_generator(cls, base):
+        def redirector(commit):
+            if commit and commit.hash:
+                return redirect('{}/commit/{}'.format(base, commit.hash))
+            return redirect('{}/commits'.format(base))
+
+        return redirector
+
+    @classmethod
+    def from_json(cls, data):
+        data = data if isinstance(data, dict) else json.loads(data)
+        if isinstance(data, list):
+            return [cls(**node) for node in data]
+        return cls(**data)
+
+    def __init__(self, url, name=None):
+        self.url = url
+        self.type = None
+        self._redirect = None
+
+        for key, params in dict(
+            bitbucket=(remote.BitBucket.URL_RE, self.bitbucket_generator),
+            trac=(re.compile(r'\Ahttps?://trac.(?P<domain>\S+)\Z'), self.trac_generator),
+            github=(remote.GitHub.URL_RE, self.github_generator),
+        ).items():
+            regex, generator = params
+            if regex.match(url):
+                self.type = key
+                self._redirect = generator(url)
+                break
+        if not self.type:
+            raise TypeError("'{}' is not a recognized redirect base")
+        self.name = name or self.type
+
+    def __call__(self, commit):
+        if not self._redirect:
+            abort(Response("No valid redirect for '{}'".format(self.url), status=500))
+        return self._redirect(commit)
+
+
+class CheckoutRoute(AuthedBlueprint):
+    def __init__(self, checkout, redirectors=None, import_name=__name__, auth_decorator=None, database=None):
+        super(CheckoutRoute, self).__init__('checkout', import_name, url_prefix=None, auth_decorator=auth_decorator)
+
+        self.checkout = checkout
+        self.database = database or Database()
+
+        if not redirectors:
+            redirectors = [Redirector(self.checkout.url)]
+        self.redirectors = redirectors
+
+        self.add_url_rule('/', 'landing', lambda: self.redirectors[0](None), methods=('GET',))
+        self.add_url_rule(
+            '/<path:ref>', 'redirect',
+            lambda ref: self.redirectors[0](self.commit(ref)),
+            methods=('GET',),
+        )
+        for redirector in self.redirectors:
+            self.add_url_rule(
+                '/{}'.format(redirector.name), 'landing-{}'.format(redirector.name),
+                lambda redirector=redirector: redirector(None),
+                methods=('GET',),
+            )
+            self.add_url_rule(
+                '/<path:ref>/{}'.format(redirector.name), 'redirect-{}'.format(redirector.name),
+                lambda ref, redirector=redirector: redirector(self.commit(ref)), methods=('GET',),
+            )
+
+        self.add_url_rule('/<path:ref>/json', 'api', self.api, methods=('GET',))
+        self.add_url_rule('/changeset/<path:revision>/webkit', 'trac', self.trac, methods=('GET',))
+
+    def commit(self, ref=None):
+        try:
+            retrieved = self.database.get(ref)
+            if retrieved:
+                return Commit.from_json(retrieved)
+        except (redis.exceptions.ResponseError, TypeError, ValueError) as e:
+            sys.stderr.write('{}\n'.format(e))
+
+        if not self.checkout.repository:
+            return None
+
+        commit = self.checkout.repository.find(ref) if ref else self.checkout.repository.commit()
+        if not commit:
+            return commit
+
+        encoded = json.dumps(commit, cls=Commit.Encoder)
+        self.database.set(commit.hash, encoded)
+        self.database.set('r{}'.format(commit.revision), encoded)
+        self.database.set(str(commit), encoded)
+
+        return commit
+
+    def api(self, ref):
+        try:
+            return current_app.response_class(
+                fjson.dumps(Commit.Encoder().default(self.commit(ref)), indent=4),
+                mimetype='application/json',
+            )
+
+        except BaseException as e:
+            return current_app.response_class(
+                fjson.dumps(dict(
+                    status='Not Found',
+                    message='No commit with the specified reference could be found',
+                    error=str(e),
+                ), indent=4),
+                mimetype='application/json',
+                status=404,
+            )
+
+    def trac(self, revision):
+        try:
+            return self.redirectors[0](self.commit('r{}'.format(int(revision))))
+        except BaseException as e:
+            return current_app.response_class(
+                fjson.dumps(dict(
+                    status='Server Error',
+                    message='Failed to digest trac link',
+                    error=str(e),
+                ), indent=4),
+                mimetype='application/json',
+                status=500,
+            )
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypydatabasepy"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/database.py (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/database.py                                (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/database.py   2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,76 @@
</span><ins>+# 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.
+
+from webkitcorepy import Environment
+
+
+class Database(object):
+    _instance = None
+    HOST_ENV = 'REDIS_HOST'
+    PASSWORD_ENV = 'REDIS_PASSWORD'
+    EXPIRATION_ENV = 'REDIS_DEFAULT_EXPIRATION'
+    DEFAULT_EXPIRATION = 60 * 60 * 24
+
+    @classmethod
+    def instance(cls, **kwargs):
+        if not cls._instance:
+            cls._instance = cls(**kwargs)
+        return cls._instance
+
+    def __init__(self, host=None, password=None, partition=None, default_expiration=None):
+        self.partition = partition or ''
+        if default_expiration is None:
+            self.default_expiration = int(Environment.instance().get(self.EXPIRATION_ENV) or self.DEFAULT_EXPIRATION)
+        else:
+            self.default_expiration = int(default_expiration)
+
+        self.host = host or Environment.instance().get(self.HOST_ENV)
+        self.password = password or Environment.instance().get(self.PASSWORD_ENV)
+
+        if host:
+            import redis
+            self._redis = redis.Redis(host=host, password=password)
+        else:
+            import fakeredis
+            self._redis = fakeredis.FakeStrictRedis()
+
+    def ping(self):
+        return self._redis.ping()
+
+    def get(self, name):
+        return self._redis.get(self.partition + name)
+
+    def set(self, name, value, **kwargs):
+        if 'ex' not in kwargs and self.default_expiration:
+            kwargs['ex'] = self.default_expiration
+        return self._redis.set(self.partition + name, value, **kwargs)
+
+    def lock(self, name, **kwargs):
+        return self._redis.lock(self.partition + name, **kwargs)
+
+    def delete(self, *names):
+        names_to_delete = [self.partition + name for name in names]
+        return self._redis.delete(*names_to_delete)
+
+    def scan_iter(self, match=None, **kwargs):
+        for key in self._redis.scan_iter(match=self.partition + match, **kwargs):
+            yield key[len(self.partition):]
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypytestscheckout_unittestpyfromrev286574trunkToolsScriptslibrarieswebkitflaskpysetuppy"></a>
<div class="copfile"><h4>Copied: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkout_unittest.py (from rev 286574, trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py) (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkout_unittest.py                         (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkout_unittest.py    2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,49 @@
</span><ins>+# 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 unittest
+
+from reporelaypy import Checkout
+from webkitcorepy import testing, OutputCapture
+from webkitscmpy import mocks
+
+
+class CheckoutUnittest(testing.PathTestCase):
+    basepath = 'mock/repository'
+
+    def setUp(self):
+        super(CheckoutUnittest, self).setUp()
+        os.mkdir(os.path.join(self.path, '.git'))
+
+    def test_constructor_sentinal(self):
+        with mocks.local.Git(self.path) as repo, OutputCapture():
+            with open(os.path.join(os.path.dirname(self.path), 'cloned'), 'w') as sentinal:
+                sentinal.write('yes\n')
+            Checkout(path=self.path, url=repo.remote, sentinal=True)
+
+    def test_constructor_no_sentinal(self):
+        with mocks.local.Git(self.path) as repo:
+            Checkout(path=self.path, url=repo.remote, sentinal=False)
+
+            with self.assertRaises(Checkout.Exception):
+                Checkout(path=self.path, url=None, sentinal=True)
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypytestscheckoutroute_unittestpy"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkoutroute_unittest.py (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkoutroute_unittest.py                            (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/checkoutroute_unittest.py       2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,176 @@
</span><ins>+# 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 json
+import unittest
+
+from reporelaypy import Checkout, CheckoutRoute, Redirector
+from webkitcorepy import testing, OutputCapture
+from webkitflaskpy import mock_app
+from webkitscmpy import mocks, Commit
+
+
+class RedirectorUnittest(unittest.TestCase):
+    def test_empty(self):
+        self.assertEqual(
+            [],
+            Redirector.from_json(json.dumps([], cls=Redirector.Encoder)),
+        )
+
+    def test_bitbucket(self):
+        redir = Redirector('https://bitbucket.webkit.org/projects/WEBKIT/repos/webkit')
+        self.assertEqual(redir.type, 'bitbucket')
+        self.assertEqual(
+            redir(None).headers.get('location'),
+            'https://bitbucket.webkit.org/projects/WEBKIT/repos/webkit/commits',
+        )
+        self.assertEqual(
+            redir(Commit(hash='deadbeef')).headers.get('location'),
+            'https://bitbucket.webkit.org/projects/WEBKIT/repos/webkit/commits/deadbeef',
+        )
+
+        transfered = Redirector.from_json(json.dumps([redir], cls=Redirector.Encoder))[0]
+        self.assertEqual(redir.type, transfered.type)
+        self.assertEqual(redir(None).headers.get('location'), transfered(None).headers.get('location'))
+        self.assertEqual(
+            redir(Commit(hash='deadbeef')).headers.get('location'),
+            transfered(Commit(hash='deadbeef')).headers.get('location'),
+        )
+
+    def test_trac(self):
+        redir = Redirector('https://trac.webkit.org')
+        self.assertEqual(redir.type, 'trac')
+        self.assertEqual(
+            redir(None).headers.get('location'),
+            'https://trac.webkit.org',
+        )
+        self.assertEqual(
+            redir(Commit(revision=12345)).headers.get('location'),
+            'https://trac.webkit.org/changeset/12345/webkit',
+        )
+
+        transfered = Redirector.from_json(json.dumps([redir], cls=Redirector.Encoder))[0]
+        self.assertEqual(redir.type, transfered.type)
+        self.assertEqual(redir(None).headers.get('location'), transfered(None).headers.get('location'))
+        self.assertEqual(
+            redir(Commit(revision=12345)).headers.get('location'),
+            transfered(Commit(revision=12345)).headers.get('location'),
+        )
+
+    def test_github(self):
+        redir = Redirector('https://github.com/WebKit/WebKit')
+        self.assertEqual(redir.type, 'github')
+        self.assertEqual(
+            redir(None).headers.get('location'),
+            'https://github.com/WebKit/WebKit/commits',
+        )
+        self.assertEqual(
+            redir(Commit(hash='deadbeef')).headers.get('location'),
+            'https://github.com/WebKit/WebKit/commit/deadbeef',
+        )
+
+        transfered = Redirector.from_json(json.dumps([redir], cls=Redirector.Encoder))[0]
+        self.assertEqual(redir.type, transfered.type)
+        self.assertEqual(redir(None).headers.get('location'), transfered(None).headers.get('location'))
+        self.assertEqual(
+            redir(Commit(hash='deadbeef')).headers.get('location'),
+            transfered(Commit(hash='deadbeef')).headers.get('location'),
+        )
+
+
+class CheckoutRouteUnittest(testing.PathTestCase):
+    basepath = 'mock/repository'
+
+    def setUp(self):
+        super(CheckoutRouteUnittest, self).setUp()
+        os.mkdir(os.path.join(self.path, '.git'))
+
+    @mock_app
+    def test_landing(self, app=None, client=None):
+        with mocks.local.Git(self.path) as repo:
+            app.register_blueprint(CheckoutRoute(
+                Checkout(path=self.path, url=repo.remote, sentinal=False),
+                redirectors=[Redirector('https://trac.webkit.org')],
+            ))
+            response = client.get('/')
+            self.assertEqual(response.status_code, 302)
+            self.assertEqual(response.headers.get('location'), 'https://trac.webkit.org')
+
+    @mock_app
+    def test_json(self, app=None, client=None):
+        with mocks.local.Git(self.path) as repo:
+            app.register_blueprint(CheckoutRoute(
+                Checkout(path=self.path, url=repo.remote, sentinal=False),
+                redirectors=[Redirector('https://trac.webkit.org')],
+            ))
+            reference = Commit.Encoder().default(repo.commits['main'][3])
+            reference['message'] = reference['message'].rstrip()
+
+            response = client.get('4@main/json')
+            self.assertEqual(response.status_code, 200)
+            self.assertEqual(response.json(), reference)
+
+    @mock_app
+    def test_redirect(self, app=None, client=None):
+        with mocks.local.Git(self.path) as repo:
+            app.register_blueprint(CheckoutRoute(
+                Checkout(path=self.path, url=repo.remote, sentinal=False),
+                redirectors=[Redirector('https://github.com/WebKit/WebKit')],
+            ))
+
+            response = client.get('1@branch-a')
+            self.assertEqual(response.status_code, 302)
+            self.assertEqual(
+                response.headers.get('location'),
+                'https://github.com/WebKit/WebKit/commit/a30ce8494bf1ac2807a69844f726be4a9843ca55',
+            )
+
+    @mock_app
+    def test_invoked_redirect(self, app=None, client=None):
+        with mocks.local.Git(self.path, git_svn=True) as repo:
+            app.register_blueprint(CheckoutRoute(
+                Checkout(path=self.path, url=repo.remote, sentinal=False),
+                redirectors=[Redirector('https://github.com/WebKit/WebKit'), Redirector('https://trac.webkit.org')],
+            ))
+
+            response = client.get('a30ce849/trac')
+            self.assertEqual(response.status_code, 302)
+            self.assertEqual(
+                response.headers.get('location'),
+                'https://trac.webkit.org/changeset/3/webkit',
+            )
+
+    @mock_app
+    def test_trac(self, app=None, client=None):
+        with mocks.local.Git(self.path, git_svn=True) as repo:
+            app.register_blueprint(CheckoutRoute(
+                Checkout(path=self.path, url=repo.remote, sentinal=False),
+                redirectors=[Redirector('https://github.com/WebKit/WebKit')],
+            ))
+
+            response = client.get('/changeset/6/webkit')
+            self.assertEqual(response.status_code, 302)
+            self.assertEqual(
+                response.headers.get('location'),
+                'https://github.com/WebKit/WebKit/commit/621652add7fc416099bd2063366cc38ff61afe36',
+            )
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypytestsdatabase_unittestpy"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/database_unittest.py (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/database_unittest.py                         (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/tests/database_unittest.py    2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,99 @@
</span><ins>+# 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 unittest
+
+from reporelaypy import Database
+
+
+class DatabaseUnittest(unittest.TestCase):
+    PARTITION_1 = 'partition_1:'
+    PARTITION_2 = 'partition_2;'
+    EMPTY_PARTITION = None
+
+    def test_get(self):
+        redis_1 = Database(partition=self.PARTITION_1)
+        redis_2 = Database(partition=self.PARTITION_2)
+        redis_3 = Database(partition=self.EMPTY_PARTITION)
+        redis_1.set('key', 'value')
+        redis_2.set('key', 'other')
+        redis_3.set('key', 'empty')
+
+        self.assertEqual(redis_1.get('key').decode('utf-8'), 'value')
+        self.assertEqual(redis_2.get('key').decode('utf-8'), 'other')
+        self.assertEqual(redis_3.get('key').decode('utf-8'), 'empty')
+
+    def test_delete(self):
+        redis_1 = Database(partition=self.PARTITION_1)
+        redis_2 = Database(partition=self.PARTITION_2)
+        redis_3 = Database(partition=self.EMPTY_PARTITION)
+
+        redis_1.set('key-a', 'value-a')
+        redis_1.set('key-b', 'value-b')
+        redis_2.set('key-a', 'other-a')
+        redis_2.set('key-b', 'other-b')
+        redis_3.set('key-a', 'empty-a')
+        redis_3.set('key-b', 'empty-b')
+
+        redis_1.delete('key-a', 'key-b')
+        self.assertEqual(redis_1.get('key-a'), None)
+        self.assertEqual(redis_1.get('key-b'), None)
+
+        self.assertEqual(redis_2.get('key-a').decode('utf-8'), 'other-a')
+        self.assertEqual(redis_2.get('key-b').decode('utf-8'), 'other-b')
+        self.assertEqual(redis_3.get('key-a').decode('utf-8'), 'empty-a')
+        self.assertEqual(redis_3.get('key-b').decode('utf-8'), 'empty-b')
+
+        redis_2.delete('key-a')
+        self.assertEqual(redis_2.get('key-a'), None)
+        self.assertEqual(redis_2.get('key-b').decode('utf-8'), 'other-b')
+
+        redis_3.delete('key-a')
+        self.assertEqual(redis_3.get('key-a'), None)
+        self.assertEqual(redis_3.get('key-b').decode('utf-8'), 'empty-b')
+
+    def test_lock(self):
+        redis_1 = Database(partition=self.PARTITION_1)
+        redis_2 = Database(partition=self.PARTITION_2)
+        redis_3 = Database(partition=self.EMPTY_PARTITION)
+        with redis_1.lock(name='lock', blocking_timeout=.5):
+            with redis_2.lock(name='lock', blocking_timeout=.5):
+                with redis_3.lock(name='lock', blocking_timeout=.5):
+                    pass
+
+    def test_scan(self):
+        redis_1 = Database(partition=self.PARTITION_1)
+        redis_2 = Database(partition=self.PARTITION_2)
+        redis_3 = Database(partition=self.EMPTY_PARTITION)
+        redis_1.set('iter-a', 'value-a')
+        redis_1.set('iter-b', 'value-b')
+        redis_2.set('iter-c', 'value-c')
+        redis_2.set('iter-d', 'value-d')
+        redis_3.set('iter-e', 'value-e')
+        redis_3.set('iter-f', 'value-f')
+
+        for key in redis_1.scan_iter('iter*'):
+            self.assertIn(key.decode('utf-8'), ['iter-a', 'iter-b'])
+        for key in redis_2.scan_iter('iter*'):
+            self.assertIn(key.decode('utf-8'), ['iter-c', 'iter-d'])
+        for key in redis_3.scan_iter('iter*'):
+            self.assertIn(key.decode('utf-8'), ['iter-e', 'iter-f'])
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyreporelaypywebserverpyfromrev286574trunkToolsScriptslibrarieswebkitflaskpysetuppy"></a>
<div class="copfile"><h4>Copied: trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/webserver.py (from rev 286574, trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py) (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/webserver.py                               (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/reporelaypy/webserver.py  2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,53 @@
</span><ins>+# 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
+
+autoinstall_path = os.environ.get('AUTOINSTALL_PATH')
+if autoinstall_path:
+    from webkitcorepy import AutoInstall
+    AutoInstall.set_directory(autoinstall_path)
+
+from flask import Flask, current_app, json as fjson
+from reporelaypy import Checkout, CheckoutRoute, Database, Redirector
+
+app = Flask(__name__)
+
+database = Database()
+checkout = Checkout.from_json(os.environ.get('CHECKOUT', '{}'))
+checkout_routes = CheckoutRoute(
+    checkout, redirectors=Redirector.from_json(os.environ.get('REDIRECTORS', '[]')),
+    import_name=__name__, database=database,
+)
+
+
+@app.route('/__health')
+def health():
+    return 'ready' if checkout.repository else 'cloning'
+
+
+app.register_blueprint(checkout_routes)
+
+
+if __name__ == '__main__':
+    port = int(os.environ.get('PORT', 5000))
+    app.run(host='0.0.0.0', port=port)
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesreporelaypyrun"></a>
<div class="addfile"><h4>Added: trunk/Tools/Scripts/libraries/reporelaypy/run (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/run                            (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/run       2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,168 @@
</span><ins>+#!/usr/bin/env python3
+
+# Copyright (C) 2020 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 argparse
+import json
+import math
+import os
+import subprocess
+import sys
+import time
+
+import reporelaypy
+import webkitcorepy
+
+scripts = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
+if os.path.isdir(os.path.join(scripts, 'webkitpy')):
+    sys.path.insert(0, scripts)
+    import webkitpy
+
+from reporelaypy import Checkout, Database, Redirector
+from webkitcorepy import arguments, AutoInstall
+from whichcraft import which
+
+
+def main(args=None):
+    parser = argparse.ArgumentParser(description='Run a git relay server for testing')
+
+    group = parser.add_argument_group('Repository')
+    group.add_argument(
+        '--path', '-p', '-C',
+        dest='path', default=os.getcwd(), action='store',
+        help='Set the repository path to be used',
+    )
+    group.add_argument(
+        '--http-proxy', dest='http_proxy', default=None, action='store',
+        help='Set an http proxy',
+    )
+    group.add_argument(
+        '--url', dest='url', default=None, action='store',
+        help='Set the repository path to be used',
+    )
+    parser.add_argument(
+        '--sentinal', '--no-sentinal', action=arguments.NoAction, dest='sentinal', default=False,
+        help='Lay down sentinal file on disk to determine checkout state.',
+    )
+    group.add_argument(
+        '--update', dest='update_interval', default=0, action='store', type=int,
+        help='Set the number of seconds to wait between polling git checkout',
+    )
+
+    group = parser.add_argument_group('Database')
+    group.add_argument(
+        '--redis-host', dest='redis_host', default=None, action='store',
+        help="Set the hostname of the Redis database to be used. (Environment variable '{}' used by default)".format(Database.HOST_ENV),
+    )
+    group.add_argument(
+        '--redis-password', dest='redis_password', default=None, action='store',
+        help="Set the password of the Redis database to be used. (Environment variable '{}' used by default)".format(Database.PASSWORD_ENV),
+    )
+    group.add_argument(
+        '--redis-expiration', dest='redis_expiration', default=Database.DEFAULT_EXPIRATION, action='store', type=int,
+        help="Set the default Redis expiration time in seconds",
+    )
+
+    group = parser.add_argument_group('Webserver')
+    group.add_argument(
+        '--port', dest='port', default=5000, action='store',
+        help='Port to expose webserver on.',
+    )
+    group.add_argument(
+        '--poll', dest='poll', default=10, action='store',
+        help='Number of seconds between polls of the webserver process.',
+    )
+    group.add_argument(
+        '--redirector', '-r', dest='redirector', action='append',
+        help='Base URL to redirect user to for commit information.',
+    )
+
+    args = parser.parse_args(args=args)
+
+    database = Database(host=args.redis_host, password=args.redis_password)
+    print('Constructed database with parameters:')
+    print('    Host: {}'.format(database.host or 'None (in-memory fake database)'))
+    if database.password:
+        print('    Password: {}'.format(len(database.password) * '*'))
+    if database.default_expiration:
+        print('    Expiration: {} hours'.format(database.default_expiration / (60.0 * 60.0)))
+    print('Connecting to database...')
+    database.ping()
+    print('Connected to database!')
+
+    print('Finding checkout...')
+    checkout = Checkout(path=args.path, url=args.url, http_proxy=args.http_proxy, sentinal=args.sentinal)
+    print('Git checkout:')
+    print('    Path: {}'.format(checkout.path))
+    print('    URL: {}'.format(checkout.url))
+    if not checkout.repository:
+        print('    Cloning repository...')
+
+    if args.update_interval:
+        print('    Polling checkout with interval of {} seconds, doing first poll now...'.format(args.update_interval))
+        checkout.update_all()
+
+    if args.redirector:
+        print('User specified the following redirect urls:')
+        for url in args.redirector:
+            redirector = Redirector(url)
+            print('    {}: {}'.format(redirector.name, redirector.url))
+
+    env = dict(
+        PYTHONPATH=':'.join(sys.path),
+        CHECKOUT=json.dumps(checkout, cls=Checkout.Encoder),
+        REDIRECTORS=json.dumps([Redirector(url) for url in args.redirector or []], cls=Redirector.Encoder)
+    )
+
+    if AutoInstall.directory:
+        env['AUTOINSTALL_PATH'] = AutoInstall.directory
+    if database.host:
+        env[database.HOST_ENV] = database.host
+    if database.password:
+        env[database.PASSWORD_ENV] = database.password
+    if database.default_expiration:
+        env[database.EXPIRATION_ENV] = str(database.default_expiration)
+
+    with subprocess.Popen(
+        [which('gunicorn'), 'reporelaypy.webserver:app'],
+        cwd=os.path.dirname(os.path.dirname(reporelaypy.__file__)),
+        env=env,
+    ) as webserver:
+        last_poll = time.time()
+        last_pull = time.time()
+
+        while True:
+            if last_poll + args.poll < time.time():
+                if webserver.poll() is None:
+                    break
+                last_poll = time.time()
+            if last_pull + args.update_interval < time.time():
+                checkout.update_all()
+                last_pull = time.time()
+            time.sleep(math.gcd(args.poll, args.update_interval))
+
+    return 0
+
+
+if '__main__' == __name__:
+    sys.exit(main(args=sys.argv[1:]))
</ins><span class="cx">Property changes on: trunk/Tools/Scripts/libraries/reporelaypy/run
</span><span class="cx">___________________________________________________________________
</span></span></pre></div>
<a id="svnexecutable"></a>
<div class="addfile"><h4>Added: svn:executable</h4></div>
<ins>+*
</ins><span class="cx">\ No newline at end of property
</span><a id="trunkToolsScriptslibrariesreporelaypysetuppyfromrev286574trunkToolsScriptslibrarieswebkitflaskpysetuppy"></a>
<div class="copfile"><h4>Copied: trunk/Tools/Scripts/libraries/reporelaypy/setup.py (from rev 286574, trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py) (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/reporelaypy/setup.py                               (rev 0)
+++ trunk/Tools/Scripts/libraries/reporelaypy/setup.py  2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,66 @@
</span><ins>+# 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.
+
+from setuptools import setup
+
+
+def readme():
+    with open('README.md') as f:
+        return f.read()
+
+
+setup(
+    name='reporelaypy',
+    version='0.1.0',
+    description='Library for visualizing, processing and storing test results.',
+    long_description=readme(),
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Framework :: Flask',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved :: BSD License',
+        'Operating System :: MacOS',
+        'Natural Language :: English',
+        'Programming Language :: Python :: 3 :: Only',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+        'Topic :: Software Development :: Version Control :: Git',
+    ],
+    keywords='git hook mirror webkit',
+    url='https://github.com/WebKit/WebKit/tree/main/Tools/Scripts/libraries/reporelaypy',
+    author='Jonathan Bedard',
+    author_email='jbedard@apple.com',
+    license='Modified BSD',
+    packages=[
+        'reporelaypy',
+        'reporelaypy.test',
+    ],
+    install_requires=[
+        'fakeredis',
+        'redis',
+        'xmltodict',
+        'webkitcorepy',
+        'webkitscmpy',
+        'webkitflaskpy',
+    ],
+    include_package_data=True,
+    zip_safe=False,
+)
</ins></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpy__init__py"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py        2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/__init__.py   2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -44,7 +44,7 @@
</span><span class="cx">         "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
</span><span class="cx">     )
</span><span class="cx"> 
</span><del>-version = Version(3, 1, 5)
</del><ins>+version = Version(3, 1, 6)
</ins><span class="cx"> 
</span><span class="cx"> import webkitflaskpy
</span><span class="cx"> 
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpyresultsdbpyflask_supportflask_testcasepy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/flask_support/flask_testcase.py (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/flask_support/flask_testcase.py    2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/resultsdbpy/flask_support/flask_testcase.py       2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -21,31 +21,17 @@
</span><span class="cx"> # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
</span><span class="cx"> 
</span><span class="cx"> import atexit
</span><del>-import json
</del><span class="cx"> import os
</span><span class="cx"> import requests
</span><span class="cx"> import unittest
</span><span class="cx"> 
</span><span class="cx"> from flask import Flask
</span><del>-from flask.wrappers import Response
</del><span class="cx"> from selenium import webdriver
</span><span class="cx"> from selenium.common.exceptions import WebDriverException
</span><span class="cx"> from resultsdbpy.flask_support.flask_test_context import FlaskTestContext
</span><ins>+from webkitflaskpy import Response
</ins><span class="cx"> 
</span><span class="cx"> 
</span><del>-class FlaskRequestsResponse(Response):
-    @property
-    def text(self):
-        return self.data.decode('utf-8')
-
-    @property
-    def content(self):
-        return self.data
-
-    def json(self):
-        return json.loads(self.text)
-
-
</del><span class="cx"> class FlaskTestCase(unittest.TestCase):
</span><span class="cx">     URL = f'http://localhost:{FlaskTestContext.PORT}'
</span><span class="cx"> 
</span><span class="lines">@@ -104,7 +90,7 @@
</span><span class="cx">         def decorator(method):
</span><span class="cx">             def real_method(val, method=method, **kwargs):
</span><span class="cx">                 app = Flask('testing')
</span><del>-                app.response_class = FlaskRequestsResponse
</del><ins>+                app.response_class = Response
</ins><span class="cx">                 app.config['TESTING'] = True
</span><span class="cx">                 val.setup_webserver(app, **kwargs)
</span><span class="cx">                 app.add_url_rule('/__health', 'health', lambda: 'ok', methods=('GET',))
</span></span></pre></div>
<a id="trunkToolsScriptslibrariesresultsdbpysetuppy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/resultsdbpy/setup.py (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/resultsdbpy/setup.py       2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/Scripts/libraries/resultsdbpy/setup.py  2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -30,7 +30,7 @@
</span><span class="cx"> 
</span><span class="cx"> setup(
</span><span class="cx">     name='resultsdbpy',
</span><del>-    version='3.1.5',
</del><ins>+    version='3.1.6',
</ins><span class="cx">     description='Library for visualizing, processing and storing test results.',
</span><span class="cx">     long_description=readme(),
</span><span class="cx">     classifiers=[
</span></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitflaskpysetuppy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py     2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py        2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -30,8 +30,8 @@
</span><span class="cx"> 
</span><span class="cx"> setup(
</span><span class="cx">     name='webkitflaskpy',
</span><del>-    version='0.2.0',
-    description='Library for visualizing, processing and storing test results.',
</del><ins>+    version='0.3.0',
+    description="Library supporting the WebKit Team's flask based web services.",
</ins><span class="cx">     long_description=readme(),
</span><span class="cx">     classifiers=[
</span><span class="cx">         'Development Status :: 4 - Beta',
</span></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitflaskpywebkitflaskpy__init__py"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/__init__.py (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/__init__.py    2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/__init__.py       2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -43,7 +43,7 @@
</span><span class="cx">         "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
</span><span class="cx">     )
</span><span class="cx"> 
</span><del>-version = Version(0, 2, 0)
</del><ins>+version = Version(0, 3, 0)
</ins><span class="cx"> 
</span><span class="cx"> AutoInstall.register(Package('click', Version(7, 1, 2)))
</span><span class="cx"> AutoInstall.register(Package('flask', Version(1, 1, 2)))
</span><span class="lines">@@ -53,5 +53,7 @@
</span><span class="cx"> AutoInstall.register(Package('werkzeug', Version(1, 0, 1)))
</span><span class="cx"> 
</span><span class="cx"> from webkitflaskpy.authed_blueprint import AuthedBlueprint  # noqa: E402
</span><ins>+from webkitflaskpy.response import Response
+from webkitflaskpy.mock_app import mock_app
</ins><span class="cx"> 
</span><span class="cx"> name = 'webkitflaskpy'
</span></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitflaskpywebkitflaskpymock_apppyfromrev286574trunkToolsScriptslibrarieswebkitflaskpysetuppy"></a>
<div class="copfile"><h4>Copied: trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/mock_app.py (from rev 286574, trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py) (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/mock_app.py                            (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/mock_app.py       2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,37 @@
</span><ins>+# 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.
+
+from flask import Flask
+
+from .response import Response
+
+
+def mock_app(method):
+    def real_method(val, method=method, **kwargs):
+        app = Flask('testing')
+        app.response_class = Response
+        app.config['TESTING'] = True
+        app.add_url_rule('/__health', 'health', lambda: 'ok', methods=('GET',))
+        return method(val, app=app, client=app.test_client(), **kwargs)
+
+    real_method.__name__ = method.__name__
+    return real_method
</ins></span></pre></div>
<a id="trunkToolsScriptslibrarieswebkitflaskpywebkitflaskpyresponsepyfromrev286574trunkToolsScriptslibrarieswebkitflaskpysetuppy"></a>
<div class="copfile"><h4>Copied: trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/response.py (from rev 286574, trunk/Tools/Scripts/libraries/webkitflaskpy/setup.py) (0 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/response.py                            (rev 0)
+++ trunk/Tools/Scripts/libraries/webkitflaskpy/webkitflaskpy/response.py       2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -0,0 +1,38 @@
</span><ins>+# 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 json
+
+from flask.wrappers import Response as FlaskResponse
+
+
+class Response(FlaskResponse):
+    @property
+    def text(self):
+        return self.data.decode('utf-8')
+
+    @property
+    def content(self):
+        return self.data
+
+    def json(self):
+        return json.loads(self.text)
</ins></span></pre></div>
<a id="trunkToolsScriptswebkitpy__init__py"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/webkitpy/__init__.py (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/webkitpy/__init__.py 2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/Scripts/webkitpy/__init__.py    2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -80,6 +80,10 @@
</span><span class="cx"> AutoInstall.register(Package('zipp', Version(1, 2, 0)))
</span><span class="cx"> AutoInstall.register(Package('zope.interface', Version(5, 1, 0), aliases=['zope'], pypi_name='zope-interface'))
</span><span class="cx"> 
</span><ins>+if sys.version_info > (3, 0):
+    AutoInstall.register(Package('reporelaypy', Version(0, 1, 0)), local=True)
+
+AutoInstall.register(Package('webkitflaskpy', Version(0, 1, 1)), local=True)
</ins><span class="cx"> AutoInstall.register(Package('webkitscmpy', Version(0, 12, 5)), local=True)
</span><span class="cx"> 
</span><span class="cx"> import webkitscmpy
</span></span></pre></div>
<a id="trunkToolsScriptswebkitpytestmainpy"></a>
<div class="modfile"><h4>Modified: trunk/Tools/Scripts/webkitpy/test/main.py (286575 => 286576)</h4>
<pre class="diff"><span>
<span class="info">--- trunk/Tools/Scripts/webkitpy/test/main.py        2021-12-07 00:06:10 UTC (rev 286575)
+++ trunk/Tools/Scripts/webkitpy/test/main.py   2021-12-07 00:18:20 UTC (rev 286576)
</span><span class="lines">@@ -65,6 +65,8 @@
</span><span class="cx">     tester.add_tree(os.path.join(_webkit_root, 'Tools', 'Scripts', 'libraries', 'webkitcorepy'), 'webkitcorepy')
</span><span class="cx">     tester.add_tree(os.path.join(_webkit_root, 'Tools', 'Scripts', 'libraries', 'webkitscmpy'), 'webkitscmpy')
</span><span class="cx">     tester.add_tree(os.path.join(_webkit_root, 'Tools', 'Scripts', 'libraries', 'webkitflaskpy'), 'webkitflaskpy')
</span><ins>+    if sys.version_info > (3, 0):
+        tester.add_tree(os.path.join(_webkit_root, 'Tools', 'Scripts', 'libraries', 'reporelaypy'), 'reporelaypy')
</ins><span class="cx"> 
</span><span class="cx">     # AppleWin is the only platform that does not support Modern WebKit
</span><span class="cx">     # FIXME: Find a better way to detect this currently assuming cygwin means AppleWin
</span><span class="lines">@@ -188,6 +190,12 @@
</span><span class="cx">         sys.path = self.finder.additional_paths(sys.path) + sys.path
</span><span class="cx"> 
</span><span class="cx">         from webkitcorepy import AutoInstall
</span><ins>+
+        # Force registration of all autoinstalled packages.
+        if sys.version_info > (3, 0):
+            import reporelaypy
+        import webkitflaskpy
+
</ins><span class="cx">         AutoInstall.install_everything()
</span><span class="cx"> 
</span><span class="cx">         start_time = time.time()
</span></span></pre>
</div>
</div>

</body>
</html>