Title: [277102] trunk/Tools
Revision
277102
Author
jbed...@apple.com
Date
2021-05-06 11:32:40 -0700 (Thu, 06 May 2021)

Log Message

[webkitcorepy] Add API to efficiently create a sequence of commits
https://bugs.webkit.org/show_bug.cgi?id=224890
<rdar://problem/76975733>

Rubber-stamped by Aakash Jain.

While it is possible to simple iterate through a range of commits to define them,
every API we use to define commits has much more efficient techniques.

* Scripts/libraries/webkitscmpy/setup.py: Bump version.
* Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
* Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py:
(Contributor): Add revision to SVN_AUTHOR_RE and add regex without lines.
(Contributor.from_scm_log): Strip leading whitespace from author.
* Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py:
(Git._args_from_content):
(Git.commits): Use `git log` to efficiently compute a range of commits.
* Scripts/libraries/webkitscmpy/webkitscmpy/local/svn.py:
(Svn._args_from_content):
(Svn.commits): Use `svn log` to efficiently compute a range of commits.
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py:
(Git.__init__): Add `git log` mock.
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py:
(Svn.__init__): Add `svn log` mock and more explicit `svn info` mock.
(Svn._log_range):
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
(GitHub._commits_response): Return all parent commits to provided ref.
(GitHub.request):
* Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/svn.py:
(Svn.range): More efficiently compute the range.
(Svn.request):
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py:
(GitHub.request): Allow caller to disable pagination.
(GitHub.commit): Reduce number of requests required to compute order.
(GitHub.commits): Using the `commits` endpoint, more efficiently
compute a range of commits.
* Scripts/libraries/webkitscmpy/webkitscmpy/remote/svn.py:
(Svn): Generalize HISTORY_RE to match any single-line SVN XML response.
(Svn._cache_revisions): Replace HISTORY_RE with DATA_RE.
(Svn.commits): Use svn/rvr to efficiently compute a range of commits.
* Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py:
(ScmBase._commit_range): Return a pair of commits representing the range
the caller is requesting, and preform some basic sanity checks.
(ScmBase.commits): Declare function implemented by decedents.
* Scripts/libraries/webkitscmpy/webkitscmpy/test/find_unittest.py:
* Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
(TestGit.test_commits):
(TestGit.test_commits_branch):
(TestGitHub.test_commits):
(TestGitHub.test_commits_branch):
* Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py:
(TestLocalSvn.test_commits):
(TestLocalSvn.test_commits_branch):
(TestRemoteSvn.test_commits):
(TestRemoteSvn.test_commits_branch):

Modified Paths

Diff

Modified: trunk/Tools/ChangeLog (277101 => 277102)


--- trunk/Tools/ChangeLog	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/ChangeLog	2021-05-06 18:32:40 UTC (rev 277102)
@@ -1,3 +1,61 @@
+2021-05-06  Jonathan Bedard  <jbed...@apple.com>
+
+        [webkitcorepy] Add API to efficiently create a sequence of commits
+        https://bugs.webkit.org/show_bug.cgi?id=224890
+        <rdar://problem/76975733>
+
+        Rubber-stamped by Aakash Jain.
+
+        While it is possible to simple iterate through a range of commits to define them,
+        every API we use to define commits has much more efficient techniques.
+
+        * Scripts/libraries/webkitscmpy/setup.py: Bump version.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py: Ditto.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py:
+        (Contributor): Add revision to SVN_AUTHOR_RE and add regex without lines.
+        (Contributor.from_scm_log): Strip leading whitespace from author.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py:
+        (Git._args_from_content):
+        (Git.commits): Use `git log` to efficiently compute a range of commits.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/local/svn.py:
+        (Svn._args_from_content):
+        (Svn.commits): Use `svn log` to efficiently compute a range of commits.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py:
+        (Git.__init__): Add `git log` mock.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py:
+        (Svn.__init__): Add `svn log` mock and more explicit `svn info` mock.
+        (Svn._log_range):
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py:
+        (GitHub._commits_response): Return all parent commits to provided ref.
+        (GitHub.request):
+        * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/svn.py:
+        (Svn.range): More efficiently compute the range.
+        (Svn.request):
+        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py:
+        (GitHub.request): Allow caller to disable pagination.
+        (GitHub.commit): Reduce number of requests required to compute order.
+        (GitHub.commits): Using the `commits` endpoint, more efficiently
+        compute a range of commits.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/remote/svn.py:
+        (Svn): Generalize HISTORY_RE to match any single-line SVN XML response.
+        (Svn._cache_revisions): Replace HISTORY_RE with DATA_RE.
+        (Svn.commits): Use svn/rvr to efficiently compute a range of commits.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py:
+        (ScmBase._commit_range): Return a pair of commits representing the range
+        the caller is requesting, and preform some basic sanity checks.
+        (ScmBase.commits): Declare function implemented by decedents.
+        * Scripts/libraries/webkitscmpy/webkitscmpy/test/find_unittest.py:
+        * Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
+        (TestGit.test_commits):
+        (TestGit.test_commits_branch):
+        (TestGitHub.test_commits):
+        (TestGitHub.test_commits_branch):
+        * Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py:
+        (TestLocalSvn.test_commits):
+        (TestLocalSvn.test_commits_branch):
+        (TestRemoteSvn.test_commits):
+        (TestRemoteSvn.test_commits_branch):
+
 2021-05-06  Chris Dumez  <cdu...@apple.com>
 
         REGRESSION (r272414?): [macOS] TestWebKitAPI.GPUProcess.CrashWhilePlayingVideo is a flaky failure

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/setup.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/setup.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -29,7 +29,7 @@
 
 setup(
     name='webkitscmpy',
-    version='0.13.9',
+    version='0.14.0',
     description='Library designed to interact with git and svn repositories.',
     long_description=readme(),
     classifiers=[

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/__init__.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -46,7 +46,7 @@
         "Please install webkitcorepy with `pip install webkitcorepy --extra-index-url <package index URL>`"
     )
 
-version = Version(0, 13, 9)
+version = Version(0, 14, 0)
 
 AutoInstall.register(Package('fasteners', Version(0, 15, 0)))
 AutoInstall.register(Package('monotonic', Version(1, 5)))

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/contributor.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -32,7 +32,8 @@
     AUTOMATED_CHECKIN_RE = re.compile(r'Author: (?P<author>.*) <devnull>')
     UNKNOWN_AUTHOR = re.compile(r'Author: (?P<author>.*) <None>')
     EMPTY_AUTHOR = re.compile(r'Author: (?P<author>.*) <>')
-    SVN_AUTHOR_RE = re.compile(r'r\d+ \| (?P<email>.*) \| (?P<date>.*) \| \d+ lines?')
+    SVN_AUTHOR_RE = re.compile(r'r(?P<revision>\d+) \| (?P<email>.*) \| (?P<date>.*) \| \d+ lines?')
+    SVN_AUTHOR_Q_RE = re.compile(r'r(?P<revision>\d+) \| (?P<email>.*) \| (?P<date>.*)')
     SVN_PATCH_FROM_RE = re.compile(r'Patch by (?P<author>.*) <(?P<email>.*)> on \d+-\d+-\d+')
 
     class Encoder(json.JSONEncoder):
@@ -115,11 +116,19 @@
         email = None
         author = None
 
-        for _expression_ in [cls.GIT_AUTHOR_RE, cls.SVN_AUTHOR_RE, cls.SVN_PATCH_FROM_RE, cls.AUTOMATED_CHECKIN_RE, cls.UNKNOWN_AUTHOR, cls.EMPTY_AUTHOR]:
+        for _expression_ in [
+            cls.GIT_AUTHOR_RE,
+            cls.SVN_AUTHOR_RE,
+            cls.SVN_PATCH_FROM_RE,
+            cls.AUTOMATED_CHECKIN_RE,
+            cls.UNKNOWN_AUTHOR,
+            cls.EMPTY_AUTHOR,
+            cls.SVN_AUTHOR_Q_RE,
+        ]:
             match = _expression_.match(line)
             if match:
                 if 'author' in _expression_.groupindex:
-                    author = match.group('author')
+                    author = match.group('author').lstrip()
                     if '(no author)' in author or 'Automated Checkin' in author or 'Unknown' in author:
                         author = None
                 if 'email' in _expression_.groupindex:

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -20,12 +20,18 @@
 # 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 calendar
 import logging
 import os
 import re
 import six
+import subprocess
+import sys
+import time
 
-from webkitcorepy import run, decorators, TimeoutExpired
+from datetime import datetime, timedelta
+
+from webkitcorepy import run, decorators
 from webkitscmpy.local import Scm
 from webkitscmpy import Commit, Contributor, log
 
@@ -300,6 +306,106 @@
             message=logcontent if include_log else None,
         )
 
+    def _args_from_content(self, content, include_log=True):
+        author = None
+        timestamp = None
+
+        for line in content.splitlines()[:4]:
+            split = line.split(': ')
+            if split[0] == 'Author':
+                author = Contributor.from_scm_log(line.lstrip(), self.contributors)
+            elif split[0] == 'CommitDate':
+                tz_diff = line.split(' ')[-1]
+                date = datetime.strptime(split[1].lstrip()[:-len(tz_diff)], '%a %b %d %H:%M:%S %Y ')
+                date += timedelta(
+                    hours=int(tz_diff[1:3]),
+                    minutes=int(tz_diff[3:5]),
+                ) * (1 if tz_diff[0] == '-' else -1)
+                timestamp = int(calendar.timegm(date.timetuple())) - time.timezone
+
+        message = ''
+        for line in content.splitlines()[5:]:
+            message += line[4:] + '\n'
+        matches = self.GIT_SVN_REVISION.findall(message)
+
+        return dict(
+            revision=int(matches[-1].split('@')[0]) if matches else None,
+            author=author,
+            timestamp=timestamp,
+            message=message.rstrip() if include_log else None,
+        )
+
+    def commits(self, begin=None, end=None, include_log=True, include_identifier=True):
+        begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier)
+
+        try:
+            log = None
+            log = subprocess.Popen(
+                [self.executable(), 'log', '--format=fuller', '{}...{}'.format(end.hash, begin.hash)],
+                cwd=self.root_path,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                **(dict(encoding='utf-8') if sys.version_info > (3, 0) else dict())
+            )
+            if log.poll():
+                raise self.Exception("Failed to construct history for '{}'".format(end.branch))
+
+            line = log.stdout.readline()
+            previous = [end]
+            while line:
+                if not line.startswith('commit '):
+                    raise OSError('Failed to parse `git log` format')
+                branch_point = previous[0].branch_point
+                identifier = previous[0].identifier
+                hash = line.split(' ')[-1].rstrip()
+                if hash != previous[0].hash:
+                    identifier -= 1
+
+                if not identifier:
+                    identifier = branch_point
+                    branch_point = None
+
+                content = ''
+                line = log.stdout.readline()
+                while line and not line.startswith('commit '):
+                    content += line
+                    line = log.stdout.readline()
+
+                commit = Commit(
+                    repository_id=self.id,
+                    hash=hash,
+                    branch=end.branch if identifier and branch_point else self.default_branch,
+                    identifier=identifier if include_identifier else None,
+                    branch_point=branch_point if include_identifier else None,
+                    order=0,
+                    **self._args_from_content(content, include_log=include_log)
+                )
+
+                # Ensure that we don't duplicate the first and last commits
+                if commit.hash == previous[0].hash:
+                    previous[0] = commit
+
+                # If we share a timestamp with the previous commit, that means that this commit has an order
+                # less than the set of commits cached in previous
+                elif commit.timestamp == previous[0].timestamp:
+                    for cached in previous:
+                        cached.order += 1
+                    previous.append(commit)
+
+                # If we don't share a timestamp with the previous set of commits, we should return all commits
+                # cached in previous.
+                else:
+                    for cached in previous:
+                        yield cached
+                    previous = [commit]
+
+            for cached in previous:
+                cached.order += begin.order
+                yield cached
+        finally:
+            if log:
+                log.kill()
+
     def find(self, argument, include_log=True, include_identifier=True):
         if not isinstance(argument, six.string_types):
             raise ValueError("Expected 'argument' to be a string, not '{}'".format(type(argument)))

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/svn.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/svn.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/svn.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -401,6 +401,91 @@
             message=message,
         )
 
+    def _args_from_content(self, content, include_log=True):
+        leading = content.splitlines()[0]
+        match = Contributor.SVN_AUTHOR_RE.match(leading) or Contributor.SVN_AUTHOR_Q_RE.match(leading)
+        if not match:
+            return {}
+
+        tz_diff = match.group('date').split(' ', 2)[-1]
+        date = datetime.strptime(match.group('date')[:-len(tz_diff)], '%Y-%m-%d %H:%M:%S ')
+        date += timedelta(
+            hours=int(tz_diff[1:3]),
+            minutes=int(tz_diff[3:5]),
+        ) * (1 if tz_diff[0] == '-' else -1)
+
+        return dict(
+            revision=int(match.group('revision')),
+            timestamp=int(calendar.timegm(date.timetuple())),
+            author=Contributor.from_scm_log(leading, self.contributors),
+            message='\n'.join(content.splitlines()[2:]).rstrip() if include_log else None,
+        )
+
+
+    def commits(self, begin=None, end=None, include_log=True, include_identifier=True):
+        begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier)
+        previous = end
+        if end.branch == self.default_branch or '/' in end.branch:
+            branch_arg = '^/{}'.format(end.branch)
+        else:
+            branch_arg = '^/branches/{}'.format(end.branch)
+
+        try:
+            log = None
+            log = subprocess.Popen(
+                [self.executable(), 'log', '-r', '{}:{}'.format(
+                    end.revision, begin.revision,
+                ), branch_arg] + ([] if include_log else ['-q']),
+                cwd=self.root_path,
+                stdout=subprocess.PIPE,
+                stderr=subprocess.PIPE,
+                **(dict(encoding='utf-8') if sys.version_info > (3, 0) else dict())
+            )
+            if log.poll():
+                raise self.Exception('Failed to find commits between {} and {} on {}'.format(begin, end, branch_arg))
+
+            content = ''
+            line = log.stdout.readline()
+            divider = '-' * 72
+            while True:
+                if line and line.rstrip() != divider:
+                    content += line
+                    line = log.stdout.readline()
+                    continue
+
+                if not content:
+                    line = log.stdout.readline()
+                    continue
+
+                branch_point = previous.branch_point if include_identifier else None
+                identifier = previous.identifier if include_identifier else None
+
+                args = self._args_from_content(content, include_log=include_log)
+                if args['revision'] != previous.revision:
+                    yield previous
+                    identifier -= 1
+                if not identifier:
+                    identifier = branch_point
+                    branch_point = None
+
+                previous = Commit(
+                    repository_id=self.id,
+                    branch=end.branch if branch_point else self.default_branch,
+                    identifier=identifier,
+                    branch_point=branch_point,
+                    **args
+                )
+                content = ''
+                if not line:
+                    break
+                line = log.stdout.readline()
+
+            yield previous
+
+        finally:
+            if log:
+                log.kill()
+
     def checkout(self, argument):
         commit = self.find(argument)
         if not commit:

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -23,6 +23,7 @@
 import json
 import os
 import re
+import time
 
 from datetime import datetime
 from webkitcorepy import mocks, OutputCapture, StringIO
@@ -228,7 +229,7 @@
                             branch=self.branch,
                             author=self.find(args[2]).author.name,
                             email=self.find(args[2]).author.email,
-                            date=datetime.fromtimestamp(self.find(args[2]).timestamp).strftime('%a %b %d %H:%M:%S %Y'),
+                            date=datetime.utcfromtimestamp(self.find(args[2]).timestamp + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'),
                             log='\n'.join([
                                     ('    ' + line) if line else '' for line in self.find(args[2]).message.splitlines()
                                 ] + (['    git-svn-id: https://svn.{}/repository/{}/trunk@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc'.format(
@@ -240,6 +241,33 @@
                         ),
                 ) if self.find(args[2]) else mocks.ProcessCompletion(returncode=128),
             ), mocks.Subprocess.Route(
+                self.executable, 'log', '--format=fuller', re.compile(r'.+\.\.\..+'),
+                cwd=self.path,
+                generator=lambda *args, **kwargs: mocks.ProcessCompletion(
+                    returncode=0,
+                    stdout='\n'.join([
+                        'commit {hash}\n'
+                        'Author:     {author} <{email}>\n'
+                        'AuthorDate: {date}\n'
+                        'Commit:     {author} <{email}>\n'
+                        'CommitDate: {date}\n'
+                        '\n{log}'.format(
+                            hash=commit.hash,
+                            author=commit.author.name,
+                            email=commit.author.email,
+                            date=datetime.utcfromtimestamp(commit.timestamp + time.timezone).strftime('%a %b %d %H:%M:%S %Y +0000'),
+                            log='\n'.join([
+                                ('    ' + line) if line else '' for line in commit.message.splitlines()
+                            ] + ([
+                                '    git-svn-id: https://svn.{}/repository/{}/trunk@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc'.format(
+                                    self.remote.split('@')[-1].split(':')[0],
+                                    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])
+                    ])
+                )
+            ), mocks.Subprocess.Route(
                 self.executable, 'rev-list', '--count', '--no-merges', re.compile(r'.+'),
                 cwd=self.path,
                 generator=lambda *args, **kwargs: mocks.ProcessCompletion(
@@ -463,3 +491,34 @@
             returncode=0,
             stdout=stdout.getvalue(),
         )
+
+    def commits_in_range(self, begin, end):
+        branches = [self.default_branch]
+        for branch, commits in self.commits.items():
+            if branch == self.default_branch:
+                continue
+            for commit in commits:
+                if commit.hash == end:
+                    branches.insert(0, branch)
+                    break
+            if len(branches) > 1:
+                break
+
+        in_range = False
+        previous = None
+        for branch in branches:
+            for commit in reversed(self.commits[branch]):
+                if commit.hash == end:
+                    in_range = True
+                if in_range and (not previous or commit.hash != previous.hash):
+                    yield commit
+                previous = commit
+                if commit.hash == begin:
+                    in_range = False
+            in_range = False
+            if not previous or branch == self.default_branch:
+                continue
+
+            for commit in reversed(self.commits[self.default_branch]):
+                if previous.branch_point == commit.identifier:
+                    end = commit.hash

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/svn.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -65,14 +65,18 @@
 
         super(Svn, self).__init__(
             mocks.Subprocess.Route(
+                self.executable, 'info', self.BRANCH_RE,
+                cwd=self.path,
+                generator=lambda *args, **kwargs: self._info(branch=self.BRANCH_RE.match(args[2]).group('branch'), cwd=kwargs.get('cwd', ''))
+            ), mocks.Subprocess.Route(
+                self.executable, 'info', '-r', re.compile(r'\d+'),
+                cwd=self.path,
+                generator=lambda *args, **kwargs: self._info(revision=int(args[3]), cwd=kwargs.get('cwd', ''))
+            ), mocks.Subprocess.Route(
                 self.executable, 'info',
                 cwd=self.path,
                 generator=lambda *args, **kwargs: self._info(cwd=kwargs.get('cwd', ''))
             ), mocks.Subprocess.Route(
-                self.executable, 'info', self.BRANCH_RE,
-                cwd=self.path,
-                generator=lambda *args, **kwargs: self._info(branch=self.BRANCH_RE.match(args[2]).group('branch'), cwd=kwargs.get('cwd', ''))
-            ), mocks.Subprocess.Route(
                 self.executable, 'list', '^/branches',
                 cwd=self.path,
                 generator=lambda *args, **kwargs: mocks.ProcessCompletion(
@@ -119,6 +123,14 @@
                     revision=args[5],
                 ) if self.connected else mocks.ProcessCompletion(returncode=1)
             ), mocks.Subprocess.Route(
+                self.executable, 'log', '-r', re.compile(r'\d+:\d+'), self.BRANCH_RE,
+                cwd=self.path,
+                generator=lambda *args, **kwargs: self._log_range(
+                    branch=self.BRANCH_RE.match(args[4]).group('branch'),
+                    end=int(args[3].split(':')[0]),
+                    begin=int(args[3].split(':')[-1]),
+                ) if self.connected else mocks.ProcessCompletion(returncode=1)
+            ), mocks.Subprocess.Route(
                 self.executable, 'up', '-r', re.compile(r'\d+'),
                 cwd=self.path,
                 generator=lambda *args, **kwargs:
@@ -204,6 +216,28 @@
                 ),
         )
 
+    def _log_range(self, branch=None, end=None, begin=None):
+        if end < begin:
+            return mocks.ProcessCompletion(returncode=1)
+
+        output = ''
+        previous = None
+        for b in [branch, 'trunk']:
+            for candidate in reversed(self.commits.get(b, [])):
+                if candidate.revision > end or candidate.revision < begin:
+                    continue
+                if previous and previous.revision <= candidate.revision:
+                    continue
+                previous = candidate
+                output += ('------------------------------------------------------------------------\n'
+                    '{line} | {lines} lines\n\n'
+                    '{log}\n').format(
+                        line=self.log_line(candidate),
+                        lines=len(candidate.message.splitlines()),
+                        log=candidate.message
+                )
+        return mocks.ProcessCompletion(returncode=0, stdout=output)
+
     def find(self, branch=None, revision=None):
         if not branch and not revision:
             return self.head

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/git_hub.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -132,6 +132,52 @@
             } for reference, commit in (self.commits if type == 'branches' else self.tags).items()
         ], url=""
 
+    def _commits_response(self, url, ref):
+        from datetime import datetime, timedelta
+
+        base = self.commit(ref)
+        if not base:
+            return mocks.Response(
+                status_code=404,
+                url=""
+                text=json.dumps(dict(message='No commit found for SHA: {}'.format(ref))),
+            )
+
+        response = []
+        for branch in [self.default_branch] if base.branch == self.default_branch else [base.branch, self.default_branch]:
+            in_range = False
+            previous = None
+            for commit in reversed(self.commits[branch]):
+                if commit.hash == ref:
+                    in_range = True
+                if not in_range:
+                    continue
+                previous = commit
+                response.append({
+                    'sha': commit.hash,
+                    'commit': {
+                        'author': {
+                            'name': commit.author.name,
+                            'email': commit.author.email,
+                            'date': datetime.utcfromtimestamp(commit.timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ'),
+                        }, 'committer': {
+                            'name': commit.author.name,
+                            'email': commit.author.email,
+                            'date': datetime.utcfromtimestamp(commit.timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ'),
+                        }, 'message': commit.message + ('\ngit-svn-id: https://svn.example.org/repository/webkit/{}@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format(
+                            'trunk' if commit.branch == self.default_branch else commit.branch, commit.revision,
+                        ) if commit.revision else ''),
+                        'url': 'https://{}/git/commits/{}'.format(self.api_remote, commit.hash),
+                    }, 'url': 'https://{}/commits/{}'.format(self.api_remote, commit.hash),
+                    'html_url': 'https://{}/commit/{}'.format(self.remote, commit.hash),
+                })
+            if branch != self.default_branch:
+                for commit in reversed(self.commits[self.default_branch]):
+                    if previous.branch_point == commit.identifier:
+                        ref = commit.hash
+
+        return mocks.Response.fromJson(response, url=""
+
     def _commit_response(self, url, ref):
         from datetime import datetime, timedelta
 
@@ -153,9 +199,9 @@
                     'name': commit.author.name,
                     'email': commit.author.email,
                     'date': datetime.utcfromtimestamp(commit.timestamp - timedelta(hours=7).seconds).strftime('%Y-%m-%dT%H:%M:%SZ'),
-                }, 'message': commit.message + '\ngit-svn-id: https://svn.example.org/repository/webkit/{}@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format(
+                }, 'message': commit.message + ('\ngit-svn-id: https://svn.example.org/repository/webkit/{}@{} 268f45cc-cd09-0410-ab3c-d52691b4dbfc\n'.format(
                     'trunk' if commit.branch == self.default_branch else commit.branch, commit.revision,
-                ) if commit.revision else '',
+                ) if commit.revision else ''),
                 'url': 'https://{}/git/commits/{}'.format(self.api_remote, commit.hash),
             }, 'url': 'https://{}/commits/{}'.format(self.api_remote, commit.hash),
             'html_url': 'https://{}/commit/{}'.format(self.remote, commit.hash),
@@ -233,10 +279,11 @@
             ), url=""
         )
 
-    def request(self, method, url, data="" **kwargs):
+    def request(self, method, url, data="" params=None, **kwargs):
         if not url.startswith('http://') and not url.startswith('https://'):
             return mocks.Response.create404(url)
 
+        params = params or {}
         stripped_url = url.split('://')[-1]
 
         # Top-level API request
@@ -247,6 +294,10 @@
         if stripped_url in ['{}/branches'.format(self.api_remote), '{}/tags'.format(self.api_remote)]:
             return self._list_refs_response(url="" type=stripped_url.split('/')[-1])
 
+        # Return a commit and it's parents
+        if stripped_url == '{}/commits'.format(self.api_remote) and params.get('sha'):
+            return self._commits_response(url="" ref=params['sha'])
+
         # Extract single commit
         if stripped_url.startswith('{}/commits/'.format(self.api_remote)):
             return self._commit_response(url="" ref=stripped_url.split('/')[-1])

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/svn.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/svn.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/remote/svn.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -106,31 +106,18 @@
 
         if category and category.startswith('branches/'):
             category = category.split('/')[-1]
+        category = [category] if category else self.branches(start) + self.tags(start)
 
-        if not category:
-            for commits in self.commits.values():
-                for commit in commits:
-                    if commit.revision == start:
-                        category = commit.branch
-                        break
+        previous = None
+        for b in category + ['trunk']:
+            for candidate in reversed(self.commits.get(b, [])):
+                if candidate.revision > start or candidate.revision < end:
+                    continue
+                if previous and previous.revision <= candidate.revision:
+                    continue
+                previous = candidate
+                yield candidate
 
-        if not category:
-            return []
-
-        result = [commit for commit in reversed(self.commits[category])]
-        if self.commits[category][0].branch_point:
-            result += [commit for commit in reversed(self.commits['trunk'][:self.commits[category][0].branch_point])]
-
-        for index in reversed(range(len(result))):
-            if result[index].revision < end:
-                result = result[:index]
-                continue
-            if result[index].revision > start:
-                result = result[index:]
-                break
-
-        return result
-
     def request(self, method, url, data="" **kwargs):
         from datetime import datetime, timedelta
 
@@ -266,11 +253,11 @@
 
         # Log for commit
         if method == 'REPORT' and stripped_url.startswith('{}!'.format(self.remote)) and match and data.get('S:log-report'):
-            commits = self.range(
+            commits = list(self.range(
                 category=match.group('category'),
                 start=int(data['S:log-report']['S:start-revision']),
                 end=int(data['S:log-report']['S:end-revision']),
-            )
+            ))
 
             limit = int(data['S:log-report'].get('S:limit', 0))
             if limit and len(commits) > limit:

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/git_hub.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -75,7 +75,7 @@
     def is_git(self):
         return True
 
-    def request(self, path=None, params=None, headers=None, authenticated=None):
+    def request(self, path=None, params=None, headers=None, authenticated=None, paginate=True):
         headers = {key: value for key, value in headers.items()} if headers else dict()
         headers['Accept'] = headers.get('Accept', 'application/vnd.github.v3+json')
 
@@ -105,7 +105,7 @@
             return None
         result = response.json()
 
-        while isinstance(response.json(), list) and len(response.json()) == params['per_page']:
+        while paginate and isinstance(response.json(), list) and len(response.json()) == params['per_page']:
             params['page'] += 1
             response = requests.get(url, params=params, headers=headers, auth=auth)
             if response.status_code != 200:
@@ -287,16 +287,22 @@
         # it's possible for a series of commits to share a commit time. To handle this case, we assign each commit a
         # zero-indexed "order" within it's timestamp.
         order = 0
-        while not identifier or order + 1 < identifier + (branch_point or 0):
-            response = self.request('commits/{}'.format('{}~{}'.format(commit_data['sha'], order + 1)))
-            if not response:
+        lhash = commit_data['sha']
+        while lhash:
+            response = self.request('commits', paginate=False, params=dict(sha=lhash, per_page=20))
+            if len(response) <= 1:
                 break
-            parent_timestamp = int(calendar.timegm(datetime.strptime(
-                response['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ',
-            ).timetuple()))
-            if parent_timestamp != timestamp:
-                break
-            order += 1
+            for c in response:
+                if lhash == c['sha']:
+                    continue
+                parent_timestamp = int(calendar.timegm(datetime.strptime(
+                    c['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ',
+                ).timetuple()))
+                if parent_timestamp != timestamp:
+                    lhash = None
+                    break
+                lhash = c['sha']
+                order += 1
 
         return Commit(
             repository_id=self.id,
@@ -313,6 +319,67 @@
             ), message=commit_data['commit']['message'] if include_log else None,
         )
 
+    def commits(self, begin=None, end=None, include_log=True, include_identifier=True):
+        begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier)
+
+        previous = end
+        cached = [previous]
+        while previous:
+            response = self.request('commits', paginate=False, params=dict(sha=previous.hash))
+            if not response:
+                break
+            for commit_data in response:
+                branch_point = previous.branch_point
+                identifier = previous.identifier
+                if commit_data['sha'] == previous.hash:
+                    cached = cached[:-1]
+                else:
+                    identifier -= 1
+
+                if not identifier:
+                    identifier = branch_point
+                    branch_point = None
+
+                matches = self.GIT_SVN_REVISION.findall(commit_data['commit']['message'])
+                revision = int(matches[-1].split('@')[0]) if matches else None
+
+                email_match = self.EMAIL_RE.match(commit_data['commit']['author']['email'])
+                timestamp = int(calendar.timegm(datetime.strptime(
+                    commit_data['commit']['committer']['date'], '%Y-%m-%dT%H:%M:%SZ',
+                ).timetuple()))
+
+                previous = Commit(
+                    repository_id=self.id,
+                    hash=commit_data['sha'],
+                    revision=revision,
+                    branch=end.branch if identifier and branch_point else self.default_branch,
+                    identifier=identifier if include_identifier else None,
+                    branch_point=branch_point if include_identifier else None,
+                    timestamp=timestamp,
+                    author=self.contributors.create(
+                        commit_data['commit']['author']['name'],
+                        email_match.group('email') if email_match else None,
+                    ), order=0,
+                    message=commit_data['commit']['message'] if include_log else None,
+                )
+                if not cached or cached[0].timestamp != previous.timestamp:
+                    for c in cached:
+                        yield c
+                    cached = [previous]
+                else:
+                    for c in cached:
+                        c.order += 1
+                    cached.append(previous)
+
+                if previous.hash == begin.hash or previous.timestamp < begin.timestamp:
+                    previous = None
+                    break
+
+        for c in cached:
+            c.order += begin.order
+            yield c
+
+
     def find(self, argument, include_log=True, include_identifier=True):
         if not isinstance(argument, six.string_types):
             raise ValueError("Expected 'argument' to be a string, not '{}'".format(type(argument)))

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/svn.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/svn.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/remote/svn.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -32,14 +32,14 @@
 
 from datetime import datetime
 
-from webkitcorepy import log, run, decorators
+from webkitcorepy import decorators, string_utils
 from webkitscmpy.remote.scm import Scm
-from webkitscmpy import Commit, Contributor, Version
+from webkitscmpy import Commit, Version
 
 
 class Svn(Scm):
     URL_RE = re.compile(r'\Ahttps?://svn.(?P<host>\S+)/repository/\S+\Z')
-    HISTORY_RE = re.compile(b'<D:version-name>(?P<revision>\d+)</D:version-name>')
+    DATA_RE = re.compile(b'<[SD]:(?P<tag>\S+)>(?P<content>.*)</[SD]:.+>')
     CACHE_VERSION = Version(1)
 
     @classmethod
@@ -244,8 +244,8 @@
 
             default_count = 0
             for line in response.iter_lines():
-                match = self.HISTORY_RE.match(line)
-                if not match:
+                match = self.DATA_RE.match(line)
+                if not match or match.group('tag') != b'version-name':
                     continue
 
                 if not did_warn:
@@ -254,7 +254,7 @@
                         self.log('Caching commit data for {}, this will take a few minutes...'.format(branch))
                         did_warn = True
 
-                revision = int(match.group('revision'))
+                revision = int(match.group('content'))
                 if pos > 0 and self._metadata_cache[branch][pos - 1] == revision:
                     break
                 if not is_default_branch:
@@ -455,3 +455,67 @@
             author=author,
             message=message,
         )
+
+    def _args_from_content(self, content, include_log=True):
+        xml = xmltodict.parse(content)
+        date = datetime.strptime(string_utils.decode(xml['S:log-item']['S:date']).split('.')[0], '%Y-%m-%dT%H:%M:%S')
+        name = string_utils.decode(xml['S:log-item']['D:creator-displayname'])
+
+        return dict(
+            revision=int(xml['S:log-item']['D:version-name']),
+            author=self.contributors.create(name, name) if name and '@' in name else self.contributors.create(name),
+            timestamp=int(calendar.timegm(date.timetuple())),
+            message=string_utils.decode(xml['S:log-item']['D:comment']) if include_log else None,
+        )
+
+    def commits(self, begin=None, end=None, include_log=True, include_identifier=True):
+        begin, end = self._commit_range(begin=begin, end=end, include_identifier=include_identifier)
+        previous = end
+
+        content = b''
+        with requests.request(
+                method='REPORT',
+                url=''.format(
+                    self.url,
+                    end.revision,
+                    end.branch if end.branch == self.default_branch or '/' in end.branch else 'branches/{}'.format(end.branch),
+                ), stream=True,
+                headers={
+                    'Content-Type': 'text/xml',
+                    'Accept-Encoding': 'gzip',
+                    'DEPTH': '1',
+                }, data=''
+                        '<S:start-revision>{end}</S:start-revision>\n'
+                        '<S:end-revision>{begin}</S:end-revision>\n'
+                        '<S:path></S:path>\n'
+                        '</S:log-report>\n'.format(end=end.revision, begin=begin.revision),
+        ) as response:
+            if response.status_code != 200:
+                raise self.Exception("Failed to construct branch history for '{}'".format(branch))
+            for line in response.iter_lines():
+                if line == b'<S:log-item>':
+                    content = line + b'\n'
+                else:
+                    content += line + b'\n'
+                if line != b'</S:log-item>':
+                    continue
+
+                args = self._args_from_content(content, include_log=include_log)
+
+                branch_point = previous.branch_point if include_identifier else None
+                identifier = previous.identifier if include_identifier else None
+                if args['revision'] != previous.revision:
+                    identifier -= 1
+                if not identifier:
+                    identifier = branch_point
+                    branch_point = None
+
+                previous = Commit(
+                    repository_id=self.id,
+                    branch=end.branch if branch_point else self.default_branch,
+                    identifier=identifier,
+                    branch_point=branch_point,
+                    **args
+                )
+                yield previous
+                content = b''

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/scm_base.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -73,6 +73,38 @@
     def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True, include_identifier=True):
         raise NotImplementedError()
 
+    def _commit_range(self, begin=None, end=None, include_log=False, include_identifier=True):
+        begin_args = begin or dict()
+        end_args = end or dict()
+
+        if not begin_args:
+            raise TypeError("_commit_range() missing required 'begin' arguments")
+        if not end_args:
+            raise TypeError("_commit_range() missing required 'end' arguments")
+
+        if list(begin_args.keys()) == ['argument']:
+            begin_result = self.find(include_log=include_log, include_identifier=False, **begin_args)
+        else:
+            begin_result = self.commit(include_log=include_log, include_identifier=False, **begin_args)
+
+        if list(end_args.keys()) == ['argument']:
+            end_result = self.find(include_log=include_log, include_identifier=include_identifier, **end_args)
+        else:
+            end_result = self.commit(include_log=include_log, include_identifier=include_identifier, **end_args)
+
+        if not begin_result:
+            raise TypeError("'{}' failed to define begin in _commit_range()".format(begin_args))
+        if not end_result:
+            raise TypeError("'{}' failed to define begin in _commit_range()".format(end_args))
+        if begin_result.timestamp > end_result.timestamp:
+            raise TypeError("'{}' pre-dates '{}' in _commit_range()".format(begin_result, end_result))
+        if end_result.branch == self.default_branch and begin_result.branch != self.default_branch:
+            raise TypeError("'{}' and '{}' do not share linear history".format(begin_result, end_result))
+        return begin_result, end_result
+
+    def commits(self, begin=None, end=None, include_log=True, include_identifier=True):
+        raise NotImplementedError()
+
     def prioritize_branches(self, branches):
         if len(branches) == 1:
             return branches[0]

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/find_unittest.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/find_unittest.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/find_unittest.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -231,7 +231,7 @@
 Date: {}
 Revision: 4
 Identifier: 3@trunk
-'''.format(datetime.fromtimestamp(1601686700).strftime('%a %b %d %H:%M:%S %Y')),
+'''.format(datetime.fromtimestamp(1601684700).strftime('%a %b %d %H:%M:%S %Y')),
         )
 
 

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -28,7 +28,7 @@
 from datetime import datetime
 from webkitcorepy import LoggerCapture, OutputCapture
 from webkitcorepy.mocks import Time as MockTime
-from webkitscmpy import local, mocks, remote
+from webkitscmpy import Commit, local, mocks, remote
 
 
 class TestGit(unittest.TestCase):
@@ -283,7 +283,29 @@
                 self.assertEqual(0, local.Git(self.path).commit(hash='bae5d1e90999').order)
                 self.assertEqual(1, local.Git(self.path).commit(hash='d8bce26fa65c').order)
 
+    def test_commits(self):
+        for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]:
+            with mock:
+                git = local.Git(self.path)
+                self.assertEqual(Commit.Encoder().default([
+                    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):
+        for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]:
+            with mock:
+                git = local.Git(self.path)
+                self.assertEqual(Commit.Encoder().default([
+                    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')))))
+
+
 class TestGitHub(unittest.TestCase):
     remote = 'https://github.example.com/WebKit/WebKit'
 
@@ -415,7 +437,28 @@
     def test_id(self):
         self.assertEqual(remote.GitHub(self.remote).id, 'webkit')
 
+    def test_commits(self):
+        with mocks.remote.GitHub():
+            git = remote.GitHub(self.remote)
+            self.assertEqual(Commit.Encoder().default([
+                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):
+        with mocks.remote.GitHub():
+            git = remote.GitHub(self.remote)
+            self.assertEqual(Commit.Encoder().default([
+                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')))))
+
+
+
 class TestBitBucket(unittest.TestCase):
     remote = 'https://bitbucket.example.com/projects/WEBKIT/repos/webkit'
 

Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py (277101 => 277102)


--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py	2021-05-06 17:20:55 UTC (rev 277101)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/svn_unittest.py	2021-05-06 18:32:40 UTC (rev 277102)
@@ -27,7 +27,7 @@
 
 from datetime import datetime, timedelta
 from webkitcorepy import OutputCapture
-from webkitscmpy import local, mocks, remote
+from webkitscmpy import Commit, local, mocks, remote
 
 
 class TestLocalSvn(unittest.TestCase):
@@ -231,7 +231,27 @@
         with mocks.local.Svn(self.path), OutputCapture():
             self.assertIsNone(local.Svn(self.path).find('trunk', include_identifier=False).identifier)
 
+    def test_commits(self):
+        with mocks.local.Svn(self.path), OutputCapture():
+            svn = local.Svn(self.path)
+            self.assertEqual(Commit.Encoder().default([
+                svn.commit(revision='r6'),
+                svn.commit(revision='r4'),
+                svn.commit(revision='r2'),
+                svn.commit(revision='r1'),
+            ]), Commit.Encoder().default(list(svn.commits(begin=dict(revision='r1'), end=dict(revision='r6')))))
 
+    def test_commits_branch(self):
+        with mocks.local.Svn(self.path), OutputCapture():
+            svn = local.Svn(self.path)
+            self.assertEqual(Commit.Encoder().default([
+                svn.commit(revision='r7'),
+                svn.commit(revision='r3'),
+                svn.commit(revision='r2'),
+                svn.commit(revision='r1'),
+            ]), Commit.Encoder().default(list(svn.commits(begin=dict(argument='r1'), end=dict(argument='r7')))))
+
+
 class TestRemoteSvn(unittest.TestCase):
     remote = 'https://svn.example.org/repository/webkit'
 
@@ -331,3 +351,24 @@
 
     def test_id(self):
         self.assertEqual(remote.Svn(self.remote).id, 'webkit')
+
+    def test_commits(self):
+        self.maxDiff = None
+        with mocks.remote.Svn():
+            svn = remote.Svn(self.remote)
+            self.assertEqual(Commit.Encoder().default([
+                svn.commit(revision='r6'),
+                svn.commit(revision='r4'),
+                svn.commit(revision='r2'),
+                svn.commit(revision='r1'),
+            ]), Commit.Encoder().default(list(svn.commits(begin=dict(revision='r1'), end=dict(revision='r6')))))
+
+    def test_commits_branch(self):
+        with mocks.remote.Svn(), OutputCapture():
+            svn = remote.Svn(self.remote)
+            self.assertEqual(Commit.Encoder().default([
+                svn.commit(revision='r7'),
+                svn.commit(revision='r3'),
+                svn.commit(revision='r2'),
+                svn.commit(revision='r1'),
+            ]), Commit.Encoder().default(list(svn.commits(begin=dict(argument='r1'), end=dict(argument='r7')))))
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to