From 35852f985916eba7eb700034c678c04fb850c171 Mon Sep 17 00:00:00 2001 From: Adam Waldenberg Date: Sat, 20 Jul 2013 11:45:11 +0200 Subject: [PATCH] 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. --- gitinspector/blame.py | 41 ++++++++++++++++---------- gitinspector/changes.py | 55 ++++++++++++++++++++++++----------- gitinspector/gitinspector.py | 2 +- gitinspector/gravatar.py | 34 ++++++++++++++++++++++ gitinspector/html/html.header | 21 +++++++++---- gitinspector/timeline.py | 19 ++++++++---- 6 files changed, 128 insertions(+), 44 deletions(-) create mode 100644 gitinspector/gravatar.py 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"