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 |