Added support for gravatars in the changes, blame and timeline modules.

References to gravatar images are generated with HTML and XML outputs only
as these are the only formats where referencing gravatars makes sense
right now. The HTMLEmbedded format, for example, does not link to any
gravatars as that format prohibits the use of external links.

To accommodate the new images; the width of the generated HTML page has
been slightly increased. However, the HTML page should still fit on a
1280 display.
This commit is contained in:
Adam Waldenberg 2013-07-20 11:45:11 +02:00
parent d07da3dc69
commit 35852f9859
6 changed files with 128 additions and 44 deletions

View File

@ -25,6 +25,7 @@ from changes import FileDiff
import comment
import filtering
import format
import gravatar
import interval
import missing
import multiprocessing
@ -61,16 +62,16 @@ class BlameThread(threading.Thread):
for j in git_blame_r.readlines():
j = j.decode("utf-8", "replace")
if Blame.is_blame_line(j):
author = Blame.get_author(j)
author_mail = Blame.get_author_mail(j)
content = Blame.get_content(j)
__blame_lock__.acquire() # Global lock used to protect calls from here...
if self.blames.get((author, self.filename), None) == None:
self.blames[(author, self.filename)] = BlameEntry()
if self.blames.get((author_mail, self.filename), None) == None:
self.blames[(author_mail, self.filename)] = BlameEntry()
(comments, is_inside_comment) = comment.handle_comment_block(is_inside_comment, self.extension, content)
self.blames[(author, self.filename)].comments += comments
self.blames[(author, self.filename)].rows += 1
self.blames[(author_mail, self.filename)].comments += comments
self.blames[(author_mail, self.filename)].rows += 1
__blame_lock__.release() # ...to here.
git_blame_r.close()
@ -92,7 +93,7 @@ class Blame:
if FileDiff.is_valid_extension(row) and not filtering.set_filtered(FileDiff.get_filename(row)):
if not missing.add(row):
blame_string = "git blame -w {0} ".format("-C -C -M" if hard else "") + \
blame_string = "git blame -e -w {0} ".format("-C -C -M" if hard else "") + \
interval.get_since() + interval.get_ref() + " -- \"" + row + "\""
thread = BlameThread(blame_string, FileDiff.get_extension(row), self.blames, row.strip())
thread.daemon = True
@ -117,9 +118,9 @@ class Blame:
return string.find(" (") != -1
@staticmethod
def get_author(string):
author = re.search(" \((.*?)\d\d\d\d-\d\d-\d\d", string)
return author.group(1).strip()
def get_author_mail(string):
author_mail = re.search(" \((.*?)\d\d\d\d-\d\d-\d\d", string)
return author_mail.group(1).strip().lstrip("<").rstrip(">")
@staticmethod
def get_content(string):
@ -150,8 +151,9 @@ BLAME_INFO_TEXT = N_("Below are the number of rows from each author that have su
"intact in the current revision")
class BlameOutput(Outputable):
def __init__(self, hard):
def __init__(self, changes, hard):
self.hard = hard
self.changes = changes
Outputable.__init__(self)
def output_html(self):
@ -171,14 +173,20 @@ class BlameOutput(Outputable):
for i, entry in enumerate(blames):
work_percentage = str("{0:.2f}".format(100.0 * entry[1].rows / total_blames))
authorname = self.changes.get_authorname_from_email(entry[0])
blame_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">")
blame_xml += "<td>" + entry[0] + "</td>"
if format.get_selected() == "html":
blame_xml += "<td><img src=\"{0}\"/>{1}</td>".format(gravatar.get_url(entry[0]), authorname)
else:
blame_xml += "<td>" + authorname + "</td>"
blame_xml += "<td>" + str(entry[1].rows) + "</td>"
blame_xml += "<td>" + "{0:.2f}".format(100.0 * entry[1].comments / entry[1].rows) + "</td>"
blame_xml += "<td style=\"display: none\">" + work_percentage + "</td>"
blame_xml += "</tr>"
chart_data += "{{label: \"{0}\", data: {1}}}".format(entry[0], work_percentage)
chart_data += "{{label: \"{0}\", data: {1}}}".format(authorname, work_percentage)
if blames[-1] != entry:
chart_data += ", "
@ -214,7 +222,8 @@ class BlameOutput(Outputable):
print(textwrap.fill(_(BLAME_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n")
terminal.printb(_("Author").ljust(21) + _("Rows").rjust(10) + _("% in comments").rjust(20))
for i in sorted(__blame__.get_summed_blames().items()):
print(i[0].ljust(20)[0:20], end=" ")
authorname = self.changes.get_authorname_from_email(i[0])
print(authorname.ljust(20)[0:20], end=" ")
print(str(i[1].rows).rjust(10), end=" ")
print("{0:.2f}".format(100.0 * i[1].comments / i[1].rows).rjust(19))
@ -225,10 +234,12 @@ class BlameOutput(Outputable):
blame_xml = ""
for i in sorted(__blame__.get_summed_blames().items()):
name_xml = "\t\t\t\t<name>" + i[0] + "</name>\n"
authorname = self.changes.get_authorname_from_email(i[0])
name_xml = "\t\t\t\t<name>" + authorname + "</name>\n"
gravatar_xml = "\t\t\t\t<gravatar>" + gravatar.get_url(i[0]) + "</gravatar>\n"
rows_xml = "\t\t\t\t<rows>" + str(i[1].rows) + "</rows>\n"
percentage_in_comments_xml = ("\t\t\t\t<percentage-in-comments>" + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) +
"</percentage-in-comments>\n")
blame_xml += "\t\t\t<author>\n" + name_xml + rows_xml + percentage_in_comments_xml + "\t\t\t</author>\n"
blame_xml += "\t\t\t<author>\n" + name_xml + gravatar_xml + rows_xml + percentage_in_comments_xml + "\t\t\t</author>\n"
print("\t<blame>\n" + message_xml + "\t\t<authors>\n" + blame_xml + "\t\t</authors>\n\t</blame>")

View File

@ -23,6 +23,8 @@ from localization import N_
from outputable import Outputable
import extensions
import filtering
import format
import gravatar
import interval
import os
import subprocess
@ -66,10 +68,11 @@ class Commit:
self.filediffs = []
commit_line = string.split("|")
if commit_line.__len__() == 3:
if commit_line.__len__() == 4:
self.date = commit_line[0]
self.sha = commit_line[1]
self.author = commit_line[2].strip()
self.email = commit_line[3].strip()
def add_filediff(self, filediff):
self.filediffs.append(filediff)
@ -79,7 +82,7 @@ class Commit:
@staticmethod
def is_commit_line(string):
return string.split("|").__len__() == 3
return string.split("|").__len__() == 4
class AuthorInfo:
insertions = 0
@ -87,9 +90,12 @@ class AuthorInfo:
commits = 0
class Changes:
authors = {}
authors_dateinfo = {}
def __init__(self, hard):
self.commits = []
git_log_r = subprocess.Popen("git log --pretty=\"%cd|%H|%aN\" --stat=100000,8192 --no-merges -w " +
git_log_r = subprocess.Popen("git log --pretty=\"%cd|%H|%aN|%aE\" --stat=100000,8192 --no-merges -w " +
interval.get_since() + interval.get_until() +
"{0} --date=short".format("-C -C -M" if hard else ""),
shell=True, bufsize=1, stdout=subprocess.PIPE).stdout
@ -136,18 +142,28 @@ class Changes:
authors[key].deletions += j.deletions
def get_authorinfo_list(self):
authors = {}
for i in self.commits:
Changes.__modify_authorinfo__(authors, i.author, i)
if not self.authors:
for i in self.commits:
Changes.__modify_authorinfo__(self.authors, (i.author, i.email), i)
return authors
return self.authors
def get_authordateinfo_list(self):
authors = {}
for i in self.commits:
Changes.__modify_authorinfo__(authors, (i.date, i.author), i)
if not self.authors_dateinfo:
for i in self.commits:
Changes.__modify_authorinfo__(self.authors_dateinfo, (i.date, i.author, i.email), i)
return authors
return self.authors_dateinfo
def get_authorname_from_email(self, email):
if not self.authors:
get_authorinfo_list(self)
for author in self.authors:
if author[1] == email:
return author[0]
return "Unknown"
__changes__ = None
@ -187,13 +203,18 @@ class ChangesOutput(Outputable):
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
changes_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">")
changes_xml += "<td>" + entry + "</td>"
if format.get_selected() == "html":
changes_xml += "<td><img src=\"{0}\"/>{1}</td>".format(gravatar.get_url(entry[1]), entry[0])
else:
changes_xml += "<td>" + entry[0] + "</td>"
changes_xml += "<td>" + str(authorinfo.commits) + "</td>"
changes_xml += "<td>" + str(authorinfo.insertions) + "</td>"
changes_xml += "<td>" + str(authorinfo.deletions) + "</td>"
changes_xml += "<td>" + "{0:.2f}".format(percentage) + "</td>"
changes_xml += "</tr>"
chart_data += "{{label: \"{0}\", data: {1}}}".format(entry, "{0:.2f}".format(percentage))
chart_data += "{{label: \"{0}\", data: {1}}}".format(entry[0], "{0:.2f}".format(percentage))
if sorted(authorinfo_list)[-1] != entry:
chart_data += ", "
@ -239,7 +260,7 @@ class ChangesOutput(Outputable):
authorinfo = authorinfo_list.get(i)
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
print(i.ljust(20)[0:20], end=" ")
print(i[0].ljust(20)[0:20], end=" ")
print(str(authorinfo.commits).rjust(13), end=" ")
print(str(authorinfo.insertions).rjust(13), end=" ")
print(str(authorinfo.deletions).rjust(14), end=" ")
@ -262,14 +283,14 @@ class ChangesOutput(Outputable):
for i in sorted(authorinfo_list):
authorinfo = authorinfo_list.get(i)
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
name_xml = "\t\t\t\t<name>" + i + "</name>\n"
name_xml = "\t\t\t\t<name>" + i[0] + "</name>\n"
gravatar_xml = "\t\t\t\t<gravatar>" + gravatar.get_url(i[1]) + "</gravatar>\n"
commits_xml = "\t\t\t\t<commits>" + str(authorinfo.commits) + "</commits>\n"
insertions_xml = "\t\t\t\t<insertions>" + str(authorinfo.insertions) + "</insertions>\n"
deletions_xml = "\t\t\t\t<deletions>" + str(authorinfo.deletions) + "</deletions>\n"
percentage_xml = "\t\t\t\t<percentage-of-changes>" + "{0:.2f}".format(percentage) + "</percentage-of-changes>\n"
changes_xml += ("\t\t\t<author>\n" + name_xml + commits_xml + insertions_xml +
changes_xml += ("\t\t\t<author>\n" + name_xml + gravatar_xml + commits_xml + insertions_xml +
deletions_xml + percentage_xml + "\t\t\t</author>\n")
print("\t<changes>\n" + message_xml + "\t\t<authors>\n" + changes_xml + "\t\t</authors>\n\t</changes>")

View File

@ -78,7 +78,7 @@ class Runner:
outputable.output(changes.ChangesOutput(self.hard))
if changes.get(self.hard).get_commits():
outputable.output(blame.BlameOutput(self.hard))
outputable.output(blame.BlameOutput(changes.get(self.hard), self.hard))
if self.timeline:
outputable.output(timeline.Timeline(changes.get(self.hard), self.useweeks))

34
gitinspector/gravatar.py Normal file
View File

@ -0,0 +1,34 @@
# coding: utf-8
#
# Copyright © 2013 Ejwa Software. All rights reserved.
#
# This file is part of gitinspector.
#
# gitinspector 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.
#
# gitinspector 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 gitinspector. If not, see <http://www.gnu.org/licenses/>.
import format
import hashlib
import urllib
def get_url(email):
md5hash = hashlib.md5(email.lower().strip()).hexdigest()
base_url = "http://www.gravatar.com/avatar/" + md5hash
params = None
if format.get_selected() == "html":
params = {"default": "identicon", "size": 20}
elif format.get_selected() == "xml":
params = {"default": "identicon"}
return base_url + "?" + urllib.urlencode(params)

View File

@ -128,7 +128,7 @@
}}
body > div {{
margin: 0 auto;
width: 50em;
width: 58em;
}}
div.box {{
border: 4px solid #ddd;
@ -158,7 +158,7 @@
}}
table.git {{
font-size: small;
width: 60%;
width: 65%;
padding-right: 5px;
}}
table.full {{
@ -179,9 +179,9 @@
-moz-border-radius: 0px 0px 8px 8px;
text-align: center;
}}
table.git td {{
padding: 0.4em;
height: 1em;
table.git td, table.git th, table#timeline td, table#timeline th {{
padding: 0.35em;
height: 2em;
}}
table.git td div.insert {{
background-color: #7a7;
@ -204,7 +204,7 @@
top: 5px;
bottom: 5px;
right: 0px;
width: 40%;
width: 35%;
font-size: x-small;
height: 210px;
}}
@ -227,6 +227,15 @@
border: 1px solid #bbb;
cursor: hand;
}}
td img {{
border-radius: 3px 3px 3px 3px;
-moz-border-radius: 3px 3px 3px 3px;
vertical-align: middle;
margin-right: 5px;
width: 20px;
height: 20px;
opacity: 0.85;
}}
</style>
</head>
<body>

