Skip to content

Instantly share code, notes, and snippets.

@adzhurinskij
Last active May 22, 2019 12:32
Show Gist options
  • Save adzhurinskij/540ff059356dcc85ebcba302e8da602c to your computer and use it in GitHub Desktop.
Save adzhurinskij/540ff059356dcc85ebcba302e8da602c to your computer and use it in GitHub Desktop.
Send GIT diff to mail in colorize html format
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
import os
import re
import argparse
from git import Repo
from jinja2 import Environment
from mailer import Message
from mailer import Mailer
TEMPLATE = u'''
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Skeleton CSS -->
<style media="all" type="text/css">.column,.columns,.container,.u-full-width{width:100%;box-sizing:border-box}h1,h2,h3{letter-spacing:-.1rem}body,h6{line-height:1.6}.container{position:relative;max-width:960px;margin:0 auto;padding:0 20px}ol,p,ul{margin-top:0}.column,.columns{float:left}@media (min-width:400px){.container{width:85%;padding:0}}html{font-size:62.5%}body{font-size:1.5em;font-weight:400;font-family:Raleway,HelveticaNeue,"Helvetica Neue",Helvetica,Arial,sans-serif;color:#222}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:2rem;font-weight:300}h1{font-size:4rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:3rem;line-height:1.3}h4{font-size:2.4rem;line-height:1.35;letter-spacing:-.08rem}h5{font-size:1.8rem;line-height:1.5;letter-spacing:-.05rem}h6{font-size:1.5rem;letter-spacing:0}@media (min-width:550px){.container{width:80%}.column,.columns{margin-left:4%}.column:first-child,.columns:first-child{margin-left:0}.one.column,.one.columns{width:4.66666666667%}.two.columns{width:13.3333333333%}.three.columns{width:22%}.four.columns{width:30.6666666667%}.five.columns{width:39.3333333333%}.six.columns{width:48%}.seven.columns{width:56.6666666667%}.eight.columns{width:65.3333333333%}.nine.columns{width:74%}.ten.columns{width:82.6666666667%}.eleven.columns{width:91.3333333333%}.twelve.columns{width:100%;margin-left:0}.one-third.column{width:30.6666666667%}.two-thirds.column{width:65.3333333333%}.one-half.column{width:48%}.offset-by-one.column,.offset-by-one.columns{margin-left:8.66666666667%}.offset-by-two.column,.offset-by-two.columns{margin-left:17.3333333333%}.offset-by-three.column,.offset-by-three.columns{margin-left:26%}.offset-by-four.column,.offset-by-four.columns{margin-left:34.6666666667%}.offset-by-five.column,.offset-by-five.columns{margin-left:43.3333333333%}.offset-by-six.column,.offset-by-six.columns{margin-left:52%}.offset-by-seven.column,.offset-by-seven.columns{margin-left:60.6666666667%}.offset-by-eight.column,.offset-by-eight.columns{margin-left:69.3333333333%}.offset-by-nine.column,.offset-by-nine.columns{margin-left:78%}.offset-by-ten.column,.offset-by-ten.columns{margin-left:86.6666666667%}.offset-by-eleven.column,.offset-by-eleven.columns{margin-left:95.3333333333%}.offset-by-one-third.column,.offset-by-one-third.columns{margin-left:34.6666666667%}.offset-by-two-thirds.column,.offset-by-two-thirds.columns{margin-left:69.3333333333%}.offset-by-one-half.column,.offset-by-one-half.columns{margin-left:52%}h1{font-size:5rem}h2{font-size:4.2rem}h3{font-size:3.6rem}h4{font-size:3rem}h5{font-size:2.4rem}h6{font-size:1.5rem}}a{color:#1EAEDB}a:hover{color:#0FA0CE}.button,button,input[type=submit],input[type=reset],input[type=button]{display:inline-block;height:38px;padding:0 30px;color:#555;text-align:center;font-size:11px;font-weight:600;line-height:38px;letter-spacing:.1rem;text-transform:uppercase;text-decoration:none;white-space:nowrap;background-color:transparent;border-radius:4px;border:1px solid #bbb;cursor:pointer;box-sizing:border-box}.button:focus,.button:hover,button:focus,button:hover,input[type=submit]:focus,input[type=submit]:hover,input[type=reset]:focus,input[type=reset]:hover,input[type=button]:focus,input[type=button]:hover{color:#333;border-color:#888;outline:0}.button.button-primary,button.button-primary,input[type=submit].button-primary,input[type=reset].button-primary,input[type=button].button-primary{color:#FFF;background-color:#33C3F0;border-color:#33C3F0}.button.button-primary:focus,.button.button-primary:hover,button.button-primary:focus,button.button-primary:hover,input[type=submit].button-primary:focus,input[type=submit].button-primary:hover,input[type=reset].button-primary:focus,input[type=reset].button-primary:hover,input[type=button].button-primary:focus,input[type=button].button-primary:hover{color:#FFF;background-color:#1EAEDB;border-color:#1EAEDB}input[type=tel],input[type=url],input[type=password],input[type=email],input[type=number],input[type=search],input[type=text],select,textarea{height:38px;padding:6px 10px;background-color:#fff;border:1px solid #D1D1D1;border-radius:4px;box-shadow:none;box-sizing:border-box}input[type=tel],input[type=url],input[type=password],input[type=email],input[type=number],input[type=search],input[type=text],textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none}textarea{min-height:65px;padding-top:6px;padding-bottom:6px}input[type=tel]:focus,input[type=url]:focus,input[type=password]:focus,input[type=email]:focus,input[type=number]:focus,input[type=search]:focus,input[type=text]:focus,select:focus,textarea:focus{border:1px solid #33C3F0;outline:0}label,legend{display:block;margin-bottom:.5rem;font-weight:600}fieldset{padding:0;border-width:0}input[type=checkbox],input[type=radio]{display:inline}label>.label-body{display:inline-block;margin-left:.5rem;font-weight:400}ul{list-style:circle inside}ol{list-style:decimal inside}ol,ul{padding-left:0}ol ol,ol ul,ul ol,ul ul{margin:1.5rem 0 1.5rem 3rem;font-size:90%}.button,button,li{margin-bottom:1rem}code{padding:.2rem .5rem;margin:0 .2rem;font-size:90%;white-space:nowrap;background:#F1F1F1;border:1px solid #E1E1E1;border-radius:4px}pre>code{display:block;padding:1rem 1.5rem;white-space:pre}td,th{padding:12px 15px;text-align:left;border-bottom:1px solid #E1E1E1}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}.u-max-full-width{max-width:100%;box-sizing:border-box}.u-pull-right{float:right}.u-pull-left{float:left}hr{margin-top:3rem;margin-bottom:3.5rem;border-width:0;border-top:1px solid #E1E1E1}.container:after,.row:after,.u-cf{content:"";display:table;clear:both}</style>
<style>
span.diffcommand { color: teal; }
span.removed { color: red; }
span.inserted { color: green; }
span.linenumber { color: purple; }
</style>
</head>
<body>
<div class="container">
<div class="row">
<h4>Git Status</h4>
<pre><code>{{ status | e }}</code></pre>
</div>
{% for item in changes %}
<div class="row">
<h5>{{ item }}</h5>
<pre><code>{{ changes[item] }}</code></pre>
</div>
{% endfor %}
</div>
</body>
</html>
'''
def quote_html(s):
'''Quote html special chars and replace space with nbsp'''
def repl_quote_html(m):
tokens = []
quote_dict = {
' ': '&nbsp;',
'<': '&lt;',
'>': '&gt;',
'&': '&amp;',
'"': '&quot;',
}
for c in m.group(0):
tokens.append(quote_dict[c])
return ''.join(tokens)
return re.sub('[ &<>"]', repl_quote_html, s)
def diff2html(diff):
lines = []
for line in diff.split('\n'):
if line.startswith('+'):
lines.append('<span class="inserted">{}</span><br>'.format(quote_html(line)))
elif line.startswith('-'):
lines.append('<span class="removed">{}</span><br>'.format(quote_html(line)))
elif line.startswith('diff'):
lines.append('<span class="diffcommand">{}</span><br>'.format(quote_html(line)))
elif line.startswith('index '):
lines.append('<span class="diffcommand">{}</span><br>'.format(quote_html(line)))
elif line.startswith('@@ -'):
lines.append('<span class="linenumber">{}</span><br>'.format(quote_html(line)))
else:
lines.append('<span>{}</span><br>'.format(quote_html(line)))
return ''.join(lines)
def argv_parse():
parser = argparse.ArgumentParser(
add_help=True,
description='Send git diff to mail')
parser.add_argument('--subject', required=False, default='Git changes', help='Email subject')
parser.add_argument('--from', dest='emailfrom', required=True, help='Send from')
parser.add_argument('repository', type=str, help='Path to git repository')
parser.add_argument('to', type=str, nargs='+', help='Send to')
return parser.parse_args()
def main():
options = argv_parse()
repo = Repo(os.path.realpath(options.repository))
changes = {}
for diff in repo.index.diff(None):
if repo.git.diff(diff.a_path):
changes[diff.a_path] = diff2html(repo.git.diff(diff.a_path))
if len(changes) == 0:
print('No changes detected')
return
body = Environment().from_string(TEMPLATE).render(
status=repo.git.status(),
changes=changes,
).encode('utf-8')
msg = Message(
From=options.emailfrom,
To=options.to,
charset='utf-8'
)
msg.Subject = options.subject
msg.Html = body
sender = Mailer('localhost')
sender.send(msg)
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment