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 |