| OLD | NEW |
| 1 #!/usr/bin/python | 1 #!/usr/bin/python |
| 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. | 2 # Copyright (c) 2010 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.5' | 9 __version__ = '1.3.5' |
| 10 | 10 |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 71 def normpath(path): | 71 def normpath(path): |
| 72 '''Version of os.path.normpath that also changes backward slashes to | 72 '''Version of os.path.normpath that also changes backward slashes to |
| 73 forward slashes when not running on Windows. | 73 forward slashes when not running on Windows. |
| 74 ''' | 74 ''' |
| 75 # This is safe to always do because the Windows version of os.path.normpath | 75 # This is safe to always do because the Windows version of os.path.normpath |
| 76 # will replace forward slashes with backward slashes. | 76 # will replace forward slashes with backward slashes. |
| 77 path = path.replace(os.sep, '/') | 77 path = path.replace(os.sep, '/') |
| 78 return os.path.normpath(path) | 78 return os.path.normpath(path) |
| 79 | 79 |
| 80 | 80 |
| 81 def PromptYesNo(input_stream, output_stream, prompt): | |
| 82 output_stream.write(prompt) | |
| 83 response = input_stream.readline().strip().lower() | |
| 84 return response == 'y' or response == 'yes' | |
| 85 | |
| 86 | |
| 87 def _RightHandSideLinesImpl(affected_files): | 81 def _RightHandSideLinesImpl(affected_files): |
| 88 """Implements RightHandSideLines for InputApi and GclChange.""" | 82 """Implements RightHandSideLines for InputApi and GclChange.""" |
| 89 for af in affected_files: | 83 for af in affected_files: |
| 90 lines = af.ChangedContents() | 84 lines = af.ChangedContents() |
| 91 for line in lines: | 85 for line in lines: |
| 92 yield (af, line[0], line[1]) | 86 yield (af, line[0], line[1]) |
| 93 | 87 |
| 94 | 88 |
| 89 class PresubmitOutput(object): |
| 90 def __init__(self, input_stream=None, output_stream=None): |
| 91 self.input_stream = input_stream |
| 92 self.output_stream = output_stream |
| 93 self.reviewers = [] |
| 94 self.written_output = [] |
| 95 self.error_count = 0 |
| 96 |
| 97 def prompt_yes_no(self, prompt_string): |
| 98 self.write(prompt_string) |
| 99 if self.input_stream: |
| 100 response = self.input_stream.readline().strip().lower() |
| 101 if response not in ('y', 'yes'): |
| 102 self.fail() |
| 103 else: |
| 104 self.fail() |
| 105 |
| 106 def fail(self): |
| 107 self.error_count += 1 |
| 108 |
| 109 def should_continue(self): |
| 110 return not self.error_count |
| 111 |
| 112 def write(self, s): |
| 113 self.written_output.append(s) |
| 114 if self.output_stream: |
| 115 self.output_stream.write(s) |
| 116 |
| 117 def getvalue(self): |
| 118 return ''.join(self.written_output) |
| 119 |
| 120 |
| 95 class OutputApi(object): | 121 class OutputApi(object): |
| 96 """This class (more like a module) gets passed to presubmit scripts so that | 122 """This class (more like a module) gets passed to presubmit scripts so that |
| 97 they can specify various types of results. | 123 they can specify various types of results. |
| 98 """ | 124 """ |
| 99 # Method could be a function | |
| 100 # pylint: disable=R0201 | |
| 101 class PresubmitResult(object): | 125 class PresubmitResult(object): |
| 102 """Base class for result objects.""" | 126 """Base class for result objects.""" |
| 127 fatal = False |
| 128 should_prompt = False |
| 103 | 129 |
| 104 def __init__(self, message, items=None, long_text=''): | 130 def __init__(self, message, items=None, long_text=''): |
| 105 """ | 131 """ |
| 106 message: A short one-line message to indicate errors. | 132 message: A short one-line message to indicate errors. |
| 107 items: A list of short strings to indicate where errors occurred. | 133 items: A list of short strings to indicate where errors occurred. |
| 108 long_text: multi-line text output, e.g. from another tool | 134 long_text: multi-line text output, e.g. from another tool |
| 109 """ | 135 """ |
| 110 self._message = message | 136 self._message = message |
| 111 self._items = [] | 137 self._items = [] |
| 112 if items: | 138 if items: |
| 113 self._items = items | 139 self._items = items |
| 114 self._long_text = long_text.rstrip() | 140 self._long_text = long_text.rstrip() |
| 115 | 141 |
| 116 def _Handle(self, output_stream, input_stream, may_prompt=True): | 142 def handle(self, output): |
| 117 """Writes this result to the output stream. | 143 output.write(self._message) |
| 118 | 144 output.write('\n') |
| 119 Args: | |
| 120 output_stream: Where to write | |
| 121 | |
| 122 Returns: | |
| 123 True if execution may continue, False otherwise. | |
| 124 """ | |
| 125 output_stream.write(self._message) | |
| 126 output_stream.write('\n') | |
| 127 if len(self._items) > 0: | 145 if len(self._items) > 0: |
| 128 output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n') | 146 output.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n') |
| 129 if self._long_text: | 147 if self._long_text: |
| 130 # Sometimes self._long_text is a ascii string, a codepage string | 148 # Sometimes self._long_text is a ascii string, a codepage string |
| 131 # (on windows), or a unicode object. | 149 # (on windows), or a unicode object. |
| 132 try: | 150 try: |
| 133 long_text = self._long_text.decode() | 151 long_text = self._long_text.decode() |
| 134 except UnicodeDecodeError: | 152 except UnicodeDecodeError: |
| 135 long_text = self._long_text.decode('ascii', 'replace') | 153 long_text = self._long_text.decode('ascii', 'replace') |
| 136 | 154 |
| 137 output_stream.write('\n***************\n%s\n***************\n' % | 155 output.write('\n***************\n%s\n***************\n' % |
| 138 long_text) | 156 long_text) |
| 157 if self.fatal: |
| 158 output.fail() |
| 139 | 159 |
| 140 if self.ShouldPrompt() and may_prompt: | 160 class PresubmitAddReviewers(PresubmitResult): |
| 141 if not PromptYesNo(input_stream, output_stream, | 161 """Add some suggested reviewers to the change.""" |
| 142 'Are you sure you want to continue? (y/N): '): | 162 def __init__(self, reviewers): |
| 143 return False | 163 super(OutputApi.PresubmitAddReviewers, self).__init__('') |
| 164 self.reviewers = reviewers |
| 144 | 165 |
| 145 return not self.IsFatal() | 166 def handle(self, output): |
| 146 | 167 output.reviewers.extend(self.reviewers) |
| 147 def IsFatal(self): | |
| 148 """An error that is fatal stops g4 mail/submit immediately, i.e. before | |
| 149 other presubmit scripts are run. | |
| 150 """ | |
| 151 return False | |
| 152 | |
| 153 def ShouldPrompt(self): | |
| 154 """Whether this presubmit result should result in a prompt warning.""" | |
| 155 return False | |
| 156 | |
| 157 class PresubmitAddText(PresubmitResult): | |
| 158 """Propagates a line of text back to the caller.""" | |
| 159 def __init__(self, message, items=None, long_text=''): | |
| 160 super(OutputApi.PresubmitAddText, self).__init__("ADD: " + message, | |
| 161 items, long_text) | |
| 162 | 168 |
| 163 class PresubmitError(PresubmitResult): | 169 class PresubmitError(PresubmitResult): |
| 164 """A hard presubmit error.""" | 170 """A hard presubmit error.""" |
| 165 def IsFatal(self): | 171 fatal = True |
| 166 return True | |
| 167 | 172 |
| 168 class PresubmitPromptWarning(PresubmitResult): | 173 class PresubmitPromptWarning(PresubmitResult): |
| 169 """An warning that prompts the user if they want to continue.""" | 174 """An warning that prompts the user if they want to continue.""" |
| 170 def ShouldPrompt(self): | 175 should_prompt = True |
| 171 return True | |
| 172 | 176 |
| 173 class PresubmitNotifyResult(PresubmitResult): | 177 class PresubmitNotifyResult(PresubmitResult): |
| 174 """Just print something to the screen -- but it's not even a warning.""" | 178 """Just print something to the screen -- but it's not even a warning.""" |
| 175 pass | 179 pass |
| 176 | 180 |
| 177 class MailTextResult(PresubmitResult): | 181 class MailTextResult(PresubmitResult): |
| 178 """A warning that should be included in the review request email.""" | 182 """A warning that should be included in the review request email.""" |
| 179 def __init__(self, *args, **kwargs): | 183 def __init__(self, *args, **kwargs): |
| 180 super(OutputApi.MailTextResult, self).__init__() | 184 super(OutputApi.MailTextResult, self).__init__() |
| 181 raise NotImplementedException() | 185 raise NotImplementedException() |
| (...skipping 804 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 986 raise exceptions.RuntimeError( | 990 raise exceptions.RuntimeError( |
| 987 'All presubmit results must be of types derived from ' | 991 'All presubmit results must be of types derived from ' |
| 988 'output_api.PresubmitResult') | 992 'output_api.PresubmitResult') |
| 989 else: | 993 else: |
| 990 result = () # no error since the script doesn't care about current event. | 994 result = () # no error since the script doesn't care about current event. |
| 991 | 995 |
| 992 # Return the process to the original working directory. | 996 # Return the process to the original working directory. |
| 993 os.chdir(main_path) | 997 os.chdir(main_path) |
| 994 return result | 998 return result |
| 995 | 999 |
| 1000 |
| 996 # TODO(dpranke): make all callers pass in tbr, host_url? | 1001 # TODO(dpranke): make all callers pass in tbr, host_url? |
| 997 def DoPresubmitChecks(change, | 1002 def DoPresubmitChecks(change, |
| 998 committing, | 1003 committing, |
| 999 verbose, | 1004 verbose, |
| 1000 output_stream, | 1005 output_stream, |
| 1001 input_stream, | 1006 input_stream, |
| 1002 default_presubmit, | 1007 default_presubmit, |
| 1003 may_prompt, | 1008 may_prompt, |
| 1004 tbr=False, | 1009 tbr=False, |
| 1005 host_url=None): | 1010 host_url=None): |
| (...skipping 16 matching lines...) Expand all Loading... |
| 1022 may_prompt: Enable (y/n) questions on warning or error. | 1027 may_prompt: Enable (y/n) questions on warning or error. |
| 1023 tbr: was --tbr specified to skip any reviewer/owner checks? | 1028 tbr: was --tbr specified to skip any reviewer/owner checks? |
| 1024 host_url: scheme, host, and port of host to use for rietveld-related | 1029 host_url: scheme, host, and port of host to use for rietveld-related |
| 1025 checks | 1030 checks |
| 1026 | 1031 |
| 1027 Warning: | 1032 Warning: |
| 1028 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream | 1033 If may_prompt is true, output_stream SHOULD be sys.stdout and input_stream |
| 1029 SHOULD be sys.stdin. | 1034 SHOULD be sys.stdin. |
| 1030 | 1035 |
| 1031 Return: | 1036 Return: |
| 1032 True if execution can continue, False if not. | 1037 A PresubmitOutput object. Use output.should_continue() to figure out |
| 1038 if there were errors or warnings and the caller should abort. |
| 1033 """ | 1039 """ |
| 1034 print "Running presubmit hooks..." | 1040 output = PresubmitOutput(input_stream, output_stream) |
| 1041 output.write("Running presubmit hooks...\n") |
| 1035 start_time = time.time() | 1042 start_time = time.time() |
| 1036 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True), | 1043 presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True), |
| 1037 change.RepositoryRoot()) | 1044 change.RepositoryRoot()) |
| 1038 if not presubmit_files and verbose: | 1045 if not presubmit_files and verbose: |
| 1039 output_stream.write("Warning, no presubmit.py found.\n") | 1046 output.write("Warning, no presubmit.py found.\n") |
| 1040 results = [] | 1047 results = [] |
| 1041 executer = PresubmitExecuter(change, committing, tbr, host_url) | 1048 executer = PresubmitExecuter(change, committing, tbr, host_url) |
| 1042 if default_presubmit: | 1049 if default_presubmit: |
| 1043 if verbose: | 1050 if verbose: |
| 1044 output_stream.write("Running default presubmit script.\n") | 1051 output.write("Running default presubmit script.\n") |
| 1045 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') | 1052 fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') |
| 1046 results += executer.ExecPresubmitScript(default_presubmit, fake_path) | 1053 results += executer.ExecPresubmitScript(default_presubmit, fake_path) |
| 1047 for filename in presubmit_files: | 1054 for filename in presubmit_files: |
| 1048 filename = os.path.abspath(filename) | 1055 filename = os.path.abspath(filename) |
| 1049 if verbose: | 1056 if verbose: |
| 1050 output_stream.write("Running %s\n" % filename) | 1057 output.write("Running %s\n" % filename) |
| 1051 # Accept CRLF presubmit script. | 1058 # Accept CRLF presubmit script. |
| 1052 presubmit_script = gclient_utils.FileRead(filename, 'rU') | 1059 presubmit_script = gclient_utils.FileRead(filename, 'rU') |
| 1053 results += executer.ExecPresubmitScript(presubmit_script, filename) | 1060 results += executer.ExecPresubmitScript(presubmit_script, filename) |
| 1054 | 1061 |
| 1055 errors = [] | 1062 errors = [] |
| 1056 notifications = [] | 1063 notifications = [] |
| 1057 warnings = [] | 1064 warnings = [] |
| 1058 for result in results: | 1065 for result in results: |
| 1059 if not result.IsFatal() and not result.ShouldPrompt(): | 1066 if result.fatal: |
| 1060 notifications.append(result) | 1067 errors.append(result) |
| 1061 elif result.ShouldPrompt(): | 1068 elif result.should_prompt: |
| 1062 warnings.append(result) | 1069 warnings.append(result) |
| 1063 else: | 1070 else: |
| 1064 errors.append(result) | 1071 notifications.append(result) |
| 1065 | 1072 |
| 1066 error_count = 0 | |
| 1067 for name, items in (('Messages', notifications), | 1073 for name, items in (('Messages', notifications), |
| 1068 ('Warnings', warnings), | 1074 ('Warnings', warnings), |
| 1069 ('ERRORS', errors)): | 1075 ('ERRORS', errors)): |
| 1070 if items: | 1076 if items: |
| 1071 output_stream.write('** Presubmit %s **\n' % name) | 1077 output.write('** Presubmit %s **\n' % name) |
| 1072 for item in items: | 1078 for item in items: |
| 1073 # Access to a protected member XXX of a client class | 1079 item.handle(output) |
| 1074 # pylint: disable=W0212 | 1080 output.write('\n') |
| 1075 if not item._Handle(output_stream, input_stream, | |
| 1076 may_prompt=False): | |
| 1077 error_count += 1 | |
| 1078 output_stream.write('\n') | |
| 1079 | 1081 |
| 1080 total_time = time.time() - start_time | 1082 total_time = time.time() - start_time |
| 1081 if total_time > 1.0: | 1083 if total_time > 1.0: |
| 1082 print "Presubmit checks took %.1fs to calculate." % total_time | 1084 output.write("Presubmit checks took %.1fs to calculate.\n" % total_time) |
| 1083 | 1085 |
| 1084 if not errors and warnings and may_prompt: | 1086 if not errors and warnings and may_prompt: |
| 1085 if not PromptYesNo(input_stream, output_stream, | 1087 output.prompt_yes_no('There were presubmit warnings. ' |
| 1086 'There were presubmit warnings. ' | 1088 'Are you sure you wish to continue? (y/N): ') |
| 1087 'Are you sure you wish to continue? (y/N): '): | |
| 1088 error_count += 1 | |
| 1089 | 1089 |
| 1090 global _ASKED_FOR_FEEDBACK | 1090 global _ASKED_FOR_FEEDBACK |
| 1091 # Ask for feedback one time out of 5. | 1091 # Ask for feedback one time out of 5. |
| 1092 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK): | 1092 if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK): |
| 1093 output_stream.write("Was the presubmit check useful? Please send feedback " | 1093 output.write("Was the presubmit check useful? Please send feedback " |
| 1094 "& hate mail to maruel@chromium.org!\n") | 1094 "& hate mail to maruel@chromium.org!\n") |
| 1095 _ASKED_FOR_FEEDBACK = True | 1095 _ASKED_FOR_FEEDBACK = True |
| 1096 return (error_count == 0) | 1096 return output |
| 1097 | 1097 |
| 1098 | 1098 |
| 1099 def ScanSubDirs(mask, recursive): | 1099 def ScanSubDirs(mask, recursive): |
| 1100 if not recursive: | 1100 if not recursive: |
| 1101 return [x for x in glob.glob(mask) if '.svn' not in x and '.git' not in x] | 1101 return [x for x in glob.glob(mask) if '.svn' not in x and '.git' not in x] |
| 1102 else: | 1102 else: |
| 1103 results = [] | 1103 results = [] |
| 1104 for root, dirs, files in os.walk('.'): | 1104 for root, dirs, files in os.walk('.'): |
| 1105 if '.svn' in dirs: | 1105 if '.svn' in dirs: |
| 1106 dirs.remove('.svn') | 1106 dirs.remove('.svn') |
| (...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 1172 else: | 1172 else: |
| 1173 logging.info('Doesn\'t seem under source control.') | 1173 logging.info('Doesn\'t seem under source control.') |
| 1174 change_class = Change | 1174 change_class = Change |
| 1175 if options.verbose: | 1175 if options.verbose: |
| 1176 if not options.files: | 1176 if not options.files: |
| 1177 print "Found no files." | 1177 print "Found no files." |
| 1178 elif len(options.files) != 1: | 1178 elif len(options.files) != 1: |
| 1179 print "Found %d files." % len(options.files) | 1179 print "Found %d files." % len(options.files) |
| 1180 else: | 1180 else: |
| 1181 print "Found 1 file." | 1181 print "Found 1 file." |
| 1182 return not DoPresubmitChecks(change_class(options.name, | 1182 results = DoPresubmitChecks(change_class(options.name, |
| 1183 options.description, | 1183 options.description, |
| 1184 options.root, | 1184 options.root, |
| 1185 options.files, | 1185 options.files, |
| 1186 options.issue, | 1186 options.issue, |
| 1187 options.patchset), | 1187 options.patchset), |
| 1188 options.commit, | 1188 options.commit, |
| 1189 options.verbose, | 1189 options.verbose, |
| 1190 sys.stdout, | 1190 sys.stdout, |
| 1191 sys.stdin, | 1191 sys.stdin, |
| 1192 options.default_presubmit, | 1192 options.default_presubmit, |
| 1193 options.may_prompt) | 1193 options.may_prompt) |
| 1194 return not results.should_continue() |
| 1194 | 1195 |
| 1195 | 1196 |
| 1196 if __name__ == '__main__': | 1197 if __name__ == '__main__': |
| 1197 sys.exit(Main(sys.argv)) | 1198 sys.exit(Main(sys.argv)) |
| OLD | NEW |