View File

@ -22,6 +22,8 @@ from __future__ import unicode_literals
from localization import N_
from outputable import Outputable
import datetime
import format
import gravatar
import terminal
import textwrap
@ -37,9 +39,9 @@ class TimelineData:
if useweeks:
yearweek = datetime.date(int(i[0][0][0:4]), int(i[0][0][5:7]), int(i[0][0][8:10])).isocalendar()
key = (i[0][1], str(yearweek[0]) + "W" + "{0:02d}".format(yearweek[1]))
key = ((i[0][1], i[0][2]), str(yearweek[0]) + "W" + "{0:02d}".format(yearweek[1]))
else:
key = (i[0][1], i[0][0][0:7])
key = ((i[0][1], i[0][2]), i[0][0][0:7])
if self.entries.get(key, None) == None:
self.entries[key] = i[1]
@ -117,7 +119,7 @@ def __output_row__text__(timeline_data, periods, names):
for name in names:
if timeline_data.is_author_in_periods(periods, name):
print(name.ljust(20)[0:20], end=" ")
print(name[0].ljust(20)[0:20], end=" ")
for period in periods:
multiplier = timeline_data.get_multiplier(period, 9)
signs = timeline_data.get_author_signs_in_period(name, period, multiplier)
@ -146,7 +148,13 @@ def __output_row__html__(timeline_data, periods, names):
for name in names:
if timeline_data.is_author_in_periods(periods, name):
timeline_xml += "<tr" + (" class=\"odd\">" if i % 2 == 1 else ">")
timeline_xml += "<td>" + name + "</td>"
#timeline_xml += "<td>" + name[0] + "</td>"
if format.get_selected() == "html":
timeline_xml += "<td><img src=\"{0}\"/>{1}</td>".format(gravatar.get_url(name[1]), name[0])
else:
timeline_xml += "<td>" + name[0] + "</td>"
for period in periods:
multiplier = timeline_data.get_multiplier(period, 14)
signs = timeline_data.get_author_signs_in_period(name, period, multiplier)
@ -225,7 +233,8 @@ class Timeline(Outputable):
if len(signs_str) == 0:
signs_str = "."
authors_xml += "\t\t\t\t\t<author>\n\t\t\t\t\t\t<name>" + name + "</name>\n"
authors_xml += "\t\t\t\t\t<author>\n\t\t\t\t\t\t<name>" + name[0] + "</name>\n"
authors_xml += "\t\t\t\t\t\t<gravatar>" + gravatar.get_url(name[1]) + "</gravatar>\n"
authors_xml += "\t\t\t\t\t\t<work>" + signs_str + "</work>\n\t\t\t\t\t</author>\n"
authors_xml += "\t\t\t\t</authors>\n"