diff --git a/gitinspector/blame.py b/gitinspector/blame.py
index e17ca2a..4278128 100644
--- a/gitinspector/blame.py
+++ b/gitinspector/blame.py
@@ -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 += "
" if i % 2 == 1 else ">")
- blame_xml += "" + entry[0] + " | "
+
+ if format.get_selected() == "html":
+ blame_xml += "{1} | ".format(gravatar.get_url(entry[0]), authorname)
+ else:
+ blame_xml += "" + authorname + " | "
+
blame_xml += "" + str(entry[1].rows) + " | "
blame_xml += "" + "{0:.2f}".format(100.0 * entry[1].comments / entry[1].rows) + " | "
blame_xml += "" + work_percentage + " | "
blame_xml += "
"
- 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" + i[0] + "\n"
+ authorname = self.changes.get_authorname_from_email(i[0])
+ name_xml = "\t\t\t\t" + authorname + "\n"
+ gravatar_xml = "\t\t\t\t" + gravatar.get_url(i[0]) + "\n"
rows_xml = "\t\t\t\t" + str(i[1].rows) + "\n"
percentage_in_comments_xml = ("\t\t\t\t" + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) +
"\n")
- blame_xml += "\t\t\t\n" + name_xml + rows_xml + percentage_in_comments_xml + "\t\t\t\n"
+ blame_xml += "\t\t\t\n" + name_xml + gravatar_xml + rows_xml + percentage_in_comments_xml + "\t\t\t\n"
print("\t\n" + message_xml + "\t\t\n" + blame_xml + "\t\t\n\t")
diff --git a/gitinspector/changes.py b/gitinspector/changes.py
index 25ce4b0..f25dfa7 100644
--- a/gitinspector/changes.py
+++ b/gitinspector/changes.py
@@ -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 += "" if i % 2 == 1 else ">")
- changes_xml += "" + entry + " | "
+
+ if format.get_selected() == "html":
+ changes_xml += "{1} | ".format(gravatar.get_url(entry[1]), entry[0])
+ else:
+ changes_xml += "" + entry[0] + " | "
+
changes_xml += "" + str(authorinfo.commits) + " | "
changes_xml += "" + str(authorinfo.insertions) + " | "
changes_xml += "" + str(authorinfo.deletions) + " | "
changes_xml += "" + "{0:.2f}".format(percentage) + " | "
changes_xml += "
"
- 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" + i + "\n"
+ name_xml = "\t\t\t\t" + i[0] + "\n"
+ gravatar_xml = "\t\t\t\t" + gravatar.get_url(i[1]) + "\n"
commits_xml = "\t\t\t\t" + str(authorinfo.commits) + "\n"
insertions_xml = "\t\t\t\t" + str(authorinfo.insertions) + "\n"
deletions_xml = "\t\t\t\t" + str(authorinfo.deletions) + "\n"
percentage_xml = "\t\t\t\t" + "{0:.2f}".format(percentage) + "\n"
- changes_xml += ("\t\t\t\n" + name_xml + commits_xml + insertions_xml +
+ changes_xml += ("\t\t\t\n" + name_xml + gravatar_xml + commits_xml + insertions_xml +
deletions_xml + percentage_xml + "\t\t\t\n")
print("\t\n" + message_xml + "\t\t\n" + changes_xml + "\t\t\n\t")
diff --git a/gitinspector/gitinspector.py b/gitinspector/gitinspector.py
index 7130493..8cb4bd0 100755
--- a/gitinspector/gitinspector.py
+++ b/gitinspector/gitinspector.py
@@ -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))
diff --git a/gitinspector/gravatar.py b/gitinspector/gravatar.py
new file mode 100644
index 0000000..9f2fd02
--- /dev/null
+++ b/gitinspector/gravatar.py
@@ -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 .
+
+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)
diff --git a/gitinspector/html/html.header b/gitinspector/html/html.header
index a591108..900393d 100644
--- a/gitinspector/html/html.header
+++ b/gitinspector/html/html.header
@@ -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;
+ }}
diff --git a/gitinspector/timeline.py b/gitinspector/timeline.py
index 165e161..5919a6f 100644
--- a/gitinspector/timeline.py
+++ b/gitinspector/timeline.py
@@ -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 += "" if i % 2 == 1 else ">")
- timeline_xml += "" + name + " | "
+ #timeline_xml += "" + name[0] + " | "
+
+ if format.get_selected() == "html":
+ timeline_xml += "{1} | ".format(gravatar.get_url(name[1]), name[0])
+ else:
+ timeline_xml += "" + name[0] + " | "
+
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\n\t\t\t\t\t\t" + name + "\n"
+ authors_xml += "\t\t\t\t\t\n\t\t\t\t\t\t" + name[0] + "\n"
+ authors_xml += "\t\t\t\t\t\t" + gravatar.get_url(name[1]) + "\n"
authors_xml += "\t\t\t\t\t\t" + signs_str + "\n\t\t\t\t\t\n"
authors_xml += "\t\t\t\t\n"