Commit ff526198 authored by nick@chromium.org's avatar nick@chromium.org

presubmit_support: Call git once per change, to get a diff for all files....

presubmit_support: Call git once per change, to get a diff for all files. Parse this result to generate per-file diffs, which go into a cache that's shared between the AffectedFile instances.

Greatly improves presubmit performance on Blink where there may be rule violations (my test case went from 50s to 5s).

BUG=236206
TEST=new test in presubmit_unittest.py; manual performance test on Mac and Windows after touching hundreds of files in webkit; testing included add/move/edit/delete on both binary and text files.

Review URL: https://chromiumcodereview.appspot.com/15898005

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@205275 0039d316-1c4b-4281-b951-d872f2087c98
parent 0bdc2650
......@@ -318,7 +318,7 @@ class FilePatchDiff(FilePatchBase):
Expects the following format:
<garbagge>
<garbage>
diff --git (|a/)<filename> (|b/)<filename>
<similarity>
<filemode changes>
......
......@@ -328,7 +328,7 @@ def CheckLongLines(input_api, output_api, maxlen, source_file_filter=None):
SPECIAL_JAVA_STARTS = ('package ', 'import ')
def no_long_lines(file_extension, line):
# Allow special java statements to be as long as neccessary.
# Allow special java statements to be as long as necessary.
if file_extension == 'java' and line.startswith(SPECIAL_JAVA_STARTS):
return True
......@@ -487,7 +487,7 @@ def GetUnitTestsInDirectory(
input_api, output_api, directory, whitelist=None, blacklist=None):
"""Lists all files in a directory and runs them. Doesn't recurse.
It's mainly a wrapper for RunUnitTests. USe whitelist and blacklist to filter
It's mainly a wrapper for RunUnitTests. Use whitelist and blacklist to filter
tests accordingly.
"""
unit_tests = []
......@@ -547,6 +547,7 @@ def GetUnitTests(input_api, output_api, unit_tests):
message=message_type))
return results
def GetPythonUnitTests(input_api, output_api, unit_tests):
"""Run the unit tests out of process, capture the output and use the result
code to determine success.
......
......@@ -489,11 +489,76 @@ class InputApi(object):
return [m for m in msgs if m]
class _DiffCache(object):
"""Caches diffs retrieved from a particular SCM."""
def GetDiff(self, path, local_root):
"""Get the diff for a particular path."""
raise NotImplementedError()
class _SvnDiffCache(_DiffCache):
"""DiffCache implementation for subversion."""
def __init__(self):
super(_SvnDiffCache, self).__init__()
self._diffs_by_file = {}
def GetDiff(self, path, local_root):
if path not in self._diffs_by_file:
self._diffs_by_file[path] = scm.SVN.GenerateDiff([path], local_root,
False, None)
return self._diffs_by_file[path]
class _GitDiffCache(_DiffCache):
"""DiffCache implementation for git; gets all file diffs at once."""
def __init__(self):
super(_GitDiffCache, self).__init__()
self._diffs_by_file = None
def GetDiff(self, path, local_root):
if not self._diffs_by_file:
# Compute a single diff for all files and parse the output; should
# with git this is much faster than computing one diff for each file.
diffs = {}
# Don't specify any filenames below, because there are command line length
# limits on some platforms and GenerateDiff would fail.
unified_diff = scm.GIT.GenerateDiff(local_root, files=[], full_move=True)
# This regex matches the path twice, separated by a space. Note that
# filename itself may contain spaces.
file_marker = re.compile('^diff --git (?P<filename>.*) (?P=filename)$')
current_diff = []
keep_line_endings = True
for x in unified_diff.splitlines(keep_line_endings):
match = file_marker.match(x)
if match:
# Marks the start of a new per-file section.
diffs[match.group('filename')] = current_diff = [x]
elif x.startswith('diff --git'):
raise PresubmitFailure('Unexpected diff line: %s' % x)
else:
current_diff.append(x)
self._diffs_by_file = dict(
(normpath(path), ''.join(diff)) for path, diff in diffs.items())
if path not in self._diffs_by_file:
raise PresubmitFailure(
'Unified diff did not contain entry for file %s' % path)
return self._diffs_by_file[path]
class AffectedFile(object):
"""Representation of a file in a change."""
DIFF_CACHE = _DiffCache
# Method could be a function
# pylint: disable=R0201
def __init__(self, path, action, repository_root):
def __init__(self, path, action, repository_root, diff_cache=None):
self._path = path
self._action = action
self._local_root = repository_root
......@@ -501,6 +566,10 @@ class AffectedFile(object):
self._properties = {}
self._cached_changed_contents = None
self._cached_new_contents = None
if diff_cache:
self._diff_cache = diff_cache
else:
self._diff_cache = self.DIFF_CACHE()
logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
def ServerPath(self):
......@@ -508,7 +577,7 @@ class AffectedFile(object):
Returns the empty string if the file does not exist in SCM.
"""
return ""
return ''
def LocalPath(self):
"""Returns the path of this file on the local disk relative to client root.
......@@ -565,22 +634,6 @@ class AffectedFile(object):
pass # File not found? That's fine; maybe it was deleted.
return self._cached_new_contents[:]
def OldContents(self):
"""Returns an iterator over the lines in the old version of file.
The old version is the file in depot, i.e. the "left hand side".
"""
raise NotImplementedError() # Implement when needed
def OldFileTempPath(self):
"""Returns the path on local disk where the old contents resides.
The old version is the file in depot, i.e. the "left hand side".
This is a read-only cached copy of the old contents. *DO NOT* try to
modify this file.
"""
raise NotImplementedError() # Implement if/when needed.
def ChangedContents(self):
"""Returns a list of tuples (line number, line text) of all new lines.
......@@ -612,7 +665,7 @@ class AffectedFile(object):
return self.LocalPath()
def GenerateScmDiff(self):
raise NotImplementedError() # Implemented in derived classes.
return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
class SvnAffectedFile(AffectedFile):
......@@ -620,11 +673,12 @@ class SvnAffectedFile(AffectedFile):
# Method 'NNN' is abstract in class 'NNN' but is not overridden
# pylint: disable=W0223
DIFF_CACHE = _SvnDiffCache
def __init__(self, *args, **kwargs):
AffectedFile.__init__(self, *args, **kwargs)
self._server_path = None
self._is_text_file = None
self._diff = None
def ServerPath(self):
if self._server_path is None:
......@@ -664,23 +718,18 @@ class SvnAffectedFile(AffectedFile):
self._is_text_file = (not mime_type or mime_type.startswith('text/'))
return self._is_text_file
def GenerateScmDiff(self):
if self._diff is None:
self._diff = scm.SVN.GenerateDiff(
[self.LocalPath()], self._local_root, False, None)
return self._diff
class GitAffectedFile(AffectedFile):
"""Representation of a file in a change out of a git checkout."""
# Method 'NNN' is abstract in class 'NNN' but is not overridden
# pylint: disable=W0223
DIFF_CACHE = _GitDiffCache
def __init__(self, *args, **kwargs):
AffectedFile.__init__(self, *args, **kwargs)
self._server_path = None
self._is_text_file = None
self._diff = None
def ServerPath(self):
if self._server_path is None:
......@@ -714,12 +763,6 @@ class GitAffectedFile(AffectedFile):
self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
return self._is_text_file
def GenerateScmDiff(self):
if self._diff is None:
self._diff = scm.GIT.GenerateDiff(
self._local_root, files=[self.LocalPath(),])
return self._diff
class Change(object):
"""Describe a change.
......@@ -728,7 +771,7 @@ class Change(object):
tested.
Instance members:
tags: Dictionnary of KEY=VALUE pairs found in the change description.
tags: Dictionary of KEY=VALUE pairs found in the change description.
self.KEY: equivalent to tags['KEY']
"""
......@@ -769,9 +812,10 @@ class Change(object):
assert all(
(isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files
diff_cache = self._AFFECTED_FILES.DIFF_CACHE()
self._affected_files = [
self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root)
for info in files
self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
for action, path in files
]
def Name(self):
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment