aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosh Gao <jmgao@google.com>2016-09-23 18:36:27 -0700
committerJosh Gao <jmgao@google.com>2016-10-06 12:51:51 -0700
commit25abf4bf354f937d46c125776f33478f0b4b71b9 (patch)
treef02950d8e113e36e0f691bdd3cac189d55e4c829
parent54a754b0769176c5ea7892a9a56b3804b06c2f27 (diff)
downloadrepohooks-25abf4bf354f937d46c125776f33478f0b4b71b9.tar.gz
Print status of hooks as they're being run.
Bug: None Test: manually checked output Change-Id: Iecc20728622b00fd392ca9f761295ebfbe94e58f
-rwxr-xr-xpre-upload.py118
-rw-r--r--rh/config.py10
-rw-r--r--rh/terminal.py35
3 files changed, 124 insertions, 39 deletions
diff --git a/pre-upload.py b/pre-upload.py
index ec723e1..61bda1e 100755
--- a/pre-upload.py
+++ b/pre-upload.py
@@ -53,44 +53,91 @@ REPOHOOKS_URL = 'https://android.googlesource.com/platform/tools/repohooks/'
Project = collections.namedtuple('Project', ['name', 'dir', 'remote'])
-def _process_hook_results(project, commit, commit_desc, results):
- """Prints the hook error to stderr with project and commit context
+class Output(object):
+ """Class for reporting hook status."""
+
+ COLOR = rh.terminal.Color()
+ COMMIT = COLOR.color(COLOR.CYAN, 'COMMIT')
+ RUNNING = COLOR.color(COLOR.YELLOW, 'RUNNING')
+ PASSED = COLOR.color(COLOR.GREEN, 'PASSED')
+ FAILED = COLOR.color(COLOR.RED, 'FAILED')
+
+ def __init__(self, project_name, num_hooks):
+ """Create a new Output object for a specified project.
+
+ Args:
+ project_name: name of project.
+ num_hooks: number of hooks to be run.
+ """
+ self.project_name = project_name
+ self.num_hooks = num_hooks
+ self.hook_index = 0
+ self.success = True
+
+ def commit_start(self, commit, commit_summary):
+ """Emit status for new commit.
+
+ Args:
+ commit: commit hash.
+ commit_summary: commit summary.
+ """
+ status_line = '[%s %s] %s' % (self.COMMIT, commit[0:12], commit_summary)
+ rh.terminal.print_status_line(status_line, print_newline=True)
+ self.hook_index = 1
+
+ def hook_start(self, hook_name):
+ """Emit status before the start of a hook.
+
+ Args:
+ hook_name: name of the hook.
+ """
+ status_line = '[%s %d/%d] %s' % (self.RUNNING, self.hook_index,
+ self.num_hooks, hook_name)
+ self.hook_index += 1
+ rh.terminal.print_status_line(status_line)
+
+ def hook_error(self, hook_name, error):
+ """Print an error.
+
+ Args:
+ hook_name: name of the hook.
+ error: error string.
+ """
+ status_line = '[%s] %s' % (self.FAILED, hook_name)
+ rh.terminal.print_status_line(status_line, print_newline=True)
+ print(error, file=sys.stderr)
+ self.success = False
+
+ def finish(self):
+ """Print repohook summary."""
+ status_line = '[%s] repohooks for %s %s' % (
+ self.PASSED if self.success else self.FAILED,
+ self.project_name,
+ 'passed' if self.success else 'failed')
+ rh.terminal.print_status_line(status_line, print_newline=True)
+
+
+def _process_hook_results(results):
+ """Returns an error string if an error occurred.
Args:
- project: The project name.
- commit: The commit hash the errors belong to.
- commit_desc: A string containing the commit message.
- results: A list of HookResult objects.
+ results: A list of HookResult objects, or None.
Returns:
- False if any errors were found, else True.
+ error output if an error occurred, otherwise None
"""
- color = rh.terminal.Color()
- def _print_banner():
- print('%s: %s: hooks failed' %
- (color.color(color.RED, 'ERROR'), project),
- file=sys.stderr)
-
- commit_summary = commit_desc.splitlines()[0]
- print('COMMIT: %s (%s)' % (commit[0:12], commit_summary),
- file=sys.stderr)
+ if not results:
+ return None
- ret = True
+ ret = ''
for result in results:
if result:
- if ret:
- _print_banner()
- ret = False
-
- print('%s: %s' % (color.color(color.CYAN, 'HOOK'), result.hook),
- file=sys.stderr)
if result.files:
- print(' FILES: %s' % (result.files,), file=sys.stderr)
+ ret += ' FILES: %s' % (result.files,)
lines = result.error.splitlines()
- print('\n'.join(' %s' % (x,) for x in lines), file=sys.stderr)
- print('', file=sys.stderr)
+ ret += '\n'.join(' %s' % (x,) for x in lines)
- return ret
+ return ret or None
def _get_project_hooks():
@@ -170,12 +217,14 @@ def _run_project_hooks(project_name, proj_dir=None,
'REPO_REMOTE': remote,
})
+ output = Output(project_name, len(hooks))
project = Project(name=project_name, dir=proj_dir, remote=remote)
if not commit_list:
commit_list = rh.git.get_commits()
ret = True
+
for commit in commit_list:
# Mix in some settings for our hooks.
os.environ['PREUPLOAD_COMMIT'] = commit
@@ -183,15 +232,18 @@ def _run_project_hooks(project_name, proj_dir=None,
desc = rh.git.get_commit_desc(commit)
os.environ['PREUPLOAD_COMMIT_MESSAGE'] = desc
- results = []
- for hook in hooks:
+ commit_summary = desc.split('\n', 1)[0]
+ output.commit_start(commit=commit, commit_summary=commit_summary)
+
+ for name, hook in hooks:
+ output.hook_start(name)
hook_results = hook(project, commit, desc, diff)
- if hook_results:
- results.extend(hook_results)
- if results:
- if not _process_hook_results(project.name, commit, desc, results):
+ error = _process_hook_results(hook_results)
+ if error:
ret = False
+ output.hook_error(name, error)
+ output.finish()
os.chdir(pwd)
return ret
diff --git a/rh/config.py b/rh/config.py
index 1c2a7d6..05d7cb2 100644
--- a/rh/config.py
+++ b/rh/config.py
@@ -153,20 +153,20 @@ class PreSubmitConfig(object):
return dict(self.config.items(self.TOOL_PATHS_SECTION, ()))
def callable_hooks(self):
- """Yield a callback for each hook to be executed (custom & builtin)."""
+ """Yield a name and callback for each hook to be executed."""
for hook in self.custom_hooks:
options = rh.hooks.HookOptions(hook,
self.custom_hook(hook),
self.tool_paths)
- yield functools.partial(rh.hooks.check_custom,
- options=options)
+ yield (hook, functools.partial(rh.hooks.check_custom,
+ options=options))
for hook in self.builtin_hooks:
options = rh.hooks.HookOptions(hook,
self.builtin_hook_option(hook),
self.tool_paths)
- yield functools.partial(rh.hooks.BUILTIN_HOOKS[hook],
- options=options)
+ yield (hook, functools.partial(rh.hooks.BUILTIN_HOOKS[hook],
+ options=options))
def _validate(self):
"""Run consistency checks on the config settings."""
diff --git a/rh/terminal.py b/rh/terminal.py
index 0fdf5a4..49da992 100644
--- a/rh/terminal.py
+++ b/rh/terminal.py
@@ -101,5 +101,38 @@ class Color(object):
self._enabled = not rh.shell.boolean_shell_value(
os.environ['NOCOLOR'], False)
else:
- self._enabled = os.isatty(sys.stdin.fileno())
+ self._enabled = is_tty(sys.stderr)
return self._enabled
+
+
+def is_tty(fh):
+ """Returns whether the specified file handle is a TTY.
+
+ Args:
+ fh: File handle to check.
+
+ Returns:
+ True if |fh| is a TTY
+ """
+ try:
+ return os.isatty(fh.fileno())
+ except IOError:
+ return False
+
+
+def print_status_line(line, print_newline=False):
+ """Clears the current terminal line, and prints |line|.
+
+ Args:
+ line: String to print.
+ print_newline: Print a newline at the end, if sys.stderr is a TTY.
+ """
+ if is_tty(sys.stderr):
+ output = '\r' + line + '\x1B[K'
+ if print_newline:
+ output += '\n'
+ else:
+ output = line + '\n'
+
+ sys.stderr.write(output)
+ sys.stderr.flush()