| 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 155 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 166 r".*\b[A-Z0-9_]+$", | 166 r".*\b[A-Z0-9_]+$", |
| 167 # SCM (can happen in dual SCM configuration). (Slightly over aggressive) | 167 # SCM (can happen in dual SCM configuration). (Slightly over aggressive) |
| 168 r".*\.git[\\\/].*", | 168 r".*\.git[\\\/].*", |
| 169 r".*\.svn[\\\/].*", | 169 r".*\.svn[\\\/].*", |
| 170 ) | 170 ) |
| 171 | 171 |
| 172 def __init__(self, change, presubmit_path, is_committing): | 172 def __init__(self, change, presubmit_path, is_committing): |
| 173 """Builds an InputApi object. | 173 """Builds an InputApi object. |
| 174 | 174 |
| 175 Args: | 175 Args: |
| 176 change: A presubmit.GclChange object. | 176 change: A presubmit.Change object. |
| 177 presubmit_path: The path to the presubmit script being processed. | 177 presubmit_path: The path to the presubmit script being processed. |
| 178 is_committing: True if the change is about to be committed. | 178 is_committing: True if the change is about to be committed. |
| 179 """ | 179 """ |
| 180 # Version number of the presubmit_support script. | 180 # Version number of the presubmit_support script. |
| 181 self.version = [int(x) for x in __version__.split('.')] | 181 self.version = [int(x) for x in __version__.split('.')] |
| 182 self.change = change | 182 self.change = change |
| 183 self.is_committing = is_committing | 183 self.is_committing = is_committing |
| 184 | 184 |
| 185 # We expose various modules and functions as attributes of the input_api | 185 # We expose various modules and functions as attributes of the input_api |
| 186 # so that presubmit scripts don't have to import them. | 186 # so that presubmit scripts don't have to import them. |
| (...skipping 166 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 353 line_number += 1 | 353 line_number += 1 |
| 354 yield (af, line_number, line) | 354 yield (af, line_number, line) |
| 355 | 355 |
| 356 | 356 |
| 357 class AffectedFile(object): | 357 class AffectedFile(object): |
| 358 """Representation of a file in a change.""" | 358 """Representation of a file in a change.""" |
| 359 | 359 |
| 360 def __init__(self, path, action, repository_root=''): | 360 def __init__(self, path, action, repository_root=''): |
| 361 self._path = path | 361 self._path = path |
| 362 self._action = action | 362 self._action = action |
| 363 self._repository_root = repository_root | 363 self._local_root = repository_root |
| 364 self._is_directory = None | 364 self._is_directory = None |
| 365 self._properties = {} | 365 self._properties = {} |
| 366 self.scm = '' | 366 self.scm = '' |
| 367 | 367 |
| 368 def ServerPath(self): | 368 def ServerPath(self): |
| 369 """Returns a path string that identifies the file in the SCM system. | 369 """Returns a path string that identifies the file in the SCM system. |
| 370 | 370 |
| 371 Returns the empty string if the file does not exist in SCM. | 371 Returns the empty string if the file does not exist in SCM. |
| 372 """ | 372 """ |
| 373 return "" | 373 return "" |
| 374 | 374 |
| 375 def LocalPath(self): | 375 def LocalPath(self): |
| 376 """Returns the path of this file on the local disk relative to client root. | 376 """Returns the path of this file on the local disk relative to client root. |
| 377 """ | 377 """ |
| 378 return normpath(self._path) | 378 return normpath(self._path) |
| 379 | 379 |
| 380 def AbsoluteLocalPath(self): | 380 def AbsoluteLocalPath(self): |
| 381 """Returns the absolute path of this file on the local disk. | 381 """Returns the absolute path of this file on the local disk. |
| 382 """ | 382 """ |
| 383 return normpath(os.path.join(self._repository_root, self.LocalPath())) | 383 return normpath(os.path.join(self._local_root, self.LocalPath())) |
| 384 | 384 |
| 385 def IsDirectory(self): | 385 def IsDirectory(self): |
| 386 """Returns true if this object is a directory.""" | 386 """Returns true if this object is a directory.""" |
| 387 if self._is_directory is None: | 387 if self._is_directory is None: |
| 388 path = self.AbsoluteLocalPath() | 388 path = self.AbsoluteLocalPath() |
| 389 self._is_directory = (os.path.exists(path) and | 389 self._is_directory = (os.path.exists(path) and |
| 390 os.path.isdir(path)) | 390 os.path.isdir(path)) |
| 391 return self._is_directory | 391 return self._is_directory |
| 392 | 392 |
| 393 def Action(self): | 393 def Action(self): |
| (...skipping 88 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 482 self._is_text_file = False | 482 self._is_text_file = False |
| 483 elif self.IsDirectory(): | 483 elif self.IsDirectory(): |
| 484 self._is_text_file = False | 484 self._is_text_file = False |
| 485 else: | 485 else: |
| 486 mime_type = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(), | 486 mime_type = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(), |
| 487 'svn:mime-type') | 487 'svn:mime-type') |
| 488 self._is_text_file = (not mime_type or mime_type.startswith('text/')) | 488 self._is_text_file = (not mime_type or mime_type.startswith('text/')) |
| 489 return self._is_text_file | 489 return self._is_text_file |
| 490 | 490 |
| 491 | 491 |
| 492 class GclChange(object): | 492 class Change(object): |
| 493 """Describe a change. | 493 """Describe a change. |
| 494 | 494 |
| 495 Used directly by the presubmit scripts to query the current change being | 495 Used directly by the presubmit scripts to query the current change being |
| 496 tested. | 496 tested. |
| 497 | 497 |
| 498 Instance members: | 498 Instance members: |
| 499 tags: Dictionnary of KEY=VALUE pairs found in the change description. | 499 tags: Dictionnary of KEY=VALUE pairs found in the change description. |
| 500 self.KEY: equivalent to tags['KEY'] | 500 self.KEY: equivalent to tags['KEY'] |
| 501 """ | 501 """ |
| 502 | 502 |
| 503 _AFFECTED_FILES = AffectedFile |
| 504 |
| 503 # Matches key/value (or "tag") lines in changelist descriptions. | 505 # Matches key/value (or "tag") lines in changelist descriptions. |
| 504 _tag_line_re = re.compile( | 506 _TAG_LINE_RE = re.compile( |
| 505 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$') | 507 '^\s*(?P<key>[A-Z][A-Z_0-9]*)\s*=\s*(?P<value>.*?)\s*$') |
| 506 | 508 |
| 507 def __init__(self, change_info): | 509 def __init__(self, name, description, local_root, files, issue, patchset): |
| 508 # Do not keep a reference to the original change_info. | 510 if files is None: |
| 509 self._name = change_info.name | 511 files = [] |
| 510 self._full_description = change_info.description | 512 self._name = name |
| 511 self._repository_root = change_info.GetLocalRoot() | 513 self._full_description = description |
| 512 self.issue = change_info.issue | 514 self._local_root = local_root |
| 513 self.patchset = change_info.patchset | 515 self.issue = issue |
| 516 self.patchset = patchset |
| 514 | 517 |
| 515 # From the description text, build up a dictionary of key/value pairs | 518 # From the description text, build up a dictionary of key/value pairs |
| 516 # plus the description minus all key/value or "tag" lines. | 519 # plus the description minus all key/value or "tag" lines. |
| 517 self._description_without_tags = [] | 520 self._description_without_tags = [] |
| 518 self.tags = {} | 521 self.tags = {} |
| 519 for line in self._full_description.splitlines(): | 522 for line in self._full_description.splitlines(): |
| 520 m = self._tag_line_re.match(line) | 523 m = self._TAG_LINE_RE.match(line) |
| 521 if m: | 524 if m: |
| 522 self.tags[m.group('key')] = m.group('value') | 525 self.tags[m.group('key')] = m.group('value') |
| 523 else: | 526 else: |
| 524 self._description_without_tags.append(line) | 527 self._description_without_tags.append(line) |
| 525 | 528 |
| 526 # Change back to text and remove whitespace at end. | 529 # Change back to text and remove whitespace at end. |
| 527 self._description_without_tags = '\n'.join(self._description_without_tags) | 530 self._description_without_tags = '\n'.join(self._description_without_tags) |
| 528 self._description_without_tags = self._description_without_tags.rstrip() | 531 self._description_without_tags = self._description_without_tags.rstrip() |
| 529 | 532 |
| 530 self._affected_files = [ | 533 self._affected_files = [ |
| 531 SvnAffectedFile(info[1], info[0].strip(), self._repository_root) | 534 self._AFFECTED_FILES(info[1], info[0].strip(), self._local_root) |
| 532 for info in change_info.GetFiles() | 535 for info in files |
| 533 ] | 536 ] |
| 534 | 537 |
| 535 def Name(self): | 538 def Name(self): |
| 536 """Returns the change name.""" | 539 """Returns the change name.""" |
| 537 return self._name | 540 return self._name |
| 538 | 541 |
| 539 def DescriptionText(self): | 542 def DescriptionText(self): |
| 540 """Returns the user-entered changelist description, minus tags. | 543 """Returns the user-entered changelist description, minus tags. |
| 541 | 544 |
| 542 Any line in the user-provided description starting with e.g. "FOO=" | 545 Any line in the user-provided description starting with e.g. "FOO=" |
| 543 (whitespace permitted before and around) is considered a tag line. Such | 546 (whitespace permitted before and around) is considered a tag line. Such |
| 544 lines are stripped out of the description this function returns. | 547 lines are stripped out of the description this function returns. |
| 545 """ | 548 """ |
| 546 return self._description_without_tags | 549 return self._description_without_tags |
| 547 | 550 |
| 548 def FullDescriptionText(self): | 551 def FullDescriptionText(self): |
| 549 """Returns the complete changelist description including tags.""" | 552 """Returns the complete changelist description including tags.""" |
| 550 return self._full_description | 553 return self._full_description |
| 551 | 554 |
| 552 def RepositoryRoot(self): | 555 def RepositoryRoot(self): |
| 553 """Returns the repository (checkout) root directory for this change, | 556 """Returns the repository (checkout) root directory for this change, |
| 554 as an absolute path. | 557 as an absolute path. |
| 555 """ | 558 """ |
| 556 return self._repository_root | 559 return self._local_root |
| 557 | 560 |
| 558 def __getattr__(self, attr): | 561 def __getattr__(self, attr): |
| 559 """Return tags directly as attributes on the object.""" | 562 """Return tags directly as attributes on the object.""" |
| 560 if not re.match(r"^[A-Z_]*$", attr): | 563 if not re.match(r"^[A-Z_]*$", attr): |
| 561 raise AttributeError(self, attr) | 564 raise AttributeError(self, attr) |
| 562 return self.tags.get(attr) | 565 return self.tags.get(attr) |
| 563 | 566 |
| 564 def AffectedFiles(self, include_dirs=False, include_deletes=True): | 567 def AffectedFiles(self, include_dirs=False, include_deletes=True): |
| 565 """Returns a list of AffectedFile instances for all files in the change. | 568 """Returns a list of AffectedFile instances for all files in the change. |
| 566 | 569 |
| (...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 615 a 3 tuple: | 618 a 3 tuple: |
| 616 the AffectedFile instance of the current file; | 619 the AffectedFile instance of the current file; |
| 617 integer line number (1-based); and | 620 integer line number (1-based); and |
| 618 the contents of the line as a string. | 621 the contents of the line as a string. |
| 619 """ | 622 """ |
| 620 return InputApi._RightHandSideLinesImpl( | 623 return InputApi._RightHandSideLinesImpl( |
| 621 filter(lambda x: x.IsTextFile(), | 624 filter(lambda x: x.IsTextFile(), |
| 622 self.AffectedFiles(include_deletes=False))) | 625 self.AffectedFiles(include_deletes=False))) |
| 623 | 626 |
| 624 | 627 |
| 628 class SvnChange(Change): |
| 629 _AFFECTED_FILES = SvnAffectedFile |
| 630 |
| 631 |
| 625 def ListRelevantPresubmitFiles(files, root): | 632 def ListRelevantPresubmitFiles(files, root): |
| 626 """Finds all presubmit files that apply to a given set of source files. | 633 """Finds all presubmit files that apply to a given set of source files. |
| 627 | 634 |
| 628 Args: | 635 Args: |
| 629 files: An iterable container containing file paths. | 636 files: An iterable container containing file paths. |
| 630 root: Path where to stop searching. | 637 root: Path where to stop searching. |
| 631 | 638 |
| 632 Return: | 639 Return: |
| 633 List of absolute paths of the existing PRESUBMIT.py scripts. | 640 List of absolute paths of the existing PRESUBMIT.py scripts. |
| 634 """ | 641 """ |
| 635 entries = [] | 642 entries = [] |
| 636 for f in files: | 643 for f in files: |
| 637 f = normpath(os.path.join(root, f)) | 644 f = normpath(os.path.join(root, f)) |
| 638 while f: | 645 while f: |
| 639 f = os.path.dirname(f) | 646 f = os.path.dirname(f) |
| 640 if f in entries: | 647 if f in entries: |
| 641 break | 648 break |
| 642 entries.append(f) | 649 entries.append(f) |
| 643 if f == root: | 650 if f == root: |
| 644 break | 651 break |
| 645 entries.sort() | 652 entries.sort() |
| 646 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries) | 653 entries = map(lambda x: os.path.join(x, 'PRESUBMIT.py'), entries) |
| 647 return filter(lambda x: os.path.isfile(x), entries) | 654 return filter(lambda x: os.path.isfile(x), entries) |
| 648 | 655 |
| 649 | 656 |
| 650 class PresubmitExecuter(object): | 657 class PresubmitExecuter(object): |
| 651 def __init__(self, change_info, committing): | 658 def __init__(self, change, committing): |
| 652 """ | 659 """ |
| 653 Args: | 660 Args: |
| 654 change_info: The gcl.ChangeInfo object for the change. | 661 change: The Change object. |
| 655 committing: True if 'gcl commit' is running, False if 'gcl upload' is. | 662 committing: True if 'gcl commit' is running, False if 'gcl upload' is. |
| 656 """ | 663 """ |
| 657 # TODO(maruel): Determine the SCM. | 664 self.change = change |
| 658 self.change = GclChange(change_info) | |
| 659 self.committing = committing | 665 self.committing = committing |
| 660 | 666 |
| 661 def ExecPresubmitScript(self, script_text, presubmit_path): | 667 def ExecPresubmitScript(self, script_text, presubmit_path): |
| 662 """Executes a single presubmit script. | 668 """Executes a single presubmit script. |
| 663 | 669 |
| 664 Args: | 670 Args: |
| 665 script_text: The text of the presubmit script. | 671 script_text: The text of the presubmit script. |
| 666 presubmit_path: The path to the presubmit file (this will be reported via | 672 presubmit_path: The path to the presubmit file (this will be reported via |
| 667 input_api.PresubmitLocalPath()). | 673 input_api.PresubmitLocalPath()). |
| 668 | 674 |
| (...skipping 21 matching lines...) Expand all Loading... |
| 690 if not isinstance(item, OutputApi.PresubmitResult): | 696 if not isinstance(item, OutputApi.PresubmitResult): |
| 691 raise exceptions.RuntimeError( | 697 raise exceptions.RuntimeError( |
| 692 'All presubmit results must be of types derived from ' | 698 'All presubmit results must be of types derived from ' |
| 693 'output_api.PresubmitResult') | 699 'output_api.PresubmitResult') |
| 694 else: | 700 else: |
| 695 result = () # no error since the script doesn't care about current event. | 701 result = () # no error since the script doesn't care about current event. |
| 696 | 702 |
| 697 return result | 703 return result |
| 698 | 704 |
| 699 | 705 |
| 700 def DoPresubmitChecks(change_info, | 706 def DoPresubmitChecks(change, |
| 701 committing, | 707 committing, |
| 702 verbose, | 708 verbose, |
| 703 output_stream, | 709 output_stream, |
| 704 input_stream, | 710 input_stream, |
| 705 default_presubmit, | 711 default_presubmit, |
| 706 may_prompt): | 712 may_prompt): |
| 707 """Runs all presubmit checks that apply to the files in the change. | 713 """Runs all presubmit checks that apply to the files in the change. |
| 708 | 714 |
| 709 This finds all PRESUBMIT.py files in directories enclosing the files in the | 715 This finds all PRESUBMIT.py files in directories enclosing the files in the |
| 710 change (up to the repository root) and calls the relevant entrypoint function | 716 change (up to the repository root) and calls the relevant entrypoint function |
| 711 depending on whether the change is being committed or uploaded. | 717 depending on whether the change is being committed or uploaded. |
| 712 | 718 |
| 713 Prints errors, warnings and notifications. Prompts the user for warnings | 719 Prints errors, warnings and notifications. Prompts the user for warnings |
| 714 when needed. | 720 when needed. |
| 715 | 721 |
| 716 Args: | 722 Args: |
| 717 change_info: The gcl.ChangeInfo object for the change. | 723 change: The Change object. |
| 718 committing: True if 'gcl commit' is running, False if 'gcl upload' is. | 724 committing: True if 'gcl commit' is running, False if 'gcl upload' is. |
| 719 verbose: Prints debug info. | 725 verbose: Prints debug info. |
| 720 output_stream: A stream to write output from presubmit tests to. | 726 output_stream: A stream to write output from presubmit tests to. |
| 721 input_stream: A stream to read input from the user. | 727 input_stream: A stream to read input from the user. |
| 722 default_presubmit: A default presubmit script to execute in any case. | 728 default_presubmit: A default presubmit script to execute in any case. |
| 723 may_prompt: Enable (y/n) questions on warning or error. | 729 may_prompt: Enable (y/n) questions on warning or error. |
| 724 | 730 |
| 725 Return: | 731 Return: |
| 726 True if execution can continue, False if not. | 732 True if execution can continue, False if not. |
| 727 """ | 733 """ |
| 728 presubmit_files = ListRelevantPresubmitFiles(change_info.GetFileNames(), | 734 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True), |
| 729 change_info.GetLocalRoot()) | 735 change.RepositoryRoot()) |
| 730 if not presubmit_files and verbose: | 736 if not presubmit_files and verbose: |
| 731 output_stream.write("Warning, no presubmit.py found.\n") | 737 output_stream.write("Warning, no presubmit.py found.\n") |
| 732 results = [] | 738 results = [] |
| 733 executer = PresubmitExecuter(change_info, committing) | 739 executer = PresubmitExecuter(change, committing) |
| 734 if default_presubmit: | 740 if default_presubmit: |
| 735 if verbose: | 741 if verbose: |
| 736 output_stream.write("Running default presubmit script.\n") | 742 output_stream.write("Running default presubmit script.\n") |
| 737 fake_path = os.path.join(change_info.GetLocalRoot(), 'PRESUBMIT.py') | 743 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') |
| 738 results += executer.ExecPresubmitScript(default_presubmit, fake_path) | 744 results += executer.ExecPresubmitScript(default_presubmit, fake_path) |
| 739 for filename in presubmit_files: | 745 for filename in presubmit_files: |
| 740 filename = os.path.abspath(filename) | 746 filename = os.path.abspath(filename) |
| 741 if verbose: | 747 if verbose: |
| 742 output_stream.write("Running %s\n" % filename) | 748 output_stream.write("Running %s\n" % filename) |
| 743 # Accept CRLF presubmit script. | 749 # Accept CRLF presubmit script. |
| 744 presubmit_script = gcl.ReadFile(filename, 'rU') | 750 presubmit_script = gcl.ReadFile(filename, 'rU') |
| 745 results += executer.ExecPresubmitScript(presubmit_script, filename) | 751 results += executer.ExecPresubmitScript(presubmit_script, filename) |
| 746 | 752 |
| 747 errors = [] | 753 errors = [] |
| (...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 798 | 804 |
| 799 def Main(argv): | 805 def Main(argv): |
| 800 parser = optparse.OptionParser(usage="%prog [options]", | 806 parser = optparse.OptionParser(usage="%prog [options]", |
| 801 version="%prog " + str(__version__)) | 807 version="%prog " + str(__version__)) |
| 802 parser.add_option("-c", "--commit", action="store_true", | 808 parser.add_option("-c", "--commit", action="store_true", |
| 803 help="Use commit instead of upload checks") | 809 help="Use commit instead of upload checks") |
| 804 parser.add_option("-r", "--recursive", action="store_true", | 810 parser.add_option("-r", "--recursive", action="store_true", |
| 805 help="Act recursively") | 811 help="Act recursively") |
| 806 parser.add_option("-v", "--verbose", action="store_true", | 812 parser.add_option("-v", "--verbose", action="store_true", |
| 807 help="Verbose output") | 813 help="Verbose output") |
| 814 parser.add_option("--files", default='') |
| 815 parser.add_option("--name", default='no name') |
| 816 parser.add_option("--description", default='') |
| 817 parser.add_option("--issue", type='int', default=0) |
| 818 parser.add_option("--patchset", type='int', default=0) |
| 819 parser.add_options("--root", default='') |
| 820 parser.add_options("--default_presubmit", default='') |
| 821 parser.add_options("--may_prompt", action='store_true') |
| 808 options, args = parser.parse_args(argv[1:]) | 822 options, args = parser.parse_args(argv[1:]) |
| 809 files = ParseFiles(args, options.recursive) | 823 if not options.files: |
| 824 options.files = ParseFiles(args, options.recursive) |
| 825 if not options.root: |
| 826 options.root = gcl.GetRepositoryRoot() |
| 810 if options.verbose: | 827 if options.verbose: |
| 811 print "Found %d files." % len(files) | 828 print "Found %d files." % len(files) |
| 812 return not DoPresubmitChecks(gcl.ChangeInfo('No name', 0, 0, '', files, | 829 return not DoPresubmitChecks(SvnChange(options.name, |
| 813 gcl.GetRepositoryRoot()), | 830 options.description, |
| 831 options.root, |
| 832 options.files, |
| 833 options.issue, |
| 834 options.patchset), |
| 814 options.commit, | 835 options.commit, |
| 815 options.verbose, | 836 options.verbose, |
| 816 sys.stdout, | 837 sys.stdout, |
| 817 sys.stdin, | 838 sys.stdin, |
| 818 None, | 839 options.default_presubmit, |
| 819 False) | 840 options.may_prompt) |
| 820 | 841 |
| 821 | 842 |
| 822 if __name__ == '__main__': | 843 if __name__ == '__main__': |
| 823 sys.exit(Main(sys.argv)) | 844 sys.exit(Main(sys.argv)) |
| OLD | NEW |