Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(315)

Side by Side Diff: presubmit_support.py

Issue 126068: Remove gcl.ChangeInfo dependency in presubmit_support.py. (Closed)
Patch Set: . Created 11 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | tests/presubmit_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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
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
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))
OLDNEW
« no previous file with comments | « no previous file | tests/presubmit_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698