| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2006-2009 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2006-2009 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.3.2' | 9 __version__ = '1.3.2' |
| 10 | 10 |
| (...skipping 325 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 336 integer line number (1-based); and | 336 integer line number (1-based); and |
| 337 the contents of the line as a string. | 337 the contents of the line as a string. |
| 338 | 338 |
| 339 Note: The cariage return (LF or CR) is stripped off. | 339 Note: The cariage return (LF or CR) is stripped off. |
| 340 """ | 340 """ |
| 341 files = self.AffectedSourceFiles(source_file_filter) | 341 files = self.AffectedSourceFiles(source_file_filter) |
| 342 return InputApi._RightHandSideLinesImpl(files) | 342 return InputApi._RightHandSideLinesImpl(files) |
| 343 | 343 |
| 344 def ReadFile(self, file, mode='r'): | 344 def ReadFile(self, file, mode='r'): |
| 345 """Reads an arbitrary file. | 345 """Reads an arbitrary file. |
| 346 | 346 |
| 347 Deny reading anything outside the repository. | 347 Deny reading anything outside the repository. |
| 348 """ | 348 """ |
| 349 if isinstance(file, AffectedFile): | 349 if isinstance(file, AffectedFile): |
| 350 file = file.AbsoluteLocalPath() | 350 file = file.AbsoluteLocalPath() |
| 351 if not file.startswith(self.change.RepositoryRoot()): | 351 if not file.startswith(self.change.RepositoryRoot()): |
| 352 raise IOError('Access outside the repository root is denied.') | 352 raise IOError('Access outside the repository root is denied.') |
| 353 return gcl.ReadFile(file, mode) | 353 return gcl.ReadFile(file, mode) |
| 354 | 354 |
| 355 @staticmethod | 355 @staticmethod |
| 356 def _RightHandSideLinesImpl(affected_files): | 356 def _RightHandSideLinesImpl(affected_files): |
| 357 """Implements RightHandSideLines for InputApi and GclChange.""" | 357 """Implements RightHandSideLines for InputApi and GclChange.""" |
| 358 for af in affected_files: | 358 for af in affected_files: |
| 359 lines = af.NewContents() | 359 lines = af.NewContents() |
| 360 line_number = 0 | 360 line_number = 0 |
| 361 for line in lines: | 361 for line in lines: |
| 362 line_number += 1 | 362 line_number += 1 |
| 363 yield (af, line_number, line) | 363 yield (af, line_number, line) |
| 364 | 364 |
| 365 | 365 |
| 366 class AffectedFile(object): | 366 class AffectedFile(object): |
| 367 """Representation of a file in a change.""" | 367 """Representation of a file in a change.""" |
| 368 | 368 |
| 369 def __init__(self, path, action, repository_root=''): | 369 def __init__(self, path, action, repository_root=''): |
| 370 self._path = path | 370 self._path = path |
| 371 self._action = action | 371 self._action = action |
| 372 self._local_root = repository_root | 372 self._local_root = repository_root |
| 373 self._is_directory = None | 373 self._is_directory = None |
| 374 self._properties = {} | 374 self._properties = {} |
| 375 self.scm = '' | |
| 376 | 375 |
| 377 def ServerPath(self): | 376 def ServerPath(self): |
| 378 """Returns a path string that identifies the file in the SCM system. | 377 """Returns a path string that identifies the file in the SCM system. |
| 379 | 378 |
| 380 Returns the empty string if the file does not exist in SCM. | 379 Returns the empty string if the file does not exist in SCM. |
| 381 """ | 380 """ |
| 382 return "" | 381 return "" |
| 383 | 382 |
| 384 def LocalPath(self): | 383 def LocalPath(self): |
| 385 """Returns the path of this file on the local disk relative to client root. | 384 """Returns the path of this file on the local disk relative to client root. |
| (...skipping 20 matching lines...) Expand all Loading... |
| 406 return self._action | 405 return self._action |
| 407 | 406 |
| 408 def Property(self, property_name): | 407 def Property(self, property_name): |
| 409 """Returns the specified SCM property of this file, or None if no such | 408 """Returns the specified SCM property of this file, or None if no such |
| 410 property. | 409 property. |
| 411 """ | 410 """ |
| 412 return self._properties.get(property_name, None) | 411 return self._properties.get(property_name, None) |
| 413 | 412 |
| 414 def IsTextFile(self): | 413 def IsTextFile(self): |
| 415 """Returns True if the file is a text file and not a binary file. | 414 """Returns True if the file is a text file and not a binary file. |
| 416 | 415 |
| 417 Deleted files are not text file.""" | 416 Deleted files are not text file.""" |
| 418 raise NotImplementedError() # Implement when needed | 417 raise NotImplementedError() # Implement when needed |
| 419 | 418 |
| 420 def NewContents(self): | 419 def NewContents(self): |
| 421 """Returns an iterator over the lines in the new version of file. | 420 """Returns an iterator over the lines in the new version of file. |
| 422 | 421 |
| 423 The new version is the file in the user's workspace, i.e. the "right hand | 422 The new version is the file in the user's workspace, i.e. the "right hand |
| 424 side". | 423 side". |
| 425 | 424 |
| 426 Contents will be empty if the file is a directory or does not exist. | 425 Contents will be empty if the file is a directory or does not exist. |
| (...skipping 24 matching lines...) Expand all Loading... |
| 451 return self.LocalPath() | 450 return self.LocalPath() |
| 452 | 451 |
| 453 | 452 |
| 454 class SvnAffectedFile(AffectedFile): | 453 class SvnAffectedFile(AffectedFile): |
| 455 """Representation of a file in a change out of a Subversion checkout.""" | 454 """Representation of a file in a change out of a Subversion checkout.""" |
| 456 | 455 |
| 457 def __init__(self, *args, **kwargs): | 456 def __init__(self, *args, **kwargs): |
| 458 AffectedFile.__init__(self, *args, **kwargs) | 457 AffectedFile.__init__(self, *args, **kwargs) |
| 459 self._server_path = None | 458 self._server_path = None |
| 460 self._is_text_file = None | 459 self._is_text_file = None |
| 461 self.scm = 'svn' | |
| 462 | 460 |
| 463 def ServerPath(self): | 461 def ServerPath(self): |
| 464 if self._server_path is None: | 462 if self._server_path is None: |
| 465 self._server_path = gclient.CaptureSVNInfo( | 463 self._server_path = gclient.CaptureSVNInfo( |
| 466 self.AbsoluteLocalPath()).get('URL', '') | 464 self.AbsoluteLocalPath()).get('URL', '') |
| 467 return self._server_path | 465 return self._server_path |
| 468 | 466 |
| 469 def IsDirectory(self): | 467 def IsDirectory(self): |
| 470 if self._is_directory is None: | 468 if self._is_directory is None: |
| 471 path = self.AbsoluteLocalPath() | 469 path = self.AbsoluteLocalPath() |
| (...skipping 26 matching lines...) Expand all Loading... |
| 498 return self._is_text_file | 496 return self._is_text_file |
| 499 | 497 |
| 500 | 498 |
| 501 class GitAffectedFile(AffectedFile): | 499 class GitAffectedFile(AffectedFile): |
| 502 """Representation of a file in a change out of a git checkout.""" | 500 """Representation of a file in a change out of a git checkout.""" |
| 503 | 501 |
| 504 def __init__(self, *args, **kwargs): | 502 def __init__(self, *args, **kwargs): |
| 505 AffectedFile.__init__(self, *args, **kwargs) | 503 AffectedFile.__init__(self, *args, **kwargs) |
| 506 self._server_path = None | 504 self._server_path = None |
| 507 self._is_text_file = None | 505 self._is_text_file = None |
| 508 self.scm = 'git' | |
| 509 | 506 |
| 510 def ServerPath(self): | 507 def ServerPath(self): |
| 511 if self._server_path is None: | 508 if self._server_path is None: |
| 512 raise NotImplementedException() # TODO(maruel) Implement. | 509 raise NotImplementedException() # TODO(maruel) Implement. |
| 513 return self._server_path | 510 return self._server_path |
| 514 | 511 |
| 515 def IsDirectory(self): | 512 def IsDirectory(self): |
| 516 if self._is_directory is None: | 513 if self._is_directory is None: |
| 517 path = self.AbsoluteLocalPath() | 514 path = self.AbsoluteLocalPath() |
| 518 if os.path.exists(path): | 515 if os.path.exists(path): |
| (...skipping 21 matching lines...) Expand all Loading... |
| 540 # raise NotImplementedException() # TODO(maruel) Implement. | 537 # raise NotImplementedException() # TODO(maruel) Implement. |
| 541 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) | 538 self._is_text_file = os.path.isfile(self.AbsoluteLocalPath()) |
| 542 return self._is_text_file | 539 return self._is_text_file |
| 543 | 540 |
| 544 | 541 |
| 545 class Change(object): | 542 class Change(object): |
| 546 """Describe a change. | 543 """Describe a change. |
| 547 | 544 |
| 548 Used directly by the presubmit scripts to query the current change being | 545 Used directly by the presubmit scripts to query the current change being |
| 549 tested. | 546 tested. |
| 550 | 547 |
| 551 Instance members: | 548 Instance members: |
| 552 tags: Dictionnary of KEY=VALUE pairs found in the change description. | 549 tags: Dictionnary of KEY=VALUE pairs found in the change description. |
| 553 self.KEY: equivalent to tags['KEY'] | 550 self.KEY: equivalent to tags['KEY'] |
| 554 """ | 551 """ |
| 555 | 552 |
| 556 _AFFECTED_FILES = AffectedFile | 553 _AFFECTED_FILES = AffectedFile |
| 557 | 554 |
| 558 # Matches key/value (or "tag") lines in changelist descriptions. | 555 # Matches key/value (or "tag") lines in changelist descriptions. |
| 559 _TAG_LINE_RE = re.compile( | 556 _TAG_LINE_RE = re.compile( |
| 560 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$') | 557 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$') |
| 561 | 558 |
| 562 def __init__(self, name, description, local_root, files, issue, patchset): | 559 def __init__(self, name, description, local_root, files, issue, patchset): |
| 563 if files is None: | 560 if files is None: |
| 564 files = [] | 561 files = [] |
| 565 self._name = name | 562 self._name = name |
| 566 self._full_description = description | 563 self._full_description = description |
| 567 self._local_root = local_root | 564 self._local_root = local_root |
| 568 self.issue = issue | 565 self.issue = issue |
| 569 self.patchset = patchset | 566 self.patchset = patchset |
| 567 self.scm = '' |
| 570 | 568 |
| 571 # From the description text, build up a dictionary of key/value pairs | 569 # From the description text, build up a dictionary of key/value pairs |
| 572 # plus the description minus all key/value or "tag" lines. | 570 # plus the description minus all key/value or "tag" lines. |
| 573 self._description_without_tags = [] | 571 self._description_without_tags = [] |
| 574 self.tags = {} | 572 self.tags = {} |
| 575 for line in self._full_description.splitlines(): | 573 for line in self._full_description.splitlines(): |
| 576 m = self._TAG_LINE_RE.match(line) | 574 m = self._TAG_LINE_RE.match(line) |
| 577 if m: | 575 if m: |
| 578 self.tags[m.group('key')] = m.group('value') | 576 self.tags[m.group('key')] = m.group('value') |
| 579 else: | 577 else: |
| (...skipping 94 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 674 the contents of the line as a string. | 672 the contents of the line as a string. |
| 675 """ | 673 """ |
| 676 return InputApi._RightHandSideLinesImpl( | 674 return InputApi._RightHandSideLinesImpl( |
| 677 filter(lambda x: x.IsTextFile(), | 675 filter(lambda x: x.IsTextFile(), |
| 678 self.AffectedFiles(include_deletes=False))) | 676 self.AffectedFiles(include_deletes=False))) |
| 679 | 677 |
| 680 | 678 |
| 681 class SvnChange(Change): | 679 class SvnChange(Change): |
| 682 _AFFECTED_FILES = SvnAffectedFile | 680 _AFFECTED_FILES = SvnAffectedFile |
| 683 | 681 |
| 682 def __init__(self, *args, **kwargs): |
| 683 Change.__init__(self, *args, **kwargs) |
| 684 self.scm = 'svn' |
| 685 |
| 686 def GetAllModifiedFiles(self): |
| 687 """Get all modified files.""" |
| 688 changelists = gcl.GetModifiedFiles() |
| 689 all_modified_files = [] |
| 690 for cl in changelists.values(): |
| 691 all_modified_files.extend([f[1] for f in cl]) |
| 692 return all_modified_files |
| 693 |
| 694 def GetModifiedFiles(self): |
| 695 """Get modified files in the current CL.""" |
| 696 changelists = gcl.GetModifiedFiles() |
| 697 return [f[1] for f in changelists[self.Name()]] |
| 698 |
| 684 | 699 |
| 685 class GitChange(Change): | 700 class GitChange(Change): |
| 686 _AFFECTED_FILES = GitAffectedFile | 701 _AFFECTED_FILES = GitAffectedFile |
| 687 | 702 |
| 703 def __init__(self, *args, **kwargs): |
| 704 Change.__init__(self, *args, **kwargs) |
| 705 self.scm = 'git' |
| 706 |
| 688 | 707 |
| 689 def ListRelevantPresubmitFiles(files, root): | 708 def ListRelevantPresubmitFiles(files, root): |
| 690 """Finds all presubmit files that apply to a given set of source files. | 709 """Finds all presubmit files that apply to a given set of source files. |
| 691 | 710 |
| 692 Args: | 711 Args: |
| 693 files: An iterable container containing file paths. | 712 files: An iterable container containing file paths. |
| 694 root: Path where to stop searching. | 713 root: Path where to stop searching. |
| 695 | 714 |
| 696 Return: | 715 Return: |
| 697 List of absolute paths of the existing PRESUBMIT.py scripts. | 716 List of absolute paths of the existing PRESUBMIT.py scripts. |
| (...skipping 226 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 924 options.commit, | 943 options.commit, |
| 925 options.verbose, | 944 options.verbose, |
| 926 sys.stdout, | 945 sys.stdout, |
| 927 sys.stdin, | 946 sys.stdin, |
| 928 options.default_presubmit, | 947 options.default_presubmit, |
| 929 options.may_prompt) | 948 options.may_prompt) |
| 930 | 949 |
| 931 | 950 |
| 932 if __name__ == '__main__': | 951 if __name__ == '__main__': |
| 933 sys.exit(Main(sys.argv)) | 952 sys.exit(Main(sys.argv)) |
| OLD | NEW |