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"