pre-upload: overhaul commit progress output

Our existing output shows each commit & hook being run, and then
dumps the full failure messages at the end.  Lets switch to the
style used in AOSP repohooks where output is more dynamic and
immediate when things go wrong.  For single commits this isn't a
big deal, but for multiple commits, this can be huge.

BUG=None
TEST=`repo upload` shows commit progress

Change-Id: I5b14488b7fe22f9d2e29adf7201db7f00590872a
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/repohooks/+/1965403
Reviewed-by: Michael Mortensen <mmortensen@google.com>
Commit-Queue: Mike Frysinger <vapier@chromium.org>
Tested-by: Mike Frysinger <vapier@chromium.org>
diff --git a/errors.py b/errors.py
index 35c0433..35ede84 100644
--- a/errors.py
+++ b/errors.py
@@ -42,12 +42,6 @@
   print(msg, file=sys.stderr)
 
 
-def _FormatCommitDesc(desc):
-  """Returns the properly prefixed commit description."""
-  regex = re.compile(r'^', re.M)
-  return regex.sub('>', desc)
-
-
 def _FormatHookFailure(hook_failure):
   """Returns the properly formatted VerifyException as a string."""
   item_prefix = '\n%s* ' % _INDENT
@@ -69,42 +63,21 @@
   print('', file=sys.stderr)
 
 
-def PrintErrorsForCommit(project, commit, commit_desc, error_list):
+def PrintErrorsForCommit(color, hook, project, error_list):
   """Prints the hook error to stderr with project and commit context
 
-  A sample error output for a project would be:
-  ----------------------------------------------------------------------------
-  Errors in PROJECT *chromiumos/repohooks*!
-    COMMIT 10041758:
-        Description:
-            >staged
-            >
-            >TEST=some
-            >Change-Id: I2c4f545a20a659541c02be16aa9dc440c876a604
-            >
-        Errors:
-            * Changelist description needs BUG field (after first line)
-            * Found line ending with white space in:
-                * src/repohooks/pre-upload.py, line 307
-            * Found lines longer than 80 characters (first 5 shown):
-                * src/repohooks/pre-upload.py, line 335, 85 chars
-  ----------------------------------------------------------------------------
-
   Args:
+    color: terminal.Color object for colorizing output.
+    hook: Hook function that generated these errors.
     project: project name
-    commit: the commit hash the errors belong to
-    commit_desc: a string containing the commit message
     error_list: a list of HookFailure instances
   """
-  _PrintWithIndent(_PROJECT_INFO % project, 0)
-
-  formatted_desc = _FormatCommitDesc(commit_desc)
-  _PrintWithIndent('COMMIT %s:' % commit[:8], 1)
-  _PrintWithIndent('Description:', 2)
-  _PrintWithIndent(formatted_desc, 3)
-  _PrintWithIndent('Errors:', 2)
+  print('[%s] %s: %s' %
+        (color.Color(color.RED, 'FAILED'), project, hook.__name__))
 
   for error in error_list:
-    _PrintWithIndent(_FormatHookFailure(error), 3)
-
-  print('', file=sys.stderr)
+    _PrintWithIndent(error.msg.strip(), 1)
+    if error.items:
+      for item in error.items:
+        _PrintWithIndent(item.strip(), 1)
+    print('', file=sys.stderr)
diff --git a/pre-upload.py b/pre-upload.py
index 1ab1ef8..e5f61e3 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -47,6 +47,7 @@
 from chromite.lib import git
 from chromite.lib import osutils
 from chromite.lib import patch
+from chromite.lib import terminal
 from chromite.licensing import licenses_lib
 
 PRE_SUBMIT = 'pre-submit'
@@ -2001,6 +2002,8 @@
   # hooks assume they are run from the root of the project
   os.chdir(proj_dir)
 
+  color = terminal.Color()
+
   remote_branch = _run_command(['git', 'rev-parse', '--abbrev-ref',
                                 '--symbolic-full-name', '@{u}']).strip()
   if not remote_branch:
@@ -2035,24 +2038,25 @@
   for i, commit in enumerate(commit_list):
     CACHE.clear()
 
-    error_list = []
+    desc = _get_commit_desc(commit)
+    print('[%s %i/%i %s] %s' %
+          (color.Color(color.CYAN, 'COMMIT'), i + 1, commit_count, commit[0:12],
+           desc.splitlines()[0]))
+
     for h, hook in enumerate(hooks):
-      output = ('PRESUBMIT.cfg: [%i/%i]: %s: Running [%i/%i] %s' %
-                (i + 1, commit_count, commit, h + 1, hook_count, hook.__name__))
+      output = ('[%s %i/%i PRESUBMIT.cfg] %s' %
+                (color.Color(color.YELLOW, 'RUNNING'), h + 1, hook_count,
+                 hook.__name__))
       print(output, end='\r')
       sys.stdout.flush()
       hook_error = hook(project, commit)
       print(' ' * len(output), end='\r')
       sys.stdout.flush()
       if hook_error:
-        if isinstance(hook_error, list):
-          error_list.extend(hook_error)
-        else:
-          error_list.append(hook_error)
+        if not isinstance(hook_error, list):
+          hook_error = [hook_error]
+        PrintErrorsForCommit(color, hook, project.name, hook_error)
         error_found = True
-    if error_list:
-      PrintErrorsForCommit(project.name, commit, _get_commit_desc(commit),
-                           error_list)
 
   os.chdir(pwd)
   return error_found
@@ -2075,6 +2079,7 @@
       automatically.
     kwargs: Leave this here for forward-compatibility.
   """
+  start_time = datetime.datetime.now()
   found_error = False
   if not worktree_list:
     worktree_list = [None] * len(project_list)
@@ -2082,13 +2087,20 @@
     if _run_project_hooks(project, proj_dir=worktree):
       found_error = True
 
+  end_time = datetime.datetime.now()
+  color = terminal.Color()
   if found_error:
-    msg = ('Preupload failed due to errors in project(s). HINTS:\n'
+    msg = ('%s: Preupload failed due to above error(s).\n'
            '- To disable some source style checks, and for other hints, see '
-           '<checkout_dir>/src/repohooks/README\n'
-           "- To upload only current project, run 'repo upload .'")
+           '<checkout_dir>/src/repohooks/README.md\n'
+           "- To upload only current project, run 'repo upload .'" %
+           (color.Color(color.RED, 'FATAL'),))
     print(msg, file=sys.stderr)
     sys.exit(1)
+  else:
+    msg = ('[%s] repohooks passed in %s' %
+           (color.Color(color.GREEN, 'PASSED'), end_time - start_time))
+    print(msg)
 
 
 def _identify_project(path):