Commit 5ac21011 authored by dpranke@chromium.org's avatar dpranke@chromium.org

This change moves the code I added to git-cl to parse the results of the...

This change moves the code I added to git-cl to parse the results of the presubmit hooks into presubmit_support itself. It also removes a lot of the text-file parsing that is no longer necessary since everything is in process.

It also simplifies all of the PresubmitResult* objects and the way we're using output streams since we can assume more about how the callers are calling us.

Note that I uses PEP-8 style method names where I was adding entirely new classes (or rewriting existing classes completely) since per maruel@ he is trying to new that style for new code.

This change also contains two small changes to git_cl to fix bugs and restore the previous behavior of --force skipping the presubmit checks.

Review URL: http://codereview.chromium.org/6694009

git-svn-id: svn://svn.chromium.org/chrome/trunk/tools/depot_tools@78328 0039d316-1c4b-4281-b951-d872f2087c98
parent 8c415129
...@@ -1169,16 +1169,19 @@ def DoPresubmitChecks(change_info, committing, may_prompt): ...@@ -1169,16 +1169,19 @@ def DoPresubmitChecks(change_info, committing, may_prompt):
change_info.GetFiles(), change_info.GetFiles(),
change_info.issue, change_info.issue,
change_info.patchset) change_info.patchset)
result = presubmit_support.DoPresubmitChecks(change=change, output = presubmit_support.DoPresubmitChecks(change=change,
committing=committing, committing=committing,
verbose=False, verbose=False,
output_stream=sys.stdout, output_stream=sys.stdout,
input_stream=sys.stdin, input_stream=sys.stdin,
default_presubmit=root_presubmit, default_presubmit=root_presubmit,
may_prompt=may_prompt) may_prompt=may_prompt)
if not result and may_prompt: if not output.should_continue() and may_prompt:
# TODO(dpranke): move into DoPresubmitChecks(), unify cmd line args.
print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)" print "\nPresubmit errors, can't continue (use --no_presubmit to bypass)"
return result
# TODO(dpranke): Return the output object and make use of it.
return output.should_continue()
@no_args @no_args
......
...@@ -482,29 +482,6 @@ def GetCodereviewSettingsInteractively(): ...@@ -482,29 +482,6 @@ def GetCodereviewSettingsInteractively():
# svn-based hackery. # svn-based hackery.
class HookResults(object):
"""Contains the parsed output of the presubmit hooks."""
def __init__(self, output_from_hooks=None):
self.reviewers = []
self.output = None
self._ParseOutputFromHooks(output_from_hooks)
def _ParseOutputFromHooks(self, output_from_hooks):
if not output_from_hooks:
return
lines = []
reviewers = []
reviewer_regexp = re.compile('ADD: R=(.+)')
for l in output_from_hooks.splitlines():
m = reviewer_regexp.match(l)
if m:
reviewers.extend(m.group(1).split(','))
else:
lines.append(l)
self.output = '\n'.join(lines)
self.reviewers = ','.join(reviewers)
class ChangeDescription(object): class ChangeDescription(object):
"""Contains a parsed form of the change description.""" """Contains a parsed form of the change description."""
def __init__(self, subject, log_desc, reviewers): def __init__(self, subject, log_desc, reviewers):
...@@ -772,31 +749,16 @@ def RunHook(committing, upstream_branch, rietveld_server, tbr, may_prompt): ...@@ -772,31 +749,16 @@ def RunHook(committing, upstream_branch, rietveld_server, tbr, may_prompt):
RunCommand(['git', 'config', '--replace-all', RunCommand(['git', 'config', '--replace-all',
'rietveld.extracc', ','.join(watchers)]) 'rietveld.extracc', ','.join(watchers)])
output = StringIO.StringIO() output = presubmit_support.DoPresubmitChecks(change, committing,
should_continue = presubmit_support.DoPresubmitChecks(change, committing, verbose=False, output_stream=sys.stdout, input_stream=sys.stdin,
verbose=None, output_stream=output, input_stream=sys.stdin, default_presubmit=None, may_prompt=may_prompt, tbr=tbr,
default_presubmit=None, may_prompt=False, tbr=tbr,
host_url=cl.GetRietveldServer()) host_url=cl.GetRietveldServer())
hook_results = HookResults(output.getvalue())
if hook_results.output:
print hook_results.output
# TODO(dpranke): We should propagate the error out instead of calling exit(). # TODO(dpranke): We should propagate the error out instead of calling exit().
if should_continue and hook_results.output and ( if not output.should_continue():
'** Presubmit ERRORS **\n' in hook_results.output or sys.exit(1)
'** Presubmit WARNINGS **\n' in hook_results.output):
should_continue = False
if not should_continue:
if may_prompt:
response = raw_input('Are you sure you want to continue? (y/N): ')
if not response.lower().startswith('y'):
sys.exit(1)
else:
sys.exit(1)
return hook_results return output
def CMDpresubmit(parser, args): def CMDpresubmit(parser, args):
...@@ -870,15 +832,13 @@ def CMDupload(parser, args): ...@@ -870,15 +832,13 @@ def CMDupload(parser, args):
base_branch = cl.GetUpstreamBranch() base_branch = cl.GetUpstreamBranch()
args = [base_branch + "..."] args = [base_branch + "..."]
if not options.bypass_hooks: if not options.bypass_hooks and not options.force:
hook_results = RunHook(committing=False, upstream_branch=base_branch, hook_results = RunHook(committing=False, upstream_branch=base_branch,
rietveld_server=cl.GetRietveldServer(), tbr=False, rietveld_server=cl.GetRietveldServer(), tbr=False,
may_prompt=(not options.force)) may_prompt=True)
else: if not options.reviewers and hook_results.reviewers:
hook_results = HookResults() options.reviewers = hook_results.reviewers
if not options.reviewers and hook_results.reviewers:
options.reviewers = hook_results.reviewers
# --no-ext-diff is broken in some versions of Git, so try to work around # --no-ext-diff is broken in some versions of Git, so try to work around
# this by overriding the environment (but there is still a problem if the # this by overriding the environment (but there is still a problem if the
...@@ -1023,12 +983,11 @@ def SendUpstream(parser, args, cmd): ...@@ -1023,12 +983,11 @@ def SendUpstream(parser, args, cmd):
'before attempting to %s.' % (base_branch, cmd)) 'before attempting to %s.' % (base_branch, cmd))
return 1 return 1
if not options.bypass_hooks: if not options.bypass_hooks and not options.force:
RunHook(committing=True, upstream_branch=base_branch, RunHook(committing=True, upstream_branch=base_branch,
rietveld_server=cl.GetRietveldServer(), tbr=options.tbr, rietveld_server=cl.GetRietveldServer(), tbr=options.tbr,
may_prompt=(not options.force)) may_prompt=True)
if not options.force and not options.bypass_hooks:
if cmd == 'dcommit': if cmd == 'dcommit':
# Check the tree status if the tree status URL is set. # Check the tree status if the tree status URL is set.
status = GetTreeStatus() status = GetTreeStatus()
......
...@@ -648,7 +648,7 @@ def CheckOwners(input_api, output_api, email_regexp=None, ...@@ -648,7 +648,7 @@ def CheckOwners(input_api, output_api, email_regexp=None,
return [] return []
suggested_reviewers = owners_db.reviewers_for(affected_files) suggested_reviewers = owners_db.reviewers_for(affected_files)
return [output_api.PresubmitAddText('R=%s' % ','.join(suggested_reviewers))] return [output_api.PresubmitAddReviewers(suggested_reviewers)]
def _Approvers(input_api, email_regexp): def _Approvers(input_api, email_regexp):
......
...@@ -78,12 +78,6 @@ def normpath(path): ...@@ -78,12 +78,6 @@ def normpath(path):
return os.path.normpath(path) return os.path.normpath(path)
def PromptYesNo(input_stream, output_stream, prompt):
output_stream.write(prompt)
response = input_stream.readline().strip().lower()
return response == 'y' or response == 'yes'
def _RightHandSideLinesImpl(affected_files): def _RightHandSideLinesImpl(affected_files):
"""Implements RightHandSideLines for InputApi and GclChange.""" """Implements RightHandSideLines for InputApi and GclChange."""
for af in affected_files: for af in affected_files:
...@@ -92,14 +86,46 @@ def _RightHandSideLinesImpl(affected_files): ...@@ -92,14 +86,46 @@ def _RightHandSideLinesImpl(affected_files):
yield (af, line[0], line[1]) yield (af, line[0], line[1])
class PresubmitOutput(object):
def __init__(self, input_stream=None, output_stream=None):
self.input_stream = input_stream
self.output_stream = output_stream
self.reviewers = []
self.written_output = []
self.error_count = 0
def prompt_yes_no(self, prompt_string):
self.write(prompt_string)
if self.input_stream:
response = self.input_stream.readline().strip().lower()
if response not in ('y', 'yes'):
self.fail()
else:
self.fail()
def fail(self):
self.error_count += 1
def should_continue(self):
return not self.error_count
def write(self, s):
self.written_output.append(s)
if self.output_stream:
self.output_stream.write(s)
def getvalue(self):
return ''.join(self.written_output)
class OutputApi(object): class OutputApi(object):
"""This class (more like a module) gets passed to presubmit scripts so that """This class (more like a module) gets passed to presubmit scripts so that
they can specify various types of results. they can specify various types of results.
""" """
# Method could be a function
# pylint: disable=R0201
class PresubmitResult(object): class PresubmitResult(object):
"""Base class for result objects.""" """Base class for result objects."""
fatal = False
should_prompt = False
def __init__(self, message, items=None, long_text=''): def __init__(self, message, items=None, long_text=''):
""" """
...@@ -113,19 +139,11 @@ class OutputApi(object): ...@@ -113,19 +139,11 @@ class OutputApi(object):
self._items = items self._items = items
self._long_text = long_text.rstrip() self._long_text = long_text.rstrip()
def _Handle(self, output_stream, input_stream, may_prompt=True): def handle(self, output):
"""Writes this result to the output stream. output.write(self._message)
output.write('\n')
Args:
output_stream: Where to write
Returns:
True if execution may continue, False otherwise.
"""
output_stream.write(self._message)
output_stream.write('\n')
if len(self._items) > 0: if len(self._items) > 0:
output_stream.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n') output.write(' ' + ' \\\n '.join(map(str, self._items)) + '\n')
if self._long_text: if self._long_text:
# Sometimes self._long_text is a ascii string, a codepage string # Sometimes self._long_text is a ascii string, a codepage string
# (on windows), or a unicode object. # (on windows), or a unicode object.
...@@ -134,41 +152,27 @@ class OutputApi(object): ...@@ -134,41 +152,27 @@ class OutputApi(object):
except UnicodeDecodeError: except UnicodeDecodeError:
long_text = self._long_text.decode('ascii', 'replace') long_text = self._long_text.decode('ascii', 'replace')
output_stream.write('\n***************\n%s\n***************\n' % output.write('\n***************\n%s\n***************\n' %
long_text) long_text)
if self.fatal:
output.fail()
if self.ShouldPrompt() and may_prompt: class PresubmitAddReviewers(PresubmitResult):
if not PromptYesNo(input_stream, output_stream, """Add some suggested reviewers to the change."""
'Are you sure you want to continue? (y/N): '): def __init__(self, reviewers):
return False super(OutputApi.PresubmitAddReviewers, self).__init__('')
self.reviewers = reviewers
return not self.IsFatal() def handle(self, output):
output.reviewers.extend(self.reviewers)
def IsFatal(self):
"""An error that is fatal stops g4 mail/submit immediately, i.e. before
other presubmit scripts are run.
"""
return False
def ShouldPrompt(self):
"""Whether this presubmit result should result in a prompt warning."""
return False
class PresubmitAddText(PresubmitResult):
"""Propagates a line of text back to the caller."""
def __init__(self, message, items=None, long_text=''):
super(OutputApi.PresubmitAddText, self).__init__("ADD: " + message,
items, long_text)
class PresubmitError(PresubmitResult): class PresubmitError(PresubmitResult):
"""A hard presubmit error.""" """A hard presubmit error."""
def IsFatal(self): fatal = True
return True
class PresubmitPromptWarning(PresubmitResult): class PresubmitPromptWarning(PresubmitResult):
"""An warning that prompts the user if they want to continue.""" """An warning that prompts the user if they want to continue."""
def ShouldPrompt(self): should_prompt = True
return True
class PresubmitNotifyResult(PresubmitResult): class PresubmitNotifyResult(PresubmitResult):
"""Just print something to the screen -- but it's not even a warning.""" """Just print something to the screen -- but it's not even a warning."""
...@@ -993,6 +997,7 @@ class PresubmitExecuter(object): ...@@ -993,6 +997,7 @@ class PresubmitExecuter(object):
os.chdir(main_path) os.chdir(main_path)
return result return result
# TODO(dpranke): make all callers pass in tbr, host_url? # TODO(dpranke): make all callers pass in tbr, host_url?
def DoPresubmitChecks(change, def DoPresubmitChecks(change,
committing, committing,
...@@ -1029,25 +1034,27 @@ def DoPresubmitChecks(change, ...@@ -1029,25 +1034,27 @@ def DoPresubmitChecks(change,
SHOULD be sys.stdin. SHOULD be sys.stdin.
Return: Return:
True if execution can continue, False if not. A PresubmitOutput object. Use output.should_continue() to figure out
if there were errors or warnings and the caller should abort.
""" """
print "Running presubmit hooks..." output = PresubmitOutput(input_stream, output_stream)
output.write("Running presubmit hooks...\n")
start_time = time.time() start_time = time.time()
presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True), presubmit_files = ListRelevantPresubmitFiles(change.AbsoluteLocalPaths(True),
change.RepositoryRoot()) change.RepositoryRoot())
if not presubmit_files and verbose: if not presubmit_files and verbose:
output_stream.write("Warning, no presubmit.py found.\n") output.write("Warning, no presubmit.py found.\n")
results = [] results = []
executer = PresubmitExecuter(change, committing, tbr, host_url) executer = PresubmitExecuter(change, committing, tbr, host_url)
if default_presubmit: if default_presubmit:
if verbose: if verbose:
output_stream.write("Running default presubmit script.\n") output.write("Running default presubmit script.\n")
fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py') fake_path = os.path.join(change.RepositoryRoot(), 'PRESUBMIT.py')
results += executer.ExecPresubmitScript(default_presubmit, fake_path) results += executer.ExecPresubmitScript(default_presubmit, fake_path)
for filename in presubmit_files: for filename in presubmit_files:
filename = os.path.abspath(filename) filename = os.path.abspath(filename)
if verbose: if verbose:
output_stream.write("Running %s\n" % filename) output.write("Running %s\n" % filename)
# Accept CRLF presubmit script. # Accept CRLF presubmit script.
presubmit_script = gclient_utils.FileRead(filename, 'rU') presubmit_script = gclient_utils.FileRead(filename, 'rU')
results += executer.ExecPresubmitScript(presubmit_script, filename) results += executer.ExecPresubmitScript(presubmit_script, filename)
...@@ -1056,44 +1063,37 @@ def DoPresubmitChecks(change, ...@@ -1056,44 +1063,37 @@ def DoPresubmitChecks(change,
notifications = [] notifications = []
warnings = [] warnings = []
for result in results: for result in results:
if not result.IsFatal() and not result.ShouldPrompt(): if result.fatal:
notifications.append(result) errors.append(result)
elif result.ShouldPrompt(): elif result.should_prompt:
warnings.append(result) warnings.append(result)
else: else:
errors.append(result) notifications.append(result)
error_count = 0
for name, items in (('Messages', notifications), for name, items in (('Messages', notifications),
('Warnings', warnings), ('Warnings', warnings),
('ERRORS', errors)): ('ERRORS', errors)):
if items: if items:
output_stream.write('** Presubmit %s **\n' % name) output.write('** Presubmit %s **\n' % name)
for item in items: for item in items:
# Access to a protected member XXX of a client class item.handle(output)
# pylint: disable=W0212 output.write('\n')
if not item._Handle(output_stream, input_stream,
may_prompt=False):
error_count += 1
output_stream.write('\n')
total_time = time.time() - start_time total_time = time.time() - start_time
if total_time > 1.0: if total_time > 1.0:
print "Presubmit checks took %.1fs to calculate." % total_time output.write("Presubmit checks took %.1fs to calculate.\n" % total_time)
if not errors and warnings and may_prompt: if not errors and warnings and may_prompt:
if not PromptYesNo(input_stream, output_stream, output.prompt_yes_no('There were presubmit warnings. '
'There were presubmit warnings. ' 'Are you sure you wish to continue? (y/N): ')
'Are you sure you wish to continue? (y/N): '):
error_count += 1
global _ASKED_FOR_FEEDBACK global _ASKED_FOR_FEEDBACK
# Ask for feedback one time out of 5. # Ask for feedback one time out of 5.
if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK): if (len(results) and random.randint(0, 4) == 0 and not _ASKED_FOR_FEEDBACK):
output_stream.write("Was the presubmit check useful? Please send feedback " output.write("Was the presubmit check useful? Please send feedback "
"& hate mail to maruel@chromium.org!\n") "& hate mail to maruel@chromium.org!\n")
_ASKED_FOR_FEEDBACK = True _ASKED_FOR_FEEDBACK = True
return (error_count == 0) return output
def ScanSubDirs(mask, recursive): def ScanSubDirs(mask, recursive):
...@@ -1179,18 +1179,19 @@ def Main(argv): ...@@ -1179,18 +1179,19 @@ def Main(argv):
print "Found %d files." % len(options.files) print "Found %d files." % len(options.files)
else: else:
print "Found 1 file." print "Found 1 file."
return not DoPresubmitChecks(change_class(options.name, results = DoPresubmitChecks(change_class(options.name,
options.description, options.description,
options.root, options.root,
options.files, options.files,
options.issue, options.issue,
options.patchset), options.patchset),
options.commit, options.commit,
options.verbose, options.verbose,
sys.stdout, sys.stdout,
sys.stdin, sys.stdin,
options.default_presubmit, options.default_presubmit,
options.may_prompt) options.may_prompt)
return not results.should_continue()
if __name__ == '__main__': if __name__ == '__main__':
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment