diff --git a/zcommit.py b/zcommit.py index 7170b59..3df54c1 100755 --- a/zcommit.py +++ b/zcommit.py @@ -5,6 +5,9 @@ import logging import json import os +import posixpath +import re +import requests import subprocess import sys import traceback @@ -16,6 +19,10 @@ ZWRITE = '/usr/bin/zwrite' LOG_FILENAME = 'logs/zcommit.log' +MAX_DIFF_LINES = 50 +MAX_DIFFSTAT_WIDTH = 80 +MIN_DIFFSTAT_GRAPH_WIDTH = 30 + # Set up a specific logger with our desired output level logger = logging.getLogger(__name__) logger.setLevel(logging.DEBUG) @@ -27,6 +34,135 @@ formatter = logging.Formatter(fmt='%(levelname)-8s %(asctime)s %(message)s') handler.setFormatter(formatter) +def zescape(input): + return (input + .replace('@', '@@') + .replace('}', '@(})')) + +def format_rename(old, new): + prefix = posixpath.commonprefix([old, new]) + suffix = posixpath.commonprefix([old[::-1], new[::-1]])[::-1] + return "%s{%s => %s}%s" % ( + prefix, + old[len(prefix):-len(suffix)], + new[len(prefix):-len(suffix)], + suffix, + ) + +def colorize_diff(diff): + diff = zescape(diff) + out = [] + in_diff = False + for l in diff.splitlines(): + if not l: + out.append(l) + continue + color = None + if l[0] == '@': + in_diff = True + color = 'cyan' + elif in_diff: + if l[0] == '+': + color = 'green' + elif l[0] == '-': + color = 'red' + elif l[0] == ' ': + out.append(l) + continue + if color: + out.append('@{@color{%s}%s}' % (color, l)) + continue + in_diff = False + out.append('@b{%s}' % (l,)) + return '\n'.join(out) + +def format_commit(c, commit_url): + info = {'name' : zescape(c['author']['name']), + 'email' : zescape(c['author']['email']), + 'message' : zescape(c['message']), + 'timestamp' : dateutil.parser.parse(c['timestamp']).strftime('%F %T %z'), + 'url' : zescape(c['url'])} + + header = """@{@color(yellow)%(url)s} +Author: %(name)s <%(email)s> +Date: %(timestamp)s + +%(message)s +--- +""" % info + + try: + if not commit_url.startswith('https://api.github.com/'): + raise ValueError('refusing to fetch commit information from '+commit_url) + # Check if the diff is small enough + r = requests.get(commit_url, headers={'Accept': 'application/vnd.github.diff'}) + diff = r.text + if len(diff.splitlines()) <= MAX_DIFF_LINES: + return header + colorize_diff(diff) + + # Otherwise, try to render a diffstat + r = requests.get(commit_url) + commit_details = r.json() + + max_filename_len = 0 + max_changes_len = 0 + actions = [] + for f in commit_details['files']: + filename = f['filename'] + if f['status'] == 'renamed': + filename = format_rename(f['previous_filename'], filename) + max_filename_len = max(max_filename_len, len(filename)) + changes = str(f['changes']) + if 'patch' not in f: + changes = 'Bin' + max_changes_len = max(max_changes_len, len(changes)) + actions.append({ + 'filename': filename, + 'changes': changes, + 'status': f['status'], + 'additions': f['additions'], + 'deletions': f['deletions'], + }) + graph_width = max(MIN_DIFFSTAT_GRAPH_WIDTH, MAX_DIFFSTAT_WIDTH - (max_filename_len + max_changes_len + 3)) + lines = [] + for a in actions: + additions = a['additions'] + deletions = a['deletions'] + total = additions + deletions + if additions + deletions > graph_width: + additions = (additions * graph_width / total) + deletions = (deletions * graph_width / total) + additions = '+' * additions + if additions: + additions = '@{@color(green)%s}' % (additions,) + deletions = '-' * deletions + if deletions: + deletions = '@{@color(red)%s}' % (deletions,) + graph = additions + deletions + if a['changes'] == 'Bin': + graph = a['status'] + lines.append('%-*s | %*s %s' % ( + max_filename_len, a['filename'], + max_changes_len, a['changes'], + graph, + )) + return header + '\n'.join(lines) + except: + logger.exception('failed to fetch commit info from GitHub') + + # If we can't get the diff, fall back on the list of changed files. + actions = [] + if c.get('added'): + actions.extend(' A %s\n' % f for f in c['added']) + if c.get('removed'): + actions.extend(' D %s\n' % f for f in c['removed']) + if c.get('modified'): + actions.extend(' M %s\n' % f for f in c['modified']) + if not actions: + actions.append('Did not add/remove/modify any nonempty files.') + actions = ''.join(actions) + + return header + actions def send_zephyr(sender, klass, instance, zsig, msg): # TODO: spoof the sender @@ -50,7 +186,7 @@ def send_zephyr(sender, klass, instance, zsig, msg): #z.fields = [ zsig, msg ] #z.send() cmd = [ZWRITE, '-c', klass, '-i', instance, - '-s', zsig, '-d', '-x', 'UTF-8', '-m', msg] + '-s', zsig, '-d', '-x', 'UTF-8', '-O', 'auto', '-m', msg] output = subprocess.check_output([p.encode('utf-8') for p in cmd]) class Application(object): @@ -122,30 +258,11 @@ def _default(self, *args, **query): sender = opts.get('sender', 'daemon.zcommit') logger.debug('Set zsig') for c in payload['commits']: + if not c.get('distinct', True): + # Skip commits that have already been pushed on another branch. + continue inst = opts.get('instance', c['id'][:8]) - actions = [] - if c.get('added'): - actions.extend(' A %s\n' % f for f in c['added']) - if c.get('removed'): - actions.extend(' D %s\n' % f for f in c['removed']) - if c.get('modified'): - actions.extend(' M %s\n' % f for f in c['modified']) - if not actions: - actions.append('Did not add/remove/modify any nonempty files.') - info = {'name' : c['author']['name'], - 'email' : c['author']['email'], - 'message' : c['message'], - 'timestamp' : dateutil.parser.parse(c['timestamp']).strftime('%F %T %z'), - 'actions' : ''.join(actions), - 'url' : c['url']} - - msg = """%(url)s -Author: %(name)s <%(email)s> -Date: %(timestamp)s - -%(message)s ---- -%(actions)s""" % info + msg = format_commit(c, payload['repository']['commits_url'].replace('{/sha}', '/'+c['id'])) send_zephyr(sender, opts['class'], inst, zsig, msg) msg = 'Thanks for posting!' else: