mirror of
https://github.com/ejwa/gitinspector.git
synced 2024-11-13 07:11:08 +01:00
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:
parent
d07da3dc69
commit
35852f9859
6 changed files with 128 additions and 44 deletions
|
@ -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>")
|
||||
|
|
|
@ -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>")
|
||||
|
|
|
@ -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
34
gitinspector/gravatar.py
Normal 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)
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue