Diff
Modified: trunk/Tools/ChangeLog (279444 => 279445)
--- trunk/Tools/ChangeLog 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/ChangeLog 2021-07-01 00:16:18 UTC (rev 279445)
@@ -1,3 +1,41 @@
+2021-06-30 Jonathan Bedard <jbed...@apple.com>
+
+ [webkitscmpy] Cache identifiers in Git checkouts
+ https://bugs.webkit.org/show_bug.cgi?id=225616
+ <rdar://problem/77789230>
+
+ Reviewed by Dewei Zhu.
+
+ Computing identifiers in git can be slow, especially if you need to compute
+ multiple. Caching all identifiers for a branch is not much more expensive than calculating a single identifier. Additionally, caching all identifiers
+ lets us build a performant subversion mapping, bypassing git-svn.
+
+ * Scripts/libraries/webkitcorepy/setup.py:
+ * Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py:
+ * Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py:
+ (Git.Cache): Class holding a cache allowing quick mapping between hashes, identifiers and revisions.
+ (Git.Cache.__init__): Load cache from json file on disk.
+ (Git.Cache.path): Path to location of cache on disk.
+ (Git.Cache._fill): Populate revision and hash to identifier dictionaries.
+ (Git.Cache.populate): Parse git-log and populate cache.
+ (Git.Cache.hash): Given an identifier or revision, find the hash.
+ (Git.Cache.revision): Given an identifier or hash, find the revision.
+ (Git.Cache.identifier): Given a hash or revision, determine the identifier.
+ (Git.__init__): Instantiate cache.
+ (Git.commit): Check cache before running expensive commands.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py: Add `git log` mock.
+ * Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py:
+ (TestGit.test_tag): Surpress logging.
+ (TestGit.test_checkout): Ditto.
+ (TestGit.test_no_log): Ditto.
+ (TestGit.test_order): Ditto.
+ (test_cache): Verify identifier cache.
+ (test_revision_cache): Verify revision cache.
+ * Scripts/webkitpy/layout_tests/models/test_run_results_unittest.py:
+ (SummarizedResultsTest.test_svn_revision_git): Surpress logging.
+ * Scripts/webkitpy/port/base_unittest.py:
+ (PortTest.test_commits_for_upload_git_svn): Surpress logging.
+
2021-06-30 Megan Gardner <megan_gard...@apple.com>
Add ID and versioning support for AppHighlights
Modified: trunk/Tools/Scripts/libraries/webkitcorepy/setup.py (279444 => 279445)
--- trunk/Tools/Scripts/libraries/webkitcorepy/setup.py 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/setup.py 2021-07-01 00:16:18 UTC (rev 279445)
@@ -30,7 +30,7 @@
setup(
name='webkitcorepy',
- version='0.7.0',
+ version='0.7.1',
description='Library containing various Python support classes and functions.',
long_description=readme(),
classifiers=[
Modified: trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py (279444 => 279445)
--- trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py 2021-07-01 00:16:18 UTC (rev 279445)
@@ -39,7 +39,7 @@
from webkitcorepy.measure_time import MeasureTime
from webkitcorepy.nested_fuzzy_dict import NestedFuzzyDict
-version = Version(0, 7, 0)
+version = Version(0, 7, 1)
from webkitcorepy.autoinstall import Package, AutoInstall
if sys.version_info > (3, 0):
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py (279444 => 279445)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/local/git.py 2021-07-01 00:16:18 UTC (rev 279445)
@@ -23,6 +23,7 @@
import calendar
import logging
import os
+import json
import re
import six
import subprocess
@@ -31,12 +32,223 @@
from datetime import datetime, timedelta
-from webkitcorepy import run, decorators
+from webkitcorepy import run, decorators, NestedFuzzyDict
from webkitscmpy.local import Scm
from webkitscmpy import Commit, Contributor, log
class Git(Scm):
+ class Cache(object):
+ def __init__(self, repo, guranteed_for=10):
+ self.repo = repo
+ self._ordered_commits = {}
+ self._hash_to_identifiers = NestedFuzzyDict(primary_size=6)
+ self._ordered_revisions = {}
+ self._revisions_to_identifiers = {}
+ self._last_populated = {}
+ self._guranteed_for = guranteed_for
+
+ if not os.path.exists(self.path):
+ return
+
+ try:
+ with open(self.path) as file:
+ content = json.load(file)
+ self._ordered_commits = content['hashes']
+ self._ordered_revisions = content['revisions']
+
+ self._fill(self.repo.default_branch)
+ for branch in self._ordered_commits.keys():
+ if branch == self.repo.default_branch:
+ continue
+ self._fill(branch)
+ except BaseException:
+ pass
+
+ @property
+ def path(self):
+ return os.path.join(self.repo.root_path, '.git', 'identifiers.json')
+
+ def _fill(self, branch):
+ default_branch = self.repo.default_branch
+ if branch == default_branch:
+ branch_point = None
+ else:
+ branch_point = int(self._hash_to_identifiers[self._ordered_commits[branch][0]].split('@')[0])
+
+ index = len(self._ordered_commits[branch]) - 1
+ while index:
+ identifier = self._hash_to_identifiers.get(self._ordered_commits[branch][index])
+
+ if identifier:
+ id_branch = identifier.split('@')[-1]
+ if branch in (default_branch, id_branch):
+ break
+ if branch != self.repo.prioritize_branches((branch, id_branch)):
+ break
+
+ identifier = '{}@{}'.format('{}.{}'.format(branch_point, index) if branch_point else index, branch)
+ self._hash_to_identifiers[self._ordered_commits[branch][index]] = identifier
+ if self._ordered_revisions[branch][index]:
+ self._revisions_to_identifiers[self._ordered_revisions[branch][index]] = identifier
+ index -= 1
+
+ def populate(self, branch=None):
+ branch = branch or self.repo.branch
+ if not branch:
+ return
+ if self._last_populated.get(branch, 0) + self._guranteed_for > time.time():
+ return
+ default_branch = self.repo.default_branch
+ is_default_branch = branch == default_branch
+ if branch not in self._ordered_commits:
+ self._ordered_commits[branch] = [''] if is_default_branch else []
+ self._ordered_revisions[branch] = [0] if is_default_branch else []
+
+ # If we aren't on the default branch, we will need the default branch to determine when
+ # our branch intersects with the default branch.
+ if not is_default_branch:
+ self.populate(branch=self.repo.default_branch)
+ hashes = []
+ revisions = []
+
+ def _append(branch, hash, revision=None):
+ hashes.append(hash)
+ revisions.append(revision)
+ identifier = self._hash_to_identifiers.get(hash, '')
+ return identifier.endswith(default_branch) or identifier.endswith(branch)
+
+ intersected = False
+ log = None
+ try:
+ kwargs = dict()
+ if sys.version_info >= (3, 0):
+ kwargs = dict(encoding='utf-8')
+ self._last_populated[branch] = time.time()
+ log = subprocess.Popen(
+ [self.repo.executable(), 'log', branch],
+ cwd=self.repo.root_path,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ ** kwargs
+ )
+ if log.poll():
+ raise self.repo.Exception("Failed to construct branch history for '{}'".format(branch))
+
+ hash = None
+ revision = None
+
+ line = log.stdout.readline()
+ while line:
+ if line.startswith(' git-svn-id: '):
+ match = self.repo.GIT_SVN_REVISION.match(line.lstrip())
+ if match:
+ revision = int(match.group('revision'))
+ if not line.startswith('commit '):
+ line = log.stdout.readline()
+ continue
+
+ if hash and _append(branch, hash, revision=revision):
+ hash = None
+ intersected = True
+ break
+
+ hash = line.split(' ')[1].rstrip()
+ revision = None
+ line = log.stdout.readline()
+
+ if hash:
+ intersected = _append(branch, hash, revision=revision)
+
+ finally:
+ if log:
+ log.kill()
+
+ if not hashes or intersected and len(hashes) <= 1:
+ return
+
+ hashes.reverse()
+ revisions.reverse()
+
+ order = len(self._ordered_commits[branch]) - 1
+ while order > 0:
+ if hashes[0] == self._ordered_commits[branch][order]:
+ order -= 1
+ break
+ order -= 1
+
+ self._ordered_commits[branch] = self._ordered_commits[branch][:order + 1] + hashes
+ self._ordered_revisions[branch] = self._ordered_revisions[branch][:order + 1] + revisions
+ self._fill(branch)
+
+ try:
+ with open(self.path, 'w') as file:
+ json.dump(dict(
+ hashes=self._ordered_commits,
+ revisions=self._ordered_revisions,
+ ), file, indent=4)
+ except (IOError, OSError):
+ self.repo.log("Failed to write identifier cache to '{}'".format(self.path))
+
+ def to_hash(self, revision=None, identifier=None, populate=True, branch=None):
+ if revision:
+ identifier = self.to_identifier(revision=revision, populate=populate, branch=branch)
+ parts = Commit._parse_identifier(identifier, do_assert=False)
+ if not parts:
+ return None
+
+ _, b_count, branch = parts
+ if b_count < 0:
+ return None
+ if branch not in self._ordered_commits or len(self._ordered_commits[branch]) <= b_count:
+ if populate:
+ self.populate(branch=branch)
+ return self.to_hash(identifier=identifier, populate=False)
+ return None
+ return self._ordered_commits[branch][b_count]
+
+ def to_revision(self, hash=None, identifier=None, populate=True, branch=None):
+ if hash:
+ identifier = self.to_identifier(hash=hash, populate=populate, branch=branch)
+ parts = Commit._parse_identifier(identifier, do_assert=False)
+ if not parts:
+ return None
+
+ _, b_count, branch = parts
+ if b_count < 0:
+ return None
+ if branch not in self._ordered_revisions or len(self._ordered_revisions[branch]) <= b_count:
+ if populate:
+ self.populate(branch=branch)
+ return self.to_revision(identifier=identifier, populate=False)
+ return None
+ return self._ordered_revisions[branch][b_count]
+
+ def to_identifier(self, hash=None, revision=None, populate=True, branch=None):
+ revision = Commit._parse_revision(revision, do_assert=False)
+ if revision:
+ if revision in self._revisions_to_identifiers:
+ return self._revisions_to_identifiers[revision]
+ if populate:
+ self.populate(branch=branch)
+ return self.to_identifier(revision=revision, populate=False)
+ return None
+
+ hash = Commit._parse_hash(hash, do_assert=False)
+ if hash:
+ try:
+ candidate = self._hash_to_identifiers.get(hash)
+ except KeyError: # Means the hash wasn't specific enough
+ return None
+
+ if candidate:
+ return candidate
+ if populate:
+ self.populate(branch=branch)
+ return self.to_identifier(hash=hash, populate=False)
+ return None
+
+
GIT_COMMIT = re.compile(r'commit (?P<hash>[0-9a-f]+)')
@classmethod
@@ -51,6 +263,7 @@
def __init__(self, path, dev_branches=None, prod_branches=None, contributors=None, id=None):
super(Git, self).__init__(path, dev_branches=dev_branches, prod_branches=prod_branches, contributors=contributors, id=id)
self._branch = None
+ self.cache = self.Cache(self) if self.root_path else None
if not self.root_path:
raise OSError('Provided path {} is not a git repository'.format(path))
@@ -163,12 +376,18 @@
return sorted(set(['/'.join(branch.split('/')[2:]) if branch.startswith('remotes/origin/') else branch for branch in result]))
def commit(self, hash=None, revision=None, identifier=None, branch=None, tag=None, include_log=True, include_identifier=True):
- # Only git-svn checkouts can convert revisions to fully qualified commits
- if revision and not self.is_svn:
+ # Only git-svn checkouts can convert revisions to fully qualified commits, unless we happen to have a SVN cache built
+ if revision:
+ if hash:
+ raise ValueError('Cannot define both hash and revision')
+ hash = self.cache.to_hash(revision=revision, branch=branch) if self.cache else None
+
+ # If we don't have an SVN cache built, and we're not git-svn, we can't reason about revisions
+ if revision and not hash and not self.is_svn:
raise self.Exception('This git checkout does not support SVN revisions')
# Determine the hash for a provided Subversion revision
- elif revision:
+ elif revision and not hash:
if hash:
raise ValueError('Cannot define both hash and revision')
@@ -206,34 +425,48 @@
),
)
branch = parsed_branch
+ hash = self.cache.to_hash(identifier='{}@{}'.format(identifier, parsed_branch), branch=branch) if self.cache else None
- baseline = branch or 'HEAD'
- is_default = baseline == default_branch
- if baseline == 'HEAD':
- is_default = default_branch in self._branches_for(baseline)
+ # If the cache managed to convert the identifier to a hash, we can skip some computation
+ if hash:
+ log = run(
+ [self.executable(), 'log', hash] + log_format,
+ cwd=self.root_path,
+ capture_output=True,
+ encoding='utf-8',
+ )
+ if log.returncode:
+ raise self.Exception("Failed to retrieve commit information for '{}'".format(hash))
- if is_default and parsed_branch_point:
- raise self.Exception('Cannot provide a branch point for a commit on the default branch')
+ # The cache has failed to convert the identifier, we need to do it the expensive way
+ else:
+ baseline = branch or 'HEAD'
+ is_default = baseline == default_branch
+ if baseline == 'HEAD':
+ is_default = default_branch in self._branches_for(baseline)
- base_count = self._commit_count(baseline if is_default else '{}..{}'.format(default_branch, baseline))
+ if is_default and parsed_branch_point:
+ raise self.Exception('Cannot provide a branch point for a commit on the default branch')
- if identifier > base_count:
- raise self.Exception('Identifier {} cannot be found on the specified branch in the current checkout'.format(identifier))
- log = run(
- [self.executable(), 'log', '{}~{}'.format(branch or 'HEAD', base_count - identifier)] + log_format,
- cwd=self.root_path,
- capture_output=True,
- encoding='utf-8',
- )
- if log.returncode:
- raise self.Exception("Failed to retrieve commit information for 'i{}@{}'".format(identifier, branch or 'HEAD'))
+ base_count = self._commit_count(baseline if is_default else '{}..{}'.format(default_branch, baseline))
- # Negative identifiers are actually commits on the default branch, we will need to re-compute the identifier
- if identifier < 0 and is_default:
- raise self.Exception('Illegal negative identifier on the default branch')
- if identifier < 0:
- identifier = None
+ if identifier > base_count:
+ raise self.Exception('Identifier {} cannot be found on the specified branch in the current checkout'.format(identifier))
+ log = run(
+ [self.executable(), 'log', '{}~{}'.format(branch or 'HEAD', base_count - identifier)] + log_format,
+ cwd=self.root_path,
+ capture_output=True,
+ encoding='utf-8',
+ )
+ if log.returncode:
+ raise self.Exception("Failed to retrieve commit information for 'i{}@{}'".format(identifier, branch or 'HEAD'))
+ # Negative identifiers are actually commits on the default branch, we will need to re-compute the identifier
+ if identifier < 0 and is_default:
+ raise self.Exception('Illegal negative identifier on the default branch')
+ if identifier < 0:
+ identifier = None
+
# Determine the `git log` output for a given branch or tag
elif branch or tag:
if hash:
@@ -258,15 +491,23 @@
raise self.Exception('Invalid commit hash in git log')
hash = match.group('hash')
+ branch_point = None
# A commit is often on multiple branches, the canonical branch is the one with the highest priority
- branch = self.prioritize_branches(self._branches_for(hash))
+ if branch != default_branch:
+ branch = self.prioritize_branches(self._branches_for(hash))
+ if not identifier and include_identifier:
+ cached_identifier = self.cache.to_identifier(hash=hash, branch=branch) if self.cache else None
+ if cached_identifier:
+ branch_point, identifier, branch = Commit._parse_identifier(cached_identifier)
+
# Compute the identifier if the function did not receive one and we were asked to
if not identifier and include_identifier:
identifier = self._commit_count(hash if branch == default_branch else '{}..{}'.format(default_branch, hash))
# Only compute the branch point we're on something other than the default branch
- branch_point = None if not include_identifier or branch == default_branch else self._commit_count(hash) - identifier
+ if not branch_point and include_identifier and branch != default_branch:
+ branch_point = self._commit_count(hash) - identifier
if branch_point and parsed_branch_point and branch_point != parsed_branch_point:
raise ValueError("Provided 'branch_point' does not match branch point of specified branch")
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py (279444 => 279445)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/mocks/local/git.py 2021-07-01 00:16:18 UTC (rev 279445)
@@ -270,6 +270,31 @@
])
)
), mocks.Subprocess.Route(
+ self.executable, 'log', re.compile(r'.+'),
+ cwd=self.path,
+ generator=lambda *args, **kwargs: mocks.ProcessCompletion(
+ returncode=0,
+ stdout='\n'.join([
+ 'commit {hash}\n'
+ 'Author: {author} <{email}>\n'
+ 'Date: {date}\n'
+ '\n{log}\n'.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(self.commits[self.default_branch][0].hash, args[2])
+ ])
+ )
+ ), mocks.Subprocess.Route(
self.executable, 'rev-list', '--count', '--no-merges', re.compile(r'.+'),
cwd=self.path,
generator=lambda *args, **kwargs: mocks.ProcessCompletion(
@@ -511,7 +536,7 @@
branches = [self.default_branch]
if end in self.commits.keys() and end != self.default_branch:
- branches.append(end)
+ branches.insert(0, end)
else:
for branch, commits in self.commits.items():
if branch == self.default_branch:
Modified: trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py (279444 => 279445)
--- trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/Scripts/libraries/webkitscmpy/webkitscmpy/test/git_unittest.py 2021-07-01 00:16:18 UTC (rev 279445)
@@ -225,7 +225,7 @@
def test_tag(self):
for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]:
- with mock:
+ with mock, LoggerCapture():
mock.tags['tag-1'] = mock.commits['branch-a'][-1]
self.assertEqual(
@@ -235,7 +235,7 @@
def test_checkout(self):
for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]:
- with mock:
+ with mock, LoggerCapture():
mock.tags['tag-1'] = mock.commits['branch-a'][-1]
repository = local.Git(self.path)
@@ -251,7 +251,7 @@
def test_no_log(self):
for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]:
- with mock:
+ with mock, LoggerCapture():
self.assertIsNone(local.Git(self.path).commit(identifier='4@main', include_log=False).message)
def test_alternative_default_branch(self):
@@ -267,7 +267,7 @@
def test_order(self):
for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]:
- with mock:
+ with mock, LoggerCapture():
self.assertEqual(0, local.Git(self.path).commit(hash='bae5d1e90999').order)
self.assertEqual(1, local.Git(self.path).commit(hash='d8bce26fa65c').order)
@@ -365,7 +365,33 @@
'''
)
+ def test_cache(self):
+ for mock in [mocks.local.Git(self.path), mocks.local.Git(self.path, git_svn=True)]:
+ with mock, OutputCapture():
+ repo = local.Git(self.path)
+ self.assertEqual(repo.cache.to_hash(identifier='1@main'), '9b8311f25a77ba14923d9d5a6532103f54abefcb')
+ self.assertEqual(repo.cache.to_identifier(hash='d8bce26fa65c'), '5@main')
+ self.assertEqual(repo.cache.to_hash(identifier='2.3@branch-b'), '790725a6d79e28db2ecdde29548d2262c0bd059d')
+ self.assertEqual(repo.cache.to_hash(identifier='2.1@branch-a'), 'a30ce8494bf1ac2807a69844f726be4a9843ca55')
+ self.assertEqual(repo.cache.to_identifier(hash='a30ce8494bf1'), '2.1@branch-a')
+
+ self.assertEqual(repo.cache.to_identifier(hash='badc0dd1f'), None)
+ self.assertEqual(repo.cache.to_hash(identifier='6@main'), None)
+
+ def test_revision_cache(self):
+ with mocks.local.Git(self.path, git_svn=True), OutputCapture():
+ repo = local.Git(self.path)
+
+ self.assertEqual(repo.cache.to_revision(identifier='1@main'), 1)
+ self.assertEqual(repo.cache.to_identifier(revision='r9'), '5@main')
+ self.assertEqual(repo.cache.to_hash(revision='r9'), 'd8bce26fa65c6fc8f39c17927abb77f69fab82fc')
+
+ self.assertEqual(repo.cache.to_identifier(revision=100), None)
+ self.assertEqual(repo.cache.to_revision(hash='badc0dd1f'), None)
+ self.assertEqual(repo.cache.to_revision(identifier='6@main'), None)
+
+
class TestGitHub(testing.TestCase):
remote = 'https://github.example.com/WebKit/WebKit'
Modified: trunk/Tools/Scripts/webkitpy/layout_tests/models/test_run_results_unittest.py (279444 => 279445)
--- trunk/Tools/Scripts/webkitpy/layout_tests/models/test_run_results_unittest.py 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/Scripts/webkitpy/layout_tests/models/test_run_results_unittest.py 2021-07-01 00:16:18 UTC (rev 279445)
@@ -35,6 +35,7 @@
from webkitpy.layout_tests.models import test_run_results
from webkitpy.tool.mocktool import MockOptions
+from webkitcorepy import OutputCapture
from webkitscmpy import mocks
@@ -166,7 +167,7 @@
self.assertEquals(summary['revision'], '6')
def test_svn_revision_git(self):
- with mocks.local.Svn(), mocks.local.Git(path='/', git_svn=True):
+ with mocks.local.Svn(), mocks.local.Git(path='/', git_svn=True), OutputCapture():
self.port._options.builder_name = 'dummy builder'
summary = summarized_results(self.port, expected=False, passing=False, flaky=False)
self.assertEquals(summary['revision'], '9')
Modified: trunk/Tools/Scripts/webkitpy/port/base_unittest.py (279444 => 279445)
--- trunk/Tools/Scripts/webkitpy/port/base_unittest.py 2021-06-30 23:54:40 UTC (rev 279444)
+++ trunk/Tools/Scripts/webkitpy/port/base_unittest.py 2021-07-01 00:16:18 UTC (rev 279445)
@@ -329,7 +329,7 @@
self.assertEqual([{'repository_id': 'webkit', 'id': '6', 'branch': 'trunk'}], port.commits_for_upload())
def test_commits_for_upload_git_svn(self):
- with mocks.local.Svn(), mocks.local.Git(path='/', git_svn=True):
+ with mocks.local.Svn(), mocks.local.Git(path='/', git_svn=True), OutputCapture():
port = self.make_port(port_name='foo')
self.assertEqual([{
'repository_id': 'webkit',