Skip to content

Instantly share code, notes, and snippets.

@CashWilliams
Created January 28, 2015 17:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save CashWilliams/5affe151c198424a55a6 to your computer and use it in GitHub Desktop.
Save CashWilliams/5affe151c198424a55a6 to your computer and use it in GitHub Desktop.
Chart from git repo to show Drupal field changes over time
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2013-2015 Sébastien Helleu <flashcode@flashtux.org>
#
# 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