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 |