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.3' | 9 __version__ = '1.3.4' |
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. | |
40 import gcl | 38 import gcl |
41 import gclient_scm | 39 import gclient_utils |
42 import presubmit_canned_checks | 40 import presubmit_canned_checks |
| 41 import scm |
43 | 42 |
44 | 43 |
45 # Ask for feedback only once in program lifetime. | 44 # Ask for feedback only once in program lifetime. |
46 _ASKED_FOR_FEEDBACK = False | 45 _ASKED_FOR_FEEDBACK = False |
47 | 46 |
48 | 47 |
49 class NotImplementedException(Exception): | 48 class NotImplementedException(Exception): |
50 """We're leaving placeholders in a bunch of places to remind us of the | 49 """We're leaving placeholders in a bunch of places to remind us of the |
51 design of the API, but we have not implemented all of it yet. Implement as | 50 design of the API, but we have not implemented all of it yet. Implement as |
52 the need arises. | 51 the need arises. |
(...skipping 181 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
234 | 233 |
235 Args: | 234 Args: |
236 Depot path as a string. | 235 Depot path as a string. |
237 | 236 |
238 Returns: | 237 Returns: |
239 The local path of the depot path under the user's current client, or None | 238 The local path of the depot path under the user's current client, or None |
240 if the file is not mapped. | 239 if the file is not mapped. |
241 | 240 |
242 Remember to check for the None case and show an appropriate error! | 241 Remember to check for the None case and show an appropriate error! |
243 """ | 242 """ |
244 local_path = gclient_scm.CaptureSVNInfo(depot_path).get('Path') | 243 local_path = scm.SVN.CaptureInfo(depot_path).get('Path') |
245 if local_path: | 244 if local_path: |
246 return local_path | 245 return local_path |
247 | 246 |
248 def LocalToDepotPath(self, local_path): | 247 def LocalToDepotPath(self, local_path): |
249 """Translate a local path to a depot path. | 248 """Translate a local path to a depot path. |
250 | 249 |
251 Args: | 250 Args: |
252 Local path (relative to current directory, or absolute) as a string. | 251 Local path (relative to current directory, or absolute) as a string. |
253 | 252 |
254 Returns: | 253 Returns: |
255 The depot path (SVN URL) of the file if mapped, otherwise None. | 254 The depot path (SVN URL) of the file if mapped, otherwise None. |
256 """ | 255 """ |
257 depot_path = gclient_scm.CaptureSVNInfo(local_path).get('URL') | 256 depot_path = scm.SVN.CaptureInfo(local_path).get('URL') |
258 if depot_path: | 257 if depot_path: |
259 return depot_path | 258 return depot_path |
260 | 259 |
261 def AffectedFiles(self, include_dirs=False, include_deletes=True): | 260 def AffectedFiles(self, include_dirs=False, include_deletes=True): |
262 """Same as input_api.change.AffectedFiles() except only lists files | 261 """Same as input_api.change.AffectedFiles() except only lists files |
263 (and optionally directories) in the same directory as the current presubmit | 262 (and optionally directories) in the same directory as the current presubmit |
264 script, or subdirectories thereof. | 263 script, or subdirectories thereof. |
265 """ | 264 """ |
266 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath()) | 265 dir_with_slash = normpath("%s/" % self.PresubmitLocalPath()) |
267 if len(dir_with_slash) == 1: | 266 if len(dir_with_slash) == 1: |
(...skipping 79 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
347 | 346 |
348 def ReadFile(self, file_item, mode='r'): | 347 def ReadFile(self, file_item, mode='r'): |
349 """Reads an arbitrary file. | 348 """Reads an arbitrary file. |
350 | 349 |
351 Deny reading anything outside the repository. | 350 Deny reading anything outside the repository. |
352 """ | 351 """ |
353 if isinstance(file_item, AffectedFile): | 352 if isinstance(file_item, AffectedFile): |
354 file_item = file_item.AbsoluteLocalPath() | 353 file_item = file_item.AbsoluteLocalPath() |
355 if not file_item.startswith(self.change.RepositoryRoot()): | 354 if not file_item.startswith(self.change.RepositoryRoot()): |
356 raise IOError('Access outside the repository root is denied.') | 355 raise IOError('Access outside the repository root is denied.') |
357 return gcl.ReadFile(file_item, mode) | 356 return gclient_utils.FileRead(file_item, mode) |
358 | 357 |
359 @staticmethod | 358 @staticmethod |
360 def _RightHandSideLinesImpl(affected_files): | 359 def _RightHandSideLinesImpl(affected_files): |
361 """Implements RightHandSideLines for InputApi and GclChange.""" | 360 """Implements RightHandSideLines for InputApi and GclChange.""" |
362 for af in affected_files: | 361 for af in affected_files: |
363 lines = af.NewContents() | 362 lines = af.NewContents() |
364 line_number = 0 | 363 line_number = 0 |
365 for line in lines: | 364 for line in lines: |
366 line_number += 1 | 365 line_number += 1 |
367 yield (af, line_number, line) | 366 yield (af, line_number, line) |
(...skipping 57 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
425 | 424 |
426 The new version is the file in the user's workspace, i.e. the "right hand | 425 The new version is the file in the user's workspace, i.e. the "right hand |
427 side". | 426 side". |
428 | 427 |
429 Contents will be empty if the file is a directory or does not exist. | 428 Contents will be empty if the file is a directory or does not exist. |
430 Note: The cariage returns (LF or CR) are stripped off. | 429 Note: The cariage returns (LF or CR) are stripped off. |
431 """ | 430 """ |
432 if self.IsDirectory(): | 431 if self.IsDirectory(): |
433 return [] | 432 return [] |
434 else: | 433 else: |
435 return gcl.ReadFile(self.AbsoluteLocalPath()).splitlines() | 434 return gclient_utils.FileRead(self.AbsoluteLocalPath(), |
| 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 = gclient_scm.CaptureSVNInfo( | 467 self._server_path = scm.SVN.CaptureInfo( |
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 = gclient_scm.CaptureSVNInfo( | 479 self._is_directory = scm.SVN.CaptureInfo( |
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] = gcl.GetSVNFileProperty( | 485 self._properties[property_name] = scm.SVN.GetFileProperty( |
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 = gcl.GetSVNFileProperty(self.AbsoluteLocalPath(), | 497 mime_type = scm.SVN.GetFileProperty(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 = gcl.ReadFile(filename, 'rU') | 812 presubmit_script = gclient_utils.FileRead(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 = gcl.ReadFile(filename, 'rU') | 928 presubmit_script = gclient_utils.FileRead(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 = gclient_scm.CaptureGitStatus([options.root]) | 1025 options.files = scm.GIT.CaptureStatus([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 = gclient_scm.CaptureSVNStatus([options.root]) | 1033 options.files = scm.SVN.CaptureStatus([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 |