| 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.4' | 9 __version__ = '1.3.3' |
| 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 cPickle # Exposed through the API. | 15 import cPickle # Exposed through the API. |
| 16 import cStringIO # Exposed through the API. | 16 import cStringIO # Exposed through the API. |
| 17 import exceptions | 17 import exceptions |
| 18 import fnmatch | 18 import fnmatch |
| 19 import glob | 19 import glob |
| 20 import logging | 20 import logging |
| 21 import marshal # Exposed through the API. | 21 import marshal # Exposed through the API. |
| 22 import optparse | 22 import optparse |
| 23 import os # Somewhat exposed through the API. | 23 import os # Somewhat exposed through the API. |
| 24 import pickle # Exposed through the API. | 24 import pickle # Exposed through the API. |
| 25 import random | 25 import random |
| 26 import re # Exposed through the API. | 26 import re # Exposed through the API. |
| 27 import subprocess # Exposed through the API. | 27 import subprocess # Exposed through the API. |
| 28 import sys # Parts exposed through API. | 28 import sys # Parts exposed through API. |
| 29 import tempfile # Exposed through the API. | 29 import tempfile # Exposed through the API. |
| 30 import time | 30 import time |
| 31 import traceback # Exposed through the API. | 31 import traceback # Exposed through the API. |
| 32 import types | 32 import types |
| 33 import unittest # Exposed through the API. | 33 import unittest # Exposed through the API. |
| 34 import urllib2 # Exposed through the API. | 34 import urllib2 # Exposed through the API. |
| 35 import warnings | 35 import warnings |
| 36 | 36 |
| 37 # Local imports. | 37 # Local imports. |
| 38 # TODO(joi) Would be cleaner to factor out utils in gcl to separate module, but |
| 39 # for now it would only be a couple of functions so hardly worth it. |
| 38 import gcl | 40 import gcl |
| 39 import gclient_utils | 41 import gclient_scm |
| 40 import presubmit_canned_checks | 42 import presubmit_canned_checks |
| 41 import scm | |
| 42 | 43 |
| 43 | 44 |
| 44 # Ask for feedback only once in program lifetime. | 45 # Ask for feedback only once in program lifetime. |
| 45 _ASKED_FOR_FEEDBACK = False | 46 _ASKED_FOR_FEEDBACK = False |
| 46 | 47 |
| 47 | 48 |
| 48 class NotImplementedException(Exception): | 49 class NotImplementedException(Exception): |
| 49 """We're leaving placeholders in a bunch of places to remind us of the | 50 """We're leaving placeholders in a bunch of places to remind us of the |
| 50 design of the API, but we have not implemented all of it yet. Implement as | 51 design of the API, but we have not implemented all of it yet. Implement as |
| 51 the need arises. | 52 the need arises. |
| (...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 233 | 234 |
| 234 Args: | 235 Args: |
| 235 Depot path as a string. | 236 Depot path as a string. |
| 236 | 237 |
| 237 Returns: | 238 Returns: |
| 238 The local path of the depot path under the user's current client, or None | 239 The local path of the depot path under the user's current client, or None |
| 239 if the file is not mapped. | 240 if the file is not mapped. |
| 240 | 241 |
| 241 Remember to check for the None case and show an appropriate error! | 242 Remember to check for the None case and show an appropriate error! |
| 242 """ | 243 """ |
| 243 local_path = scm.SVN.CaptureInfo(depot_path).get('Path') | 244 local_path = gclient_scm.CaptureSVNInfo(depot_path).get('Path') |
| 244 if local_path: | 245 if local_path: |
| 245 return local_path | 246 return local_path |
| 246 | 247 |
| 247 def LocalToDepotPath(self, local_path): | 248 def LocalToDepotPath(self, local_path): |
| 248 """Translate a local path to a depot path. | 249 """Translate a local path to a depot path. |
| 249 | 250 |
| 250 Args: | 251 Args: |
| 251 Local path (relative to current directory, or absolute) as a string. | 252 Local path (relative to current directory, or absolute) as a string. |
| 252 | 253 |
| 253 Returns: | 254 Returns: |
| 254 The depot path (SVN URL) of the file if mapped, otherwise None. | 255 The depot path (SVN URL) of the file if mapped, otherwise None. |
| 255 """ | 256 """ |
| 256 depot_path = scm.SVN.CaptureInfo(local_path).get('URL') | 257 depot_path = gclient_scm.CaptureSVNInfo(local_path).get('URL') |
| 257 if depot_path: | 258 if depot_path: |
| 258 return depot_path | 259 return depot_path |
| 259 | 260 |
| 260 def AffectedFiles(self, include_dirs=False, include_deletes=True): | 261 def AffectedFiles(self, include_dirs=False, include_deletes=True): |
| 261 """Same as input_api.change.AffectedFiles() except only lists files | 262 """Same as input_api.change.AffectedFiles() except only lists files |
| 262 (and optionally directories) in the same directory as the current presubmit | 263 (and optionally directories) in the same directory as the current presubmit |
| 263 script, or subdirectories thereof. | 264 script, or subdirectories thereof. |
| 264 """ | 265 """ |
| 265 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath()) | 266 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath()) |
| 266 if len(dir_with_slash) == 1: | 267 if len(dir_with_slash) == 1: |
| (...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 346 | 347 |
| 347 def ReadFile(self, file_item, mode='r'): | 348 def ReadFile(self, file_item, mode='r'): |
| 348 """Reads an arbitrary file. | 349 """Reads an arbitrary file. |
| 349 | 350 |
| 350 Deny reading anything outside the repository. | 351 Deny reading anything outside the repository. |
| 351 """ | 352 """ |
| 352 if isinstance(file_item, AffectedFile): | 353 if isinstance(file_item, AffectedFile): |
| 353 file_item = file_item.AbsoluteLocalPath() | 354 file_item = file_item.AbsoluteLocalPath() |
| 354 if not file_item.startswith(self.change.RepositoryRoot()): | 355 if not file_item.startswith(self.change.RepositoryRoot()): |
| 355 raise IOError('Access outside the repository root is denied.') | 356 raise IOError('Access outside the repository root is denied.') |
| 356 return gclient_utils.FileRead(file_item, mode) | 357 return gcl.ReadFile(file_item, mode) |
| 357 | 358 |
| 358 @staticmethod | 359 @staticmethod |
| 359 def _RightHandSideLinesImpl(affected_files): | 360 def _RightHandSideLinesImpl(affected_files): |
| 360 """Implements RightHandSideLines for InputApi and GclChange.""" | 361 """Implements RightHandSideLines for InputApi and GclChange.""" |
| 361 for af in affected_files: | 362 for af in affected_files: |
| 362 lines = af.NewContents() | 363 lines = af.NewContents() |
| 363 line_number = 0 | 364 line_number = 0 |
| 364 for line in lines: | 365 for line in lines: |
| 365 line_number += 1 | 366 line_number += 1 |
| 366 yield (af, line_number, line) | 367 yield (af, line_number, line) |
| (...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 424 | 425 |
| 425 The new version is the file in the user's workspace, i.e. the "right hand | 426 The new version is the file in the user's workspace, i.e. the "right hand |
| 426 side". | 427 side". |
| 427 | 428 |
| 428 Contents will be empty if the file is a directory or does not exist. | 429 Contents will be empty if the file is a directory or does not exist. |
| 429 Note: The cariage returns (LF or CR) are stripped off. | 430 Note: The cariage returns (LF or CR) are stripped off. |
| 430 """ | 431 """ |
| 431 if self.IsDirectory(): | 432 if self.IsDirectory(): |
| 432 return [] | 433 return [] |
| 433 else: | 434 else: |
| 434 return gclient_utils.FileRead(self.AbsoluteLocalPath(), | 435 return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines() |
| 435 'rU').splitlines() | |
| 436 | 436 |
| 437 def OldContents(self): | 437 def OldContents(self): |
| 438 """Returns an iterator over the lines in the old version of file. | 438 """Returns an iterator over the lines in the old version of file. |
| 439 | 439 |
| 440 The old version is the file in depot, i.e. the "left hand side". | 440 The old version is the file in depot, i.e. the "left hand side". |
| 441 """ | 441 """ |
| 442 raise NotImplementedError() # Implement when needed | 442 raise NotImplementedError() # Implement when needed |
| 443 | 443 |
| 444 def OldFileTempPath(self): | 444 def OldFileTempPath(self): |
| 445 """Returns the path on local disk where the old contents resides. | 445 """Returns the path on local disk where the old contents resides. |
| (...skipping 11 matching lines...) Expand all Loading... |
| 457 class SvnAffectedFile(AffectedFile): | 457 class SvnAffectedFile(AffectedFile): |
| 458 """Representation of a file in a change out of a Subversion checkout.""" | 458 """Representation of a file in a change out of a Subversion checkout.""" |
| 459 | 459 |
| 460 def __init__(self, *args, **kwargs): | 460 def __init__(self, *args, **kwargs): |
| 461 AffectedFile.__init__(self, *args, **kwargs) | 461 AffectedFile.__init__(self, *args, **kwargs) |
| 462 self._server_path = None | 462 self._server_path = None |
| 463 self._is_text_file = None | 463 self._is_text_file = None |
| 464 | 464 |
| 465 def ServerPath(self): | 465 def ServerPath(self): |
| 466 if self._server_path is None: | 466 if self._server_path is None: |
| 467 self._server_path = scm.SVN.CaptureInfo( | 467 self._server_path = gclient_scm.CaptureSVNInfo( |
| 468 self.AbsoluteLocalPath()).get('URL', '') | 468 self.AbsoluteLocalPath()).get('URL', '') |
| 469 return self._server_path | 469 return self._server_path |
| 470 | 470 |
| 471 def IsDirectory(self): | 471 def IsDirectory(self): |
| 472 if self._is_directory is None: | 472 if self._is_directory is None: |
| 473 path = self.AbsoluteLocalPath() | 473 path = self.AbsoluteLocalPath() |
| 474 if os.path.exists(path): | 474 if os.path.exists(path): |
| 475 # Retrieve directly from the file system; it is much faster than | 475 # Retrieve directly from the file system; it is much faster than |
| 476 # querying subversion, especially on Windows. | 476 # querying subversion, especially on Windows. |
| 477 self._is_directory = os.path.isdir(path) | 477 self._is_directory = os.path.isdir(path) |
| 478 else: | 478 else: |
| 479 self._is_directory = scm.SVN.CaptureInfo( | 479 self._is_directory = gclient_scm.CaptureSVNInfo( |
| 480 path).get('Node Kind') in ('dir', 'directory') | 480 path).get('Node Kind') in ('dir', 'directory') |
| 481 return self._is_directory | 481 return self._is_directory |
| 482 | 482 |
| 483 def Property(self, property_name): | 483 def Property(self, property_name): |
| 484 if not property_name in self._properties: | 484 if not property_name in self._properties: |
| 485 self._properties[property_name] = scm.SVN.GetFileProperty( | 485 self._properties[property_name] = gcl.GetSVNFileProperty( |
| 486 self.AbsoluteLocalPath(), property_name).rstrip() | 486 self.AbsoluteLocalPath(), property_name).rstrip() |
| 487 return self._properties[property_name] | 487 return self._properties[property_name] |
| 488 | 488 |
| 489 def IsTextFile(self): | 489 def IsTextFile(self): |
| 490 if self._is_text_file is None: | 490 if self._is_text_file is None: |
| 491 if self.Action() == 'D': | 491 if self.Action() == 'D': |
| 492 # A deleted file is not a text file. | 492 # A deleted file is not a text file. |
| 493 self._is_text_file = False | 493 self._is_text_file = False |
| 494 elif self.IsDirectory(): | 494 elif self.IsDirectory(): |
| 495 self._is_text_file = False | 495 self._is_text_file = False |
| 496 else: | 496 else: |
| 497 mime_type = scm.SVN.GetFileProperty(self.AbsoluteLocalPath(), | 497 mime_type = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(), |
| 498 'svn:mime-type') | 498 'svn:mime-type') |
| 499 self._is_text_file = (not mime_type or mime_type.startswith('text/')) | 499 self._is_text_file = (not mime_type or mime_type.startswith('text/')) |
| 500 return self._is_text_file | 500 return self._is_text_file |
| 501 | 501 |
| 502 | 502 |
| 503 class GitAffectedFile(AffectedFile): | 503 class GitAffectedFile(AffectedFile): |
| 504 """Representation of a file in a change out of a git checkout.""" | 504 """Representation of a file in a change out of a git checkout.""" |
| 505 | 505 |
| 506 def __init__(self, *args, **kwargs): | 506 def __init__(self, *args, **kwargs): |
| 507 AffectedFile.__init__(self, *args, **kwargs) | 507 AffectedFile.__init__(self, *args, **kwargs) |
| 508 self._server_path = None | 508 self._server_path = None |
| (...skipping 293 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 802 executer = GetTrySlavesExecuter() | 802 executer = GetTrySlavesExecuter() |
| 803 if default_presubmit: | 803 if default_presubmit: |
| 804 if verbose: | 804 if verbose: |
| 805 output_stream.write("Running default presubmit script.\n") | 805 output_stream.write("Running default presubmit script.\n") |
| 806 results += executer.ExecPresubmitScript(default_presubmit) | 806 results += executer.ExecPresubmitScript(default_presubmit) |
| 807 for filename in presubmit_files: | 807 for filename in presubmit_files: |
| 808 filename = os.path.abspath(filename) | 808 filename = os.path.abspath(filename) |
| 809 if verbose: | 809 if verbose: |
| 810 output_stream.write("Running %s\n" % filename) | 810 output_stream.write("Running %s\n" % filename) |
| 811 # Accept CRLF presubmit script. | 811 # Accept CRLF presubmit script. |
| 812 presubmit_script = gclient_utils.FileRead(filename, 'rU') | 812 presubmit_script = gcl.ReadFile(filename, 'rU') |
| 813 results += executer.ExecPresubmitScript(presubmit_script) | 813 results += executer.ExecPresubmitScript(presubmit_script) |
| 814 | 814 |
| 815 slaves = list(set(results)) | 815 slaves = list(set(results)) |
| 816 if slaves and verbose: | 816 if slaves and verbose: |
| 817 output_stream.write(', '.join(slaves)) | 817 output_stream.write(', '.join(slaves)) |
| 818 output_stream.write('\n') | 818 output_stream.write('\n') |
| 819 return slaves | 819 return slaves |
| 820 | 820 |
| 821 | 821 |
| 822 class PresubmitExecuter(object): | 822 class PresubmitExecuter(object): |
| (...skipping 95 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 918 if default_presubmit: | 918 if default_presubmit: |
| 919 if verbose: | 919 if verbose: |
| 920 output_stream.write("Running default presubmit script.\n") | 920 output_stream.write("Running default presubmit script.\n") |
| 921 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') | 921 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') |
| 922 results += executer.ExecPresubmitScript(default_presubmit, fake_path) | 922 results += executer.ExecPresubmitScript(default_presubmit, fake_path) |
| 923 for filename in presubmit_files: | 923 for filename in presubmit_files: |
| 924 filename = os.path.abspath(filename) | 924 filename = os.path.abspath(filename) |
| 925 if verbose: | 925 if verbose: |
| 926 output_stream.write("Running %s\n" % filename) | 926 output_stream.write("Running %s\n" % filename) |
| 927 # Accept CRLF presubmit script. | 927 # Accept CRLF presubmit script. |
| 928 presubmit_script = gclient_utils.FileRead(filename, 'rU') | 928 presubmit_script = gcl.ReadFile(filename, 'rU') |
| 929 results += executer.ExecPresubmitScript(presubmit_script, filename) | 929 results += executer.ExecPresubmitScript(presubmit_script, filename) |
| 930 | 930 |
| 931 errors = [] | 931 errors = [] |
| 932 notifications = [] | 932 notifications = [] |
| 933 warnings = [] | 933 warnings = [] |
| 934 for result in results: | 934 for result in results: |
| 935 if not result.IsFatal() and not result.ShouldPrompt(): | 935 if not result.IsFatal() and not result.ShouldPrompt(): |
| 936 notifications.append(result) | 936 notifications.append(result) |
| 937 elif result.ShouldPrompt(): | 937 elif result.ShouldPrompt(): |
| 938 warnings.append(result) | 938 warnings.append(result) |
| (...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1015 options, args = parser.parse_args(argv[1:]) | 1015 options, args = parser.parse_args(argv[1:]) |
| 1016 if not options.root: | 1016 if not options.root: |
| 1017 options.root = os.getcwd() | 1017 options.root = os.getcwd() |
| 1018 if os.path.isdir(os.path.join(options.root, '.git')): | 1018 if os.path.isdir(os.path.join(options.root, '.git')): |
| 1019 change_class = GitChange | 1019 change_class = GitChange |
| 1020 if not options.files: | 1020 if not options.files: |
| 1021 if args: | 1021 if args: |
| 1022 options.files = ParseFiles(args, options.recursive) | 1022 options.files = ParseFiles(args, options.recursive) |
| 1023 else: | 1023 else: |
| 1024 # Grab modified files. | 1024 # Grab modified files. |
| 1025 options.files = scm.GIT.CaptureStatus([options.root]) | 1025 options.files = gclient_scm.CaptureGitStatus([options.root]) |
| 1026 elif os.path.isdir(os.path.join(options.root, '.svn')): | 1026 elif os.path.isdir(os.path.join(options.root, '.svn')): |
| 1027 change_class = SvnChange | 1027 change_class = SvnChange |
| 1028 if not options.files: | 1028 if not options.files: |
| 1029 if args: | 1029 if args: |
| 1030 options.files = ParseFiles(args, options.recursive) | 1030 options.files = ParseFiles(args, options.recursive) |
| 1031 else: | 1031 else: |
| 1032 # Grab modified files. | 1032 # Grab modified files. |
| 1033 options.files = scm.SVN.CaptureStatus([options.root]) | 1033 options.files = gclient_scm.CaptureSVNStatus([options.root]) |
| 1034 else: | 1034 else: |
| 1035 # Doesn't seem under source control. | 1035 # Doesn't seem under source control. |
| 1036 change_class = Change | 1036 change_class = Change |
| 1037 if options.verbose: | 1037 if options.verbose: |
| 1038 if len(options.files) != 1: | 1038 if len(options.files) != 1: |
| 1039 print "Found %d files." % len(options.files) | 1039 print "Found %d files." % len(options.files) |
| 1040 else: | 1040 else: |
| 1041 print "Found 1 file." | 1041 print "Found 1 file." |
| 1042 return not DoPresubmitChecks(change_class(options.name, | 1042 return not DoPresubmitChecks(change_class(options.name, |
| 1043 options.description, | 1043 options.description, |
| 1044 options.root, | 1044 options.root, |
| 1045 options.files, | 1045 options.files, |
| 1046 options.issue, | 1046 options.issue, |
| 1047 options.patchset), | 1047 options.patchset), |
| 1048 options.commit, | 1048 options.commit, |
| 1049 options.verbose, | 1049 options.verbose, |
| 1050 sys.stdout, | 1050 sys.stdout, |
| 1051 sys.stdin, | 1051 sys.stdin, |
| 1052 options.default_presubmit, | 1052 options.default_presubmit, |
| 1053 options.may_prompt) | 1053 options.may_prompt) |
| 1054 | 1054 |
| 1055 | 1055 |
| 1056 if __name__ == '__main__': | 1056 if __name__ == '__main__': |
| 1057 sys.exit(Main(sys.argv)) | 1057 sys.exit(Main(sys.argv)) |
| OLD | NEW |