From d31374a93fde4fec10bde16318ca4181700de21f Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Mon, 27 Jan 2020 23:57:02 -0500 Subject: [PATCH 1/5] Render diffs and diffstats when possible --- zcommit.py | 119 ++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 96 insertions(+), 23 deletions(-) diff --git a/zcommit.py b/zcommit.py index 7170b59..e3c712c 100755 --- a/zcommit.py +++ b/zcommit.py @@ -5,6 +5,8 @@ import logging import json import os +import posixpath +import requests import subprocess import sys import traceback @@ -16,6 +18,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 +33,95 @@ formatter = logging.Formatter(fmt='%(levelname)-8s %(asctime)s %(message)s') handler.setFormatter(formatter) +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 format_commit(c, commit_url): + info = {'name' : c['author']['name'], + 'email' : c['author']['email'], + 'message' : c['message'], + 'timestamp' : dateutil.parser.parse(c['timestamp']).strftime('%F %T %z'), + 'url' : c['url']} + + header = """%(url)s +Author: %(name)s <%(email)s> +Date: %(timestamp)s + +%(message)s +--- +""" % info + + try: + # 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 + 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) + 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 @@ -123,29 +218,7 @@ def _default(self, *args, **query): logger.debug('Set zsig') for c in payload['commits']: 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: From b65745bb6312d4bd5db58af497342f9c5ae57d93 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Tue, 28 Jan 2020 00:44:53 -0500 Subject: [PATCH 2/5] Colorize diff and diffstat --- zcommit.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 46 insertions(+), 7 deletions(-) diff --git a/zcommit.py b/zcommit.py index e3c712c..410324b 100755 --- a/zcommit.py +++ b/zcommit.py @@ -6,6 +6,7 @@ import json import os import posixpath +import re import requests import subprocess import sys @@ -33,6 +34,11 @@ 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] @@ -43,14 +49,41 @@ def format_rename(old, new): 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' : c['author']['name'], - 'email' : c['author']['email'], - 'message' : c['message'], + 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' : c['url']} + 'url' : zescape(c['url'])} - header = """%(url)s + header = """@{@color(yellow)%(url)s} Author: %(name)s <%(email)s> Date: %(timestamp)s @@ -63,7 +96,7 @@ def format_commit(c, commit_url): r = requests.get(commit_url, headers={'Accept': 'application/vnd.github.diff'}) diff = r.text if len(diff.splitlines()) <= MAX_DIFF_LINES: - return header + diff + return header + colorize_diff(diff) # Otherwise, try to render a diffstat r = requests.get(commit_url) @@ -97,7 +130,13 @@ def format_commit(c, commit_url): if additions + deletions > graph_width: additions = (additions * graph_width / total) deletions = (deletions * graph_width / total) - graph = ('+' * additions) + ('-' * deletions) + 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' % ( From a23ad2e0406abc49ec2cc69a67cb925e094fd2c7 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Wed, 29 Jan 2020 01:26:35 -0500 Subject: [PATCH 3/5] Protect against hostile webhook invocations --- zcommit.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zcommit.py b/zcommit.py index 410324b..37110ae 100755 --- a/zcommit.py +++ b/zcommit.py @@ -92,6 +92,8 @@ def format_commit(c, commit_url): """ % 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 From 85db4cd7e4685ead695c251040fa3ddbd5b07437 Mon Sep 17 00:00:00 2001 From: Quentin Smith Date: Sat, 14 Mar 2020 02:39:39 -0400 Subject: [PATCH 4/5] Skip commits that have already been pushed on another branch. --- zcommit.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/zcommit.py b/zcommit.py index 37110ae..2bf5c7a 100755 --- a/zcommit.py +++ b/zcommit.py @@ -258,6 +258,9 @@ 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]) msg = format_commit(c, payload['repository']['commits_url'].replace('{/sha}', '/'+c['id'])) send_zephyr(sender, opts['class'], inst, zsig, msg) From 1a72092d99d42a42f969c8f02024bb00af006cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20R=2E=20Sede=C3=B1o?= Date: Mon, 28 Sep 2020 22:49:58 -0400 Subject: [PATCH 5/5] use opcode auto --- zcommit.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zcommit.py b/zcommit.py index 2bf5c7a..3df54c1 100755 --- a/zcommit.py +++ b/zcommit.py @@ -186,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):