Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(112)

Side by Side Diff: presubmit_support.py

Issue 6694009: refactor presubmit parsing code from git-cl into presubmit (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/depot_tools
Patch Set: backport git-cl fixes from gcl_owners patch Created 9 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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
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
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
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
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))
OLDNEW
« no previous file with comments | « presubmit_canned_checks.py ('k') | tests/presubmit_unittest.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698