Chromium Code Reviews| OLD | NEW |
|---|---|
| 1 #!/usr/bin/env python | 1 #!/usr/bin/env python |
| 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2011 The Chromium Authors. All rights reserved. |
| 3 # Use of this source code is governed by a BSD-style license that can be | 3 # Use of this source code is governed by a BSD-style license that can be |
| 4 # found in the LICENSE file. | 4 # found in the LICENSE file. |
| 5 | 5 |
| 6 """Enables directory-specific presubmit checks to run at upload and/or commit. | 6 """Enables directory-specific presubmit checks to run at upload and/or commit. |
| 7 """ | 7 """ |
| 8 | 8 |
| 9 __version__ = '1.6.1' | 9 __version__ = '1.6.1' |
| 10 | 10 |
| 11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow | 11 # TODO(joi) Add caching where appropriate/needed. The API is designed to allow |
| 12 # caching (between all different invocations of presubmit scripts for a given | 12 # caching (between all different invocations of presubmit scripts for a given |
| 13 # change). We should add it as our presubmit scripts start feeling slow. | 13 # change). We should add it as our presubmit scripts start feeling slow. |
| 14 | 14 |
| 15 import copy | |
|
M-A Ruel
2011/04/23 01:05:03
not needed.
ncarter (slow)
2011/04/26 17:15:19
Done.
| |
| 15 import cPickle # Exposed through the API. | 16 import cPickle # Exposed through the API. |
| 16 import cStringIO # Exposed through the API. | 17 import cStringIO # Exposed through the API. |
| 17 import fnmatch | 18 import fnmatch |
| 18 import glob | 19 import glob |
| 19 import logging | 20 import logging |
| 20 import marshal # Exposed through the API. | 21 import marshal # Exposed through the API. |
| 21 import optparse | 22 import optparse |
| 22 import os # Somewhat exposed through the API. | 23 import os # Somewhat exposed through the API. |
| 23 import pickle # Exposed through the API. | 24 import pickle # Exposed through the API. |
| 24 import random | 25 import random |
| (...skipping 364 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 389 | 390 |
| 390 This is useful for doing line-by-line regex checks, like checking for | 391 This is useful for doing line-by-line regex checks, like checking for |
| 391 trailing whitespace. | 392 trailing whitespace. |
| 392 | 393 |
| 393 Yields: | 394 Yields: |
| 394 a 3 tuple: | 395 a 3 tuple: |
| 395 the AffectedFile instance of the current file; | 396 the AffectedFile instance of the current file; |
| 396 integer line number (1-based); and | 397 integer line number (1-based); and |
| 397 the contents of the line as a string. | 398 the contents of the line as a string. |
| 398 | 399 |
| 399 Note: The cariage return (LF or CR) is stripped off. | 400 Note: The carriage return (LF or CR) is stripped off. |
| 400 """ | 401 """ |
| 401 files = self.AffectedSourceFiles(source_file_filter) | 402 files = self.AffectedSourceFiles(source_file_filter) |
| 402 return _RightHandSideLinesImpl(files) | 403 return _RightHandSideLinesImpl(files) |
| 403 | 404 |
| 404 def ReadFile(self, file_item, mode='r'): | 405 def ReadFile(self, file_item, mode='r'): |
| 405 """Reads an arbitrary file. | 406 """Reads an arbitrary file. |
| 406 | 407 |
| 407 Deny reading anything outside the repository. | 408 Deny reading anything outside the repository. |
| 408 """ | 409 """ |
| 409 if isinstance(file_item, AffectedFile): | 410 if isinstance(file_item, AffectedFile): |
| 410 file_item = file_item.AbsoluteLocalPath() | 411 file_item = file_item.AbsoluteLocalPath() |
| 411 if not file_item.startswith(self.change.RepositoryRoot()): | 412 if not file_item.startswith(self.change.RepositoryRoot()): |
| 412 raise IOError('Access outside the repository root is denied.') | 413 raise IOError('Access outside the repository root is denied.') |
| 413 return gclient_utils.FileRead(file_item, mode) | 414 return gclient_utils.FileRead(file_item, mode) |
| 414 | 415 |
| 415 | 416 |
| 416 class AffectedFile(object): | 417 class AffectedFile(object): |
| 417 """Representation of a file in a change.""" | 418 """Representation of a file in a change.""" |
| 418 # Method could be a function | 419 # Method could be a function |
| 419 # pylint: disable=R0201 | 420 # pylint: disable=R0201 |
| 420 def __init__(self, path, action, repository_root=''): | 421 def __init__(self, path, action, repository_root=''): |
| 421 self._path = path | 422 self._path = path |
| 422 self._action = action | 423 self._action = action |
| 423 self._local_root = repository_root | 424 self._local_root = repository_root |
| 424 self._is_directory = None | 425 self._is_directory = None |
| 425 self._properties = {} | 426 self._properties = {} |
| 427 self._cached_changed_contents = None | |
| 428 self._cached_new_contents = None | |
| 426 logging.debug('%s(%s)' % (self.__class__.__name__, self._path)) | 429 logging.debug('%s(%s)' % (self.__class__.__name__, self._path)) |
| 427 | 430 |
| 428 def ServerPath(self): | 431 def ServerPath(self): |
| 429 """Returns a path string that identifies the file in the SCM system. | 432 """Returns a path string that identifies the file in the SCM system. |
| 430 | 433 |
| 431 Returns the empty string if the file does not exist in SCM. | 434 Returns the empty string if the file does not exist in SCM. |
| 432 """ | 435 """ |
| 433 return "" | 436 return "" |
| 434 | 437 |
| 435 def LocalPath(self): | 438 def LocalPath(self): |
| (...skipping 32 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 468 Deleted files are not text file.""" | 471 Deleted files are not text file.""" |
| 469 raise NotImplementedError() # Implement when needed | 472 raise NotImplementedError() # Implement when needed |
| 470 | 473 |
| 471 def NewContents(self): | 474 def NewContents(self): |
| 472 """Returns an iterator over the lines in the new version of file. | 475 """Returns an iterator over the lines in the new version of file. |
| 473 | 476 |
| 474 The new version is the file in the user's workspace, i.e. the "right hand | 477 The new version is the file in the user's workspace, i.e. the "right hand |
| 475 side". | 478 side". |
| 476 | 479 |
| 477 Contents will be empty if the file is a directory or does not exist. | 480 Contents will be empty if the file is a directory or does not exist. |
| 478 Note: The cariage returns (LF or CR) are stripped off. | 481 Note: The carriage returns (LF or CR) are stripped off. |
| 479 """ | 482 """ |
| 480 if self.IsDirectory(): | 483 if self._cached_new_contents is None: |
| 481 return [] | 484 if self.IsDirectory(): |
| 482 else: | 485 self._cached_new_contents = [] |
| 483 return gclient_utils.FileRead(self.AbsoluteLocalPath(), | 486 else: |
| 484 'rU').splitlines() | 487 self._cached_new_contents = gclient_utils.FileRead( |
|
M-A Ruel
2011/04/23 01:05:03
It should trap IOError and set to []. The file cou
ncarter (slow)
2011/04/26 17:15:19
Done. Also, added unittest.
| |
| 488 self.AbsoluteLocalPath(), 'rU').splitlines() | |
| 489 return copy.deepcopy(self._cached_new_contents) | |
|
M-A Ruel
2011/04/23 01:05:03
return self._cached_new_contents[:]
ncarter (slow)
2011/04/26 17:15:19
Of course. Done.
| |
| 485 | 490 |
| 486 def OldContents(self): | 491 def OldContents(self): |
| 487 """Returns an iterator over the lines in the old version of file. | 492 """Returns an iterator over the lines in the old version of file. |
| 488 | 493 |
| 489 The old version is the file in depot, i.e. the "left hand side". | 494 The old version is the file in depot, i.e. the "left hand side". |
| 490 """ | 495 """ |
| 491 raise NotImplementedError() # Implement when needed | 496 raise NotImplementedError() # Implement when needed |
| 492 | 497 |
| 493 def OldFileTempPath(self): | 498 def OldFileTempPath(self): |
| 494 """Returns the path on local disk where the old contents resides. | 499 """Returns the path on local disk where the old contents resides. |
| 495 | 500 |
| 496 The old version is the file in depot, i.e. the "left hand side". | 501 The old version is the file in depot, i.e. the "left hand side". |
| 497 This is a read-only cached copy of the old contents. *DO NOT* try to | 502 This is a read-only cached copy of the old contents. *DO NOT* try to |
| 498 modify this file. | 503 modify this file. |
| 499 """ | 504 """ |
| 500 raise NotImplementedError() # Implement if/when needed. | 505 raise NotImplementedError() # Implement if/when needed. |
| 501 | 506 |
| 502 def ChangedContents(self): | 507 def ChangedContents(self): |
| 503 """Returns a list of tuples (line number, line text) of all new lines. | 508 """Returns a list of tuples (line number, line text) of all new lines. |
| 504 | 509 |
| 505 This relies on the scm diff output describing each changed code section | 510 This relies on the scm diff output describing each changed code section |
| 506 with a line of the form | 511 with a line of the form |
| 507 | 512 |
| 508 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ | 513 ^@@ <old line num>,<old size> <new line num>,<new size> @@$ |
| 509 """ | 514 """ |
| 510 new_lines = [] | 515 if self._cached_changed_contents is not None: |
| 516 return copy.deepcopy(self._cached_changed_contents) | |
| 517 self._cached_changed_contents = [] | |
| 511 line_num = 0 | 518 line_num = 0 |
| 512 | 519 |
| 513 if self.IsDirectory(): | 520 if self.IsDirectory(): |
| 514 return [] | 521 return [] |
| 515 | 522 |
| 516 for line in self.GenerateScmDiff().splitlines(): | 523 for line in self.GenerateScmDiff().splitlines(): |
| 517 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line) | 524 m = re.match(r'^@@ [0-9\,\+\-]+ \+([0-9]+)\,[0-9]+ @@', line) |
| 518 if m: | 525 if m: |
| 519 line_num = int(m.groups(1)[0]) | 526 line_num = int(m.groups(1)[0]) |
| 520 continue | 527 continue |
| 521 if line.startswith('+') and not line.startswith('++'): | 528 if line.startswith('+') and not line.startswith('++'): |
| 522 new_lines.append((line_num, line[1:])) | 529 self._cached_changed_contents.append((line_num, line[1:])) |
| 523 if not line.startswith('-'): | 530 if not line.startswith('-'): |
| 524 line_num += 1 | 531 line_num += 1 |
| 525 return new_lines | 532 return copy.deepcopy(self._cached_changed_contents) |
| 526 | 533 |
| 527 def __str__(self): | 534 def __str__(self): |
| 528 return self.LocalPath() | 535 return self.LocalPath() |
| 529 | 536 |
| 530 def GenerateScmDiff(self): | 537 def GenerateScmDiff(self): |
| 531 raise NotImplementedError() # Implemented in derived classes. | 538 raise NotImplementedError() # Implemented in derived classes. |
| 532 | 539 |
| 533 | 540 |
| 534 class SvnAffectedFile(AffectedFile): | 541 class SvnAffectedFile(AffectedFile): |
| 535 """Representation of a file in a change out of a Subversion checkout.""" | 542 """Representation of a file in a change out of a Subversion checkout.""" |
| (...skipping 677 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 1213 except PresubmitFailure, e: | 1220 except PresubmitFailure, e: |
| 1214 print >> sys.stderr, e | 1221 print >> sys.stderr, e |
| 1215 print >> sys.stderr, 'Maybe your depot_tools is out of date?' | 1222 print >> sys.stderr, 'Maybe your depot_tools is out of date?' |
| 1216 print >> sys.stderr, 'If all fails, contact maruel@' | 1223 print >> sys.stderr, 'If all fails, contact maruel@' |
| 1217 return 2 | 1224 return 2 |
| 1218 | 1225 |
| 1219 | 1226 |
| 1220 if __name__ == '__main__': | 1227 if __name__ == '__main__': |
| 1221 fix_encoding.fix_encoding() | 1228 fix_encoding.fix_encoding() |
| 1222 sys.exit(Main(None)) | 1229 sys.exit(Main(None)) |
| OLD | NEW |