Created
January 28, 2015 17:12
-
-
Save CashWilliams/5affe151c198424a55a6 to your computer and use it in GitHub Desktop.
Chart from git repo to show Drupal field changes over time
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python | |
# -*- coding: utf-8 -*- | |
# | |
# Copyright (C) 2013-2015 Sébastien Helleu <[email protected]> | |
# | |
# This program is free software; you can redistribute it and/or modify | |
# it under the terms of the GNU General Public License as published by | |
# the Free Software Foundation; either version 3 of the License, or | |
# (at your option) any later version. | |
# | |
# This program is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# GNU General Public License for more details. | |
# | |
# You should have received a copy of the GNU General Public License | |
# along with this program. If not, see <http://www.gnu.org/licenses/>. | |
""" | |
Generate statistic charts for a git repository using pygal | |
(http://pygal.org). | |
""" | |
from __future__ import division, print_function | |
import argparse | |
import datetime | |
import os | |
import pygal | |
import re | |
import select | |
import subprocess | |
import sys | |
import traceback | |
VERSION = '1.3-dev' | |
# pylint: disable=too-few-public-methods,too-many-instance-attributes | |
class GitChart(object): | |
"""Generate a git stat chart.""" | |
# List of supported chart styles | |
charts = { | |
'fields': 'Drupal Field Changes', | |
} | |
months = [datetime.date(2001, month, 1).strftime('%b') | |
for month in range(1, 13)] | |
# Pygal style with transparent background and custom colors | |
style = pygal.style.Style( | |
background='transparent', | |
plot_background='transparent', | |
foreground='rgba(0, 0, 0, 0.9)', | |
foreground_light='rgba(0, 0, 0, 0.6)', | |
foreground_dark='rgba(0, 0, 0, 0.2)', | |
opacity_hover='.4', | |
colors=('#9999ff', '#8cedff', '#b6e354', | |
'#feed6c', '#ff9966', '#ff0000', | |
'#ff00cc', '#899ca1', '#bf4646') | |
) | |
# pylint: disable=too-many-arguments | |
def __init__(self, chart_name, title=None, repository='.', output=None, | |
max_diff=20, sort_max=0, js='', in_data=None): | |
self.chart_name = chart_name | |
self.title = title if title is not None else self.charts[chart_name] | |
self.repository = repository | |
self.output = output | |
self.max_diff = max_diff | |
self.sort_max = sort_max | |
self.javascript = js.split(',') | |
self.in_data = in_data | |
def _git_command(self, command1, command2=None): | |
""" | |
Execute one or two piped git commands. | |
Return the output lines as a list. | |
""" | |
if command2: | |
# pipe the two commands and return output | |
proc1 = subprocess.Popen(command1, stdout=subprocess.PIPE, | |
cwd=self.repository) | |
proc2 = subprocess.Popen(command2, stdin=proc1.stdout, | |
stdout=subprocess.PIPE, | |
cwd=self.repository) | |
proc1.stdout.close() | |
return proc2.communicate()[0].decode('utf-8', errors='ignore') \ | |
.strip().split('\n') | |
else: | |
# execute a single git cmd and return output | |
proc = subprocess.Popen(command1, stdout=subprocess.PIPE, | |
cwd=self.repository) | |
return proc.communicate()[0].decode('utf-8', errors='ignore') \ | |
.strip().split('\n') | |
# pylint: disable=too-many-arguments | |
def _generate_bar_chart(self, data, sorted_keys=None, max_keys=0, | |
max_x_labels=0, x_label_rotation=0): | |
"""Generate a bar chart.""" | |
bar_chart = pygal.Bar(style=self.style, show_legend=False, | |
x_label_rotation=x_label_rotation, | |
label_font_size=12, js=self.javascript) | |
bar_chart.title = self.title | |
# sort and keep max entries (if asked) | |
if self.sort_max != 0: | |
sorted_keys = sorted(data, key=data.get, reverse=self.sort_max < 0) | |
keep = -1 * self.sort_max | |
if keep > 0: | |
sorted_keys = sorted_keys[:keep] | |
else: | |
sorted_keys = sorted_keys[keep:] | |
elif not sorted_keys: | |
sorted_keys = sorted(data) | |
if max_keys != 0: | |
sorted_keys = sorted_keys[-1 * max_keys:] | |
bar_chart.x_labels = sorted_keys[:] | |
if max_x_labels > 0 and len(bar_chart.x_labels) > max_x_labels: | |
# reduce number of x labels for readability: keep only one label | |
# on N, starting from the end | |
num = max(2, (len(bar_chart.x_labels) // max_x_labels) * 2) | |
count = 0 | |
for i in range(len(bar_chart.x_labels) - 1, -1, -1): | |
if count % num != 0: | |
bar_chart.x_labels[i] = '' | |
count += 1 | |
bar_chart.add('', [data[k] for k in sorted_keys]) | |
self._render(bar_chart) | |
def _chart_fields(self): | |
"""Generate bar chart with field changes.""" | |
# format of lines in stdout: 2014-10-01 | |
stdout = self._git_command(['git', 'log', '--date=short', | |
'--pretty=format:%ad', '--', | |
'*/*.features.field_base.inc']) | |
commits = {} | |
for line in stdout: | |
date = datetime.datetime.strptime(line, "%Y-%m-%d").date() | |
year, week, weekday = date.isocalendar() | |
commit_week = "{}-{}".format(year, week) | |
commits[commit_week] = commits.get(commit_week, 0) + 1 | |
self._generate_bar_chart(commits, max_keys=self.max_diff, | |
x_label_rotation=45) | |
return True | |
def _render(self, chart): | |
"""Render the chart in a file (or stdout).""" | |
if self.output == '-': | |
# display SVG on stdout | |
print(chart.render()) | |
elif self.output.lower().endswith('.png'): | |
# write PNG in file | |
chart.render_to_png(self.output) | |
else: | |
# write SVG in file | |
chart.render_to_file(self.output) | |
def generate(self): | |
"""Generate a chart, and return True if OK, False if error.""" | |
try: | |
# call method to build chart (name of method is dynamic) | |
return getattr(self, '_chart_' + self.chart_name)() | |
except Exception: | |
traceback.print_exc() | |
return False | |
def main(): | |
"""Main function, entry point.""" | |
# parse command line arguments | |
pygal_config = pygal.Config() | |
parser = argparse.ArgumentParser( | |
formatter_class=argparse.ArgumentDefaultsHelpFormatter, | |
description='Generate statistic charts for a git repository.', | |
epilog='Return value: 0 = success, 1 = error.') | |
parser.add_argument( | |
'-t', '--title', | |
help='override the default chart title') | |
parser.add_argument( | |
'-r', '--repo', | |
default='.', | |
help='directory with git repository') | |
parser.add_argument( | |
'-d', '--max-diff', | |
type=int, default=20, | |
help='max different entries in chart: after this number, an entry is ' | |
'counted in "others" (for charts authors and files_type); max number ' | |
'of days (for chart commits_day); 0=unlimited') | |
parser.add_argument( | |
'-s', '--sort-max', | |
type=int, default=0, | |
help='keep max entries in chart and sort them by value; a negative ' | |
'number will reverse the sort (only for charts: commits_hour_day, ' | |
'commits_day, commits_day_week, commits_month, commits_year, ' | |
'commits_year_month, commits_version); 0=no sort/max') | |
parser.add_argument( | |
'-j', '--js', | |
default=','.join(pygal_config.js), | |
help='comma-separated list of the two javascript files/links used in ' | |
'SVG') | |
parser.add_argument( | |
'chart', | |
metavar='chart', choices=sorted(GitChart.charts), | |
help='{0}: {1}'.format('name of chart, one of', | |
', '.join(sorted(GitChart.charts)))) | |
parser.add_argument( | |
'output', | |
help='output file (svg or png), special value "-" displays SVG ' | |
'content on standard output') | |
parser.add_argument( | |
'-v', '--version', | |
action='version', version=VERSION) | |
if len(sys.argv) == 1: | |
parser.print_help() | |
sys.exit(1) | |
args = parser.parse_args(sys.argv[1:]) | |
# check javascript files | |
js_list = args.js.split(',') | |
if not js_list or len(js_list) != 2 or not js_list[0] or not js_list[1]: | |
sys.exit('ERROR: invalid javascript files') | |
# read data on standard input | |
in_data = '' | |
while True: | |
inr = select.select([sys.stdin], [], [], 0)[0] | |
if not inr: | |
break | |
data = os.read(sys.stdin.fileno(), 4096) | |
if not data: | |
break | |
in_data += data.decode('utf-8') | |
# generate chart | |
chart = GitChart(args.chart, args.title, args.repo, args.output, | |
args.max_diff, args.sort_max, args.js, in_data) | |
if chart.generate(): | |
sys.exit(0) | |
# error | |
print('ERROR: failed to generate chart:', vars(args)) | |
sys.exit(1) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment