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): ...@@ -318,7 +318,7 @@ class FilePatchDiff(FilePatchBase):
Expects the following format: Expects the following format:
<garbagge> <garbage>
diff --git (|a/)<filename> (|b/)<filename> diff --git (|a/)<filename> (|b/)<filename>
<similarity> <similarity>
<filemode changes> <filemode changes>
......
...@@ -328,7 +328,7 @@ def CheckLongLines(input_api, output_api, maxlen, source_file_filter=None): ...@@ -328,7 +328,7 @@ def CheckLongLines(input_api, output_api, maxlen, source_file_filter=None):
SPECIAL_JAVA_STARTS = ('package ', 'import ') SPECIAL_JAVA_STARTS = ('package ', 'import ')
def no_long_lines(file_extension, line): 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): if file_extension == 'java' and line.startswith(SPECIAL_JAVA_STARTS):
return True return True
...@@ -487,7 +487,7 @@ def GetUnitTestsInDirectory( ...@@ -487,7 +487,7 @@ def GetUnitTestsInDirectory(
input_api, output_api, directory, whitelist=None, blacklist=None): input_api, output_api, directory, whitelist=None, blacklist=None):
"""Lists all files in a directory and runs them. Doesn't recurse. """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. tests accordingly.
""" """
unit_tests = [] unit_tests = []
...@@ -547,6 +547,7 @@ def GetUnitTests(input_api, output_api, unit_tests): ...@@ -547,6 +547,7 @@ def GetUnitTests(input_api, output_api, unit_tests):
message=message_type)) message=message_type))
return results return results
def GetPythonUnitTests(input_api, output_api, unit_tests): def GetPythonUnitTests(input_api, output_api, unit_tests):
"""Run the unit tests out of process, capture the output and use the result """Run the unit tests out of process, capture the output and use the result
code to determine success. code to determine success.
......
...@@ -489,11 +489,76 @@ class InputApi(object): ...@@ -489,11 +489,76 @@ class InputApi(object):
return [m for m in msgs if m] 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): class AffectedFile(object):
"""Representation of a file in a change.""" """Representation of a file in a change."""
DIFF_CACHE = _DiffCache
# Method could be a function # Method could be a function
# pylint: disable=R0201 # pylint: disable=R0201
def __init__(self, path, action, repository_root): def __init__(self, path, action, repository_root, diff_cache=None):
self._path = path self._path = path
self._action = action self._action = action
self._local_root = repository_root self._local_root = repository_root
...@@ -501,6 +566,10 @@ class AffectedFile(object): ...@@ -501,6 +566,10 @@ class AffectedFile(object):
self._properties = {} self._properties = {}
self._cached_changed_contents = None self._cached_changed_contents = None
self._cached_new_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)) logging.debug('%s(%s)' % (self.__class__.__name__, self._path))
def ServerPath(self): def ServerPath(self):
...@@ -508,7 +577,7 @@ class AffectedFile(object): ...@@ -508,7 +577,7 @@ class AffectedFile(object):
Returns the empty string if the file does not exist in SCM. Returns the empty string if the file does not exist in SCM.
""" """
return "" return ''
def LocalPath(self): def LocalPath(self):
"""Returns the path of this file on the local disk relative to client root. """Returns the path of this file on the local disk relative to client root.
...@@ -565,22 +634,6 @@ class AffectedFile(object): ...@@ -565,22 +634,6 @@ class AffectedFile(object):
pass # File not found? That's fine; maybe it was deleted. pass # File not found? That's fine; maybe it was deleted.
return self._cached_new_contents[:] 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): def ChangedContents(self):
"""Returns a list of tuples (line number, line text) of all new lines. """Returns a list of tuples (line number, line text) of all new lines.
...@@ -612,7 +665,7 @@ class AffectedFile(object): ...@@ -612,7 +665,7 @@ class AffectedFile(object):
return self.LocalPath() return self.LocalPath()
def GenerateScmDiff(self): def GenerateScmDiff(self):
raise NotImplementedError() # Implemented in derived classes. return self._diff_cache.GetDiff(self.LocalPath(), self._local_root)
class SvnAffectedFile(AffectedFile): class SvnAffectedFile(AffectedFile):
...@@ -620,11 +673,12 @@ class SvnAffectedFile(AffectedFile): ...@@ -620,11 +673,12 @@ class SvnAffectedFile(AffectedFile):
# Method 'NNN' is abstract in class 'NNN' but is not overridden # Method 'NNN' is abstract in class 'NNN' but is not overridden
# pylint: disable=W0223 # pylint: disable=W0223
DIFF_CACHE = _SvnDiffCache
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
AffectedFile.__init__(self, *args, **kwargs) AffectedFile.__init__(self, *args, **kwargs)
self._server_path = None self._server_path = None
self._is_text_file = None self._is_text_file = None
self._diff = None
def ServerPath(self): def ServerPath(self):
if self._server_path is None: if self._server_path is None:
...@@ -664,23 +718,18 @@ class SvnAffectedFile(AffectedFile): ...@@ -664,23 +718,18 @@ class SvnAffectedFile(AffectedFile):
self._is_text_file = (not mime_type or mime_type.startswith('text/')) self._is_text_file = (not mime_type or mime_type.startswith('text/'))
return self._is_text_file 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): class GitAffectedFile(AffectedFile):
"""Representation of a file in a change out of a git checkout.""" """Representation of a file in a change out of a git checkout."""
# Method 'NNN' is abstract in class 'NNN' but is not overridden # Method 'NNN' is abstract in class 'NNN' but is not overridden
# pylint: disable=W0223 # pylint: disable=W0223
DIFF_CACHE = _GitDiffCache
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
AffectedFile.__init__(self, *args, **kwargs) AffectedFile.__init__(self, *args, **kwargs)
self._server_path = None self._server_path = None
self._is_text_file = None self._is_text_file = None
self._diff = None
def ServerPath(self): def ServerPath(self):
if self._server_path is None: if self._server_path is None:
...@@ -714,12 +763,6 @@ class GitAffectedFile(AffectedFile): ...@@ -714,12 +763,6 @@ class GitAffectedFile(AffectedFile):
self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) self._is_text_file = os.path.isfile(self.AbsoluteLocalPath())
return self._is_text_file 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): class Change(object):
"""Describe a change. """Describe a change.
...@@ -728,7 +771,7 @@ class Change(object): ...@@ -728,7 +771,7 @@ class Change(object):
tested. tested.
Instance members: 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'] self.KEY: equivalent to tags['KEY']
""" """
...@@ -769,9 +812,10 @@ class Change(object): ...@@ -769,9 +812,10 @@ class Change(object):
assert all( assert all(
(isinstance(f, (list, tuple)) and len(f) == 2) for f in files), files (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 = [
self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root) self._AFFECTED_FILES(path, action.strip(), self._local_root, diff_cache)
for info in files for action, path in files
] ]
def Name(self): 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