#!/usr/bin/python import cherrypy from flup.server.fcgi import WSGIServer import logging import json import os import posixpath import re import requests import subprocess import sys import traceback import dateutil.parser import zephyr HERE = os.path.abspath(os.path.dirname(__file__)) ZWRITE = os.path.join(HERE, 'bin', 'zsend') 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) # Add the log message handler to the logger handler = logging.FileHandler(LOG_FILENAME) logger.addHandler(handler) 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 logger.info("""About to send zephyr: \"\"\" sender: %(sender)s class: %(klass)s instance: %(instance)s zsig: %(zsig)s msg: %(msg)s \"\"\" """ % {'sender' : sender, 'klass' : klass, 'instance' : instance, 'zsig' : zsig, 'msg' : msg}) #z = zephyr.ZNotice() #z.sender = sender #z.cls = klass #z.instance = instance #z.fields = [ zsig, msg ] #z.send() cmd = [ZWRITE, '-c', klass, '-i', instance, '-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): @cherrypy.expose def index(self): logger.debug('Hello world app reached') return """
Welcome to zcommit.
zcommit allows you to send zephyr notifications by sending an HTTP POST request to a URL. Currently zcommit supports POST-backs from github. If you would like it to support another form of POST-back, please let us know (zcommit@mit.edu).