From 37c426014958fa035177e7d45d02457596a84b00 Mon Sep 17 00:00:00 2001
From: Adam Waldenberg <adam.waldenberg@ejwa.se>
Date: Mon, 11 Mar 2013 00:23:50 +0100
Subject: [PATCH] Added an Outputable class and worked a little more on the
 HTML output.

The Outputable class is now the base class of all the classes that want to
output formatted text. This is more object oriented and cleaner solution
compared to the previous implementation.
---
 blame.py            | 116 +++++++++++++++++++++-------------------
 changes.py          | 127 +++++++++++++++++++++++++++++---------------
 extensions.py       |  49 +++++++++--------
 filtering.py        |  31 ++++++-----
 format.py           |  14 ++---
 gitinspector.py     |  19 ++++---
 metrics.py          |  51 +++++++++---------
 missing.py          |  31 ++++++-----
 outputable.py       |  39 ++++++++++++++
 responsibilities.py |  68 +++++++++++++-----------
 timeline.py         |  74 ++++++++++++++------------
 11 files changed, 350 insertions(+), 269 deletions(-)
 create mode 100644 outputable.py

diff --git a/blame.py b/blame.py
index 53b8297..3e117ea 100644
--- a/blame.py
+++ b/blame.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 from changes import FileDiff
 import comment
 import filtering
@@ -146,72 +147,77 @@ def get(hard):
 __blame_info_text__ = ("Below are the number of rows from each author that have survived and are still "
                        "intact in the current revision")
 
-def output_html(hard):
-	get(hard)
+class BlameOutput(Outputable):
+	def __init__(self, hard):
+		self.hard = hard
+		Outputable.__init__(self)
 
-	blame_xml = "<div class=\"box statistics\">"
-	blame_xml += "<p>" + __blame_info_text__ + ".</p><div><table class=\"git\">"
-	blame_xml += "<thead><tr> <th>Author</th> <th>Rows</th> <th>% in comments</th> </tr></thead>"
-	blame_xml += "<tbody>"
-	chart_data = ""
-	blames = sorted(__blame__.get_summed_blames().items())
-	total_blames = 0
+	def output_html(self):
+		get(self.hard)
 
-	for i in blames:
-		total_blames += i[1].rows
+		blame_xml = "<div class=\"statistics right\"><div class=\"box\">"
+		blame_xml += "<p>" + __blame_info_text__ + ".</p><div><table class=\"git\">"
+		blame_xml += "<thead><tr> <th>Author</th> <th>Rows</th> <th>% in comments</th> </tr></thead>"
+		blame_xml += "<tbody>"
+		chart_data = ""
+		blames = sorted(__blame__.get_summed_blames().items())
+		total_blames = 0
 
-	for i, entry in enumerate(blames):
-		blame_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">")
-		blame_xml += "<td>" + entry[0] + "</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 += "</tr>"
-		chart_data += "{{label: \"{0}\", data: {1}}}".format(entry[0], "{0:.2f}".format(100.0 * entry[1].rows / total_blames))
+		for i in blames:
+			total_blames += i[1].rows
 
-		if blames[-1] != entry:
-			chart_data += ", "
+		for i, entry in enumerate(blames):
+			blame_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">")
+			blame_xml += "<td>" + entry[0] + "</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 += "</tr>"
+			chart_data += "{{label: \"{0}\", data: {1}}}".format(entry[0], "{0:.2f}".format(100.0 * entry[1].rows / total_blames))
 
-	blame_xml += "<tfoot><tr> <td>&nbsp;</td> <td>&nbsp;</td> <td>&nbsp;</td> </tr></tfoot></tbody></table>"
-	blame_xml += "<div class=\"chart\" id=\"blame_chart\"></div></div></div>"
+			if blames[-1] != entry:
+				chart_data += ", "
 
-	blame_xml += "<script type=\"text/javascript\">"
-	blame_xml += "    $.plot($(\"#blame_chart\"), [{0}], {{".format(chart_data)
-	blame_xml += "        series: {"
-	blame_xml += "            pie: {"
-	blame_xml += "                innerRadius: 0.4,"
-	blame_xml += "                show: true"
-	blame_xml += "            }"
-	blame_xml += "        }"
-	blame_xml += "    });"
-	blame_xml += "</script>"
+		blame_xml += "<tfoot><tr> <td>&nbsp;</td> <td>&nbsp;</td> <td>&nbsp;</td> </tr></tfoot></tbody></table>"
+		blame_xml += "<div class=\"chart\" id=\"blame_chart\"></div></div></div></div>"
 
-	print(blame_xml)
+		blame_xml += "<script type=\"text/javascript\">"
+		blame_xml += "    $.plot($(\"#blame_chart\"), [{0}], {{".format(chart_data)
+		blame_xml += "        series: {"
+		blame_xml += "            pie: {"
+		blame_xml += "                innerRadius: 0.4,"
+		blame_xml += "                show: true"
+		blame_xml += "            }"
+		blame_xml += "        }"
+		blame_xml += "    });"
+		blame_xml += "</script>"
 
-def output_text(hard):
-	print("")
-	get(hard)
+		print(blame_xml)
 
-	if hard and sys.stdout.isatty():
-		terminal.clear_row()
+	def output_text(self):
+		print("")
+		get(self.hard)
 
-	print(textwrap.fill(__blame_info_text__ + ":", width=terminal.get_size()[0]) + "\n")
-	terminal.printb("Author".ljust(21) + "Rows".rjust(10) + "% in comments".rjust(16))
-	for i in sorted(__blame__.get_summed_blames().items()):
-		print(i[0].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(15))
+		if self.hard and sys.stdout.isatty():
+			terminal.clear_row()
 
-def output_xml(hard):
-	get(hard)
+		print(textwrap.fill(__blame_info_text__ + ":", width=terminal.get_size()[0]) + "\n")
+		terminal.printb("Author".ljust(21) + "Rows".rjust(10) + "% in comments".rjust(16))
+		for i in sorted(__blame__.get_summed_blames().items()):
+			print(i[0].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(15))
 
-	message_xml = "\t\t<message>" + __blame_info_text__ + "</message>\n"
-	blame_xml = ""
+	def output_xml(self):
+		get(self.hard)
 
-	for i in sorted(__blame__.get_summed_blames().items()):
-		name_xml = "\t\t\t\t<name>" + i[0] + "</name>\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"
+		message_xml = "\t\t<message>" + __blame_info_text__ + "</message>\n"
+		blame_xml = ""
 
-	print("\t<blame>\n" + message_xml + "\t\t<authors>\n" + blame_xml + "\t\t</authors>\n\t</blame>")
+		for i in sorted(__blame__.get_summed_blames().items()):
+			name_xml = "\t\t\t\t<name>" + i[0] + "</name>\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"
+
+		print("\t<blame>\n" + message_xml + "\t\t<authors>\n" + blame_xml + "\t\t</authors>\n\t</blame>")
diff --git a/changes.py b/changes.py
index 3873a1c..5c6fe20 100644
--- a/changes.py
+++ b/changes.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 import extensions
 import filtering
 import re
@@ -151,60 +152,98 @@ def get(hard):
 	return __changes__
 
 __historical_info_text__ = "The following historical commit information, by author, was found in the repository"
+__no_commited_files__ = "No commited files with the specified extensions were found"
 
-def output_html(hard):
-	print("HTML output not yet supported.")
+class ChangesOutput(Outputable):
+	def __init__(self, hard):
+		self.hard = hard
+		Outputable.__init__(self)
 
-def output_text(hard):
-	authorinfo_list = get(hard).get_authorinfo_list()
-	total_changes = 0.0
+	def output_html(self):
+		authorinfo_list = get(self.hard).get_authorinfo_list()
+		total_changes = 0.0
+		changes_xml = "<div class=\"statistics right\"><div class=\"box\">"
 
-	for i in authorinfo_list:
-		total_changes += authorinfo_list.get(i).insertions
-		total_changes += authorinfo_list.get(i).deletions
+		for i in authorinfo_list:
+			total_changes += authorinfo_list.get(i).insertions
+			total_changes += authorinfo_list.get(i).deletions
 
-	if authorinfo_list:
-		print(textwrap.fill(__historical_info_text__ + ":", width=terminal.get_size()[0]) + "\n")
-		terminal.printb("Author".ljust(21) + "Commits   " + "Insertions   " + "Deletions   " + "% of changes")
+		if authorinfo_list:
+			changes_xml += "<p>" + __historical_info_text__ + ".</p><div><table class=\"git\">"
+			changes_xml += ("<thead><tr> <th>Author</th> <th>Commits</th> <th>Insertions</th> <th>Deletions</th>" +
+			               "<th>% of changes</th> </tr></thead>")
+			changes_xml += "<tbody>"
 
-		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
+			for i, entry in enumerate(sorted(authorinfo_list)):
+				authorinfo = authorinfo_list.get(entry)
+				percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
 
-			print(i.ljust(20)[0:20], end=" ")
-			print(str(authorinfo.commits).rjust(7), end=" ")
-			print(str(authorinfo.insertions).rjust(12), end=" ")
-			print(str(authorinfo.deletions).rjust(11), end=" ")
-			print("{0:.2f}".format(percentage).rjust(14))
-	else:
-		print("No commited files with the specified extensions were found.")
+				changes_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">")
+				changes_xml += "<td>" + entry + "</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>"
 
-def output_xml(hard):
-	authorinfo_list = get(hard).get_authorinfo_list()
-	total_changes = 0.0
+			changes_xml += ("<tfoot><tr> <td>&nbsp;</td> <td>&nbsp;</td> <td>&nbsp;</td> <td>&nbsp;</td> <td>&nbsp;</td>" +
+			               "</tr></tfoot></tbody></table>")
+			changes_xml += "</div>"
+		else:
+			changes_xml += "<p>" + __no_commited_files__ + ".</p>"
 
-	for i in authorinfo_list:
-		total_changes += authorinfo_list.get(i).insertions
-		total_changes += authorinfo_list.get(i).deletions
+		changes_xml += "</div></div>"
+		print(changes_xml)
 
-	if authorinfo_list:
-		message_xml = "\t\t<message>" + __historical_info_text__ + "</message>\n"
-		changes_xml = ""
+	def output_text(self):
+		authorinfo_list = get(self.hard).get_authorinfo_list()
+		total_changes = 0.0
 
-		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
+		for i in authorinfo_list:
+			total_changes += authorinfo_list.get(i).insertions
+			total_changes += authorinfo_list.get(i).deletions
 
-			name_xml = "\t\t\t\t<name>" + i + "</name>\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"
+		if authorinfo_list:
+			print(textwrap.fill(__historical_info_text__ + ":", width=terminal.get_size()[0]) + "\n")
+			terminal.printb("Author".ljust(21) + "Commits   " + "Insertions   " + "Deletions   " + "% of changes")
 
-			changes_xml += ("\t\t\t<author>\n" + name_xml + commits_xml + insertions_xml +
-			                deletions_xml + percentage_xml + "\t\t\t</author>\n")
+			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
 
-		print("\t<changes>\n" + message_xml + "\t\t<authors>\n" + changes_xml + "\t\t</authors>\n\t</changes>")
-	else:
-		print("\t<changes>\n\t\t<exception>" + "No commited files with the specified extensions were found." +
-		      "</exception>\n\t</changes>")
+				print(i.ljust(20)[0:20], end=" ")
+				print(str(authorinfo.commits).rjust(7), end=" ")
+				print(str(authorinfo.insertions).rjust(12), end=" ")
+				print(str(authorinfo.deletions).rjust(11), end=" ")
+				print("{0:.2f}".format(percentage).rjust(14))
+		else:
+			print(__no_commited_files__ + ".")
+
+	def output_xml(self):
+		authorinfo_list = get(self.hard).get_authorinfo_list()
+		total_changes = 0.0
+
+		for i in authorinfo_list:
+			total_changes += authorinfo_list.get(i).insertions
+			total_changes += authorinfo_list.get(i).deletions
+
+		if authorinfo_list:
+			message_xml = "\t\t<message>" + __historical_info_text__ + "</message>\n"
+			changes_xml = ""
+
+			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"
+				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 +
+				                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>")
+		else:
+			print("\t<changes>\n\t\t<exception>" + __no_commited_files__ + "</exception>\n\t</changes>")
diff --git a/extensions.py b/extensions.py
index 17fa510..3a7a3fc 100644
--- a/extensions.py
+++ b/extensions.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 import terminal
 import textwrap
 
@@ -38,32 +39,30 @@ def add_located(string):
 
 __extensions_info_text__ = "The extensions below were found in the repository history"
 
-def output_html():
-	print("HTML output not yet supported.")
+class Extensions(Outputable):
+	def output_text(self):
+		if __located_extensions__:
+			print("\n" + textwrap.fill(__extensions_info_text__ + "\n(extensions used during statistical analysis are marked):",
+			      width=terminal.get_size()[0]))
 
-def output_text():
-	if __located_extensions__:
-		print("\n" + textwrap.fill(__extensions_info_text__ + "\n(extensions used during statistical analysis are marked):",
-		      width=terminal.get_size()[0]))
+			for i in __located_extensions__:
+				if i in __extensions__:
+					print("[" + terminal.__bold__ + i + terminal.__normal__ + "]", end=" ")
+				else:
+					print (i, end=" ")
+			print("")
 
-		for i in __located_extensions__:
-			if i in __extensions__:
-				print("[" + terminal.__bold__ + i + terminal.__normal__ + "]", end=" ")
-			else:
-				print (i, end=" ")
-		print("")
+	def output_xml(self):
+		if __located_extensions__:
+			message_xml = "\t\t<message>" + __extensions_info_text__ + "</message>\n"
+			used_extensions_xml = ""
+			unused_extensions_xml = ""
 
-def output_xml():
-	if __located_extensions__:
-		message_xml = "\t\t<message>" + __extensions_info_text__ + "</message>\n"
-		used_extensions_xml = ""
-		unused_extensions_xml = ""
+			for i in __located_extensions__:
+				if i in __extensions__:
+					used_extensions_xml += "\t\t\t<extension>" + i + "</extension>\n"
+				else:
+					unused_extensions_xml += "\t\t\t<extension>" + i + "</extension>\n"
 
-		for i in __located_extensions__:
-			if i in __extensions__:
-				used_extensions_xml += "\t\t\t<extension>" + i + "</extension>\n"
-			else:
-				unused_extensions_xml += "\t\t\t<extension>" + i + "</extension>\n"
-
-		print("\t<extensions>\n" + "\t\t<used>\n" + used_extensions_xml + "\t\t</used>\n" +
-		      "\t\t<unused>\n" + unused_extensions_xml + "\t\t</unused>\n" + "\t</extensions>")
+			print("\t<extensions>\n" + "\t\t<used>\n" + used_extensions_xml + "\t\t</used>\n" +
+			      "\t\t<unused>\n" + unused_extensions_xml + "\t\t</unused>\n" + "\t</extensions>")
diff --git a/filtering.py b/filtering.py
index f0ee051..2ddd3f9 100644
--- a/filtering.py
+++ b/filtering.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 import terminal
 import textwrap
 
@@ -46,23 +47,21 @@ def set_filtered(file_name):
 __filtering_info_text__ = ("The following files were excluded from the statistics due to the"
                            "specified exclusion patterns")
 
-def output_html():
-	print("HTML output not yet supported.")
+class Filtering(Outputable):
+	def output_text(self):
+		if __filtered_files__:
+			print("\n" + textwrap.fill(__filtering_info_text__ + ":", width=terminal.get_size()[0]))
 
-def output_text():
-	if __filtered_files__:
-		print("\n" + textwrap.fill(__filtering_info_text__ + ":", width=terminal.get_size()[0]))
+			for i in __filtered_files__:
+				(width, _) = terminal.get_size()
+				print("...%s" % i[-width+3:] if len(i) > width else i)
 
-		for i in __filtered_files__:
-			(width, _) = terminal.get_size()
-			print("...%s" % i[-width+3:] if len(i) > width else i)
+	def output_xml(self):
+		if __filtered_files__:
+			message_xml = "\t\t<message>" + __filtering_info_text__ + "</message>\n"
+			filtering_xml = ""
 
-def output_xml():
-	if __filtered_files__:
-		message_xml = "\t\t<message>" + __filtering_info_text__ + "</message>\n"
-		filtering_xml = ""
+			for i in __filtered_files__:
+				filtering_xml += "\t\t\t<file>" + i + "</file>\n"
 
-		for i in __filtered_files__:
-			filtering_xml += "\t\t\t<file>" + i + "</file>\n"
-
-		print("\t<filering>\n" + message_xml + "\t\t<files>\n" + filtering_xml + "\t\t</files>\n\t</filtering>")
+			print("\t<filering>\n" + message_xml + "\t\t<files>\n" + filtering_xml + "\t\t</files>\n\t</filtering>")
diff --git a/format.py b/format.py
index a64675a..6429eda 100644
--- a/format.py
+++ b/format.py
@@ -18,11 +18,10 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+import version
 import base64
 import basedir
 import os
-import terminal
-import version
 import zipfile
 
 __available_formats__ = ["html", "text", "xml"]
@@ -38,6 +37,9 @@ def select(format):
 
 	return format in __available_formats__
 
+def get_selected():
+	return __selected_format__
+
 def is_interactive_format():
 	return __selected_format__ == "text"
 
@@ -75,11 +77,3 @@ def output_footer():
 		print(html_footer)
 	elif __selected_format__ == "xml":
 		print("</gitinspector>")
- 
-def call_output_function(html_function, text_function, xml_function, *parameters):
-	if __selected_format__ == "html":
-		html_function(*parameters)
-	elif __selected_format__ == "text":
-		text_function(*parameters)
-	else:
-		xml_function(*parameters)
diff --git a/gitinspector.py b/gitinspector.py
index 59cc0c6..7adbe02 100755
--- a/gitinspector.py
+++ b/gitinspector.py
@@ -29,6 +29,7 @@ import help
 import metrics
 import missing
 import os
+import outputable
 import responsibilities
 import sys
 import terminal
@@ -51,27 +52,25 @@ class Runner:
 		previous_directory = os.getcwd()
 		os.chdir(self.repo)
 		format.output_header()
-		format.call_output_function(changes.output_html, changes.output_text, changes.output_xml, self.hard)
+		outputable.output(changes.ChangesOutput(self.hard))
 
 		if changes.get(self.hard).get_commits():
-			format.call_output_function(blame.output_html, blame.output_text, blame.output_xml, self.hard)
+			outputable.output(blame.BlameOutput(self.hard))
 
 			if self.timeline:
-				format.call_output_function(timeline.output_html, timeline.output_text, timeline.output_xml,
-				                            changes.get(self.hard), self.useweeks)
+				outputable.output(timeline.Timeline(changes.get(self.hard), self.useweeks))
 
 			if self.include_metrics:
-				format.call_output_function(metrics.output_html, metrics.output_text, metrics.output_xml)
+				outputable.output(metrics.Metrics())
 
 			if self.responsibilities:
-				format.call_output_function(responsibilities.output_html, responsibilities.output_text,
-				                            responsibilities.output_xml, self.hard)
+				outputable.output(responsibilities.ResponsibilitiesOutput(self.hard))
 
-			format.call_output_function(missing.output_html, missing.output_text, missing.output_xml)
-			format.call_output_function(filtering.output_html, filtering.output_text, filtering.output_xml)
+			outputable.output(missing.Missing())
+			outputable.output(filtering.Filtering())
 
 			if self.list_file_types:
-				format.call_output_function(extensions.output_html, extensions.output_text, extensions.output_xml)
+				outputable.output(extensions.Extensions())
 
 		format.output_footer()
 		os.chdir(previous_directory)
diff --git a/metrics.py b/metrics.py
index 442dd86..01e41c0 100644
--- a/metrics.py
+++ b/metrics.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 from changes import FileDiff
 import comment
 import filtering
@@ -27,7 +28,7 @@ import subprocess
 __metric_eloc__ = {"java": 500, "c": 500, "cpp": 500, "h": 300, "hpp": 300, "py": 500, "glsl": 1000,
                    "rb": 500, "js": 500, "sql": 1000, "xml": 1000}
 
-class Metrics:
+class MetricsLogic:
 	def __init__(self):
 		self.eloc = {}
 		ls_tree_r = subprocess.Popen("git ls-tree --name-only -r HEAD", shell=True, bufsize=1, stdout=subprocess.PIPE).stdout
@@ -38,7 +39,7 @@ class Metrics:
 				if not missing.add(i.strip()):
 					file_r = open(i.strip(), "rb")
 					extension = FileDiff.get_extension(i)
-					lines = Metrics.get_eloc(file_r, extension)
+					lines = MetricsLogic.get_eloc(file_r, extension)
 
 					if __metric_eloc__.get(extension, None) != None and __metric_eloc__[extension] < lines:
 						self.eloc[i.strip()] = lines
@@ -63,31 +64,29 @@ class Metrics:
 __eloc_info_text__ = "The following files are suspiciously big (in order of severity)"
 __metrics_missing_info_text__ = "No metrics violations were found in the repository"
 
-def output_html():
-	print("HTML output not yet supported.")
+class Metrics(Outputable):
+	def output_text(self):
+		metrics_logic = MetricsLogic()
 
-def output_text():
-	metrics = Metrics()
+		if not metrics_logic.eloc:
+			print("\n" + __metrics_missing_info_text__ + ".")
+		else:
+			print("\n" + __eloc_info_text__ + ":")
+			for i in sorted(set([(j, i) for (i, j) in metrics_logic.eloc.items()]), reverse = True):
+				print(i[1] + " (" + str(i[0]) + " eloc)")
 
-	if not metrics.eloc:
-		print("\n" + __metrics_missing_info_text__ + ".")
-	else:
-		print("\n" + __eloc_info_text__ + ":")
-		for i in sorted(set([(j, i) for (i, j) in metrics.eloc.items()]), reverse = True):
-			print(i[1] + " (" + str(i[0]) + " eloc)")
+	def output_xml(self):
+		metrics_logic = MetricsLogic()
 
-def output_xml():
-	metrics = Metrics()
+		if not metrics_logic.eloc:
+			print("\t<metrics>\n\t\t<message>" + __metrics_missing_info_text__ + "</message>\n\t</metrics>")
+		else:
+			eloc_xml = ""
+			for i in sorted(set([(j, i) for (i, j) in metrics_logic.eloc.items()]), reverse = True):
+				eloc_xml += "\t\t\t\t\t<violation>\n"
+				eloc_xml += "\t\t\t\t\t\t<file-name>" + i[1] + "</file-name>\n"
+				eloc_xml += "\t\t\t\t\t\t<lines-of-code>" + str(i[0]) + "</lines-of-code>\n"
+				eloc_xml += "\t\t\t\t\t</violation>\n"
 
-	if not metrics.eloc:
-		print("\t<metrics>\n\t\t<message>" + __metrics_missing_info_text__ + "</message>\n\t</metrics>")
-	else:
-		eloc_xml = ""
-		for i in sorted(set([(j, i) for (i, j) in metrics.eloc.items()]), reverse = True):
-			eloc_xml += "\t\t\t\t\t<violation>\n"
-			eloc_xml += "\t\t\t\t\t\t<file-name>" + i[1] + "</file-name>\n"
-			eloc_xml += "\t\t\t\t\t\t<lines-of-code>" + str(i[0]) + "</lines-of-code>\n"
-			eloc_xml += "\t\t\t\t\t</violation>\n"
-
-		print("\t\t<metrics>\n\t\t\t<eloc>\n\t\t\t\t<message>" + __eloc_info_text__ +
-		      "</message>\n\t\t\t\t<violations>\n" + eloc_xml + "\t\t\t\t</violations>\n\t\t\t</eloc>\n\t\t</metrics>")
+			print("\t\t<metrics>\n\t\t\t<eloc>\n\t\t\t\t<message>" + __eloc_info_text__ +
+			      "</message>\n\t\t\t\t<violations>\n" + eloc_xml + "\t\t\t\t</violations>\n\t\t\t</eloc>\n\t\t</metrics>")
diff --git a/missing.py b/missing.py
index 11e1649..c14b324 100644
--- a/missing.py
+++ b/missing.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 import os
 import subprocess
 import terminal
@@ -43,23 +44,21 @@ __missing_info_text__ = ("The following files were missing in the repository and
                          "completely included in the statistical analysis. To include them, you can "
                          "either checkout manually using git or use the -c option in gitinspector")
 
-def output_html():
-	print("HTML output not yet supported.")
+class Missing(Outputable):
+	def output_text(self):
+		if __missing_files__:
+			print("\n" + textwrap.fill(__missing_info_text__ + ":", width=terminal.get_size()[0]))
 
-def output_text():
-	if __missing_files__:
-		print("\n" + textwrap.fill(__missing_info_text__ + ":", width=terminal.get_size()[0]))
+			for missing in __missing_files__:
+				(width, _) = terminal.get_size()
+				print("...%s" % missing[-width+3:] if len(missing) > width else missing)
 
-		for missing in __missing_files__:
-			(width, _) = terminal.get_size()
-			print("...%s" % missing[-width+3:] if len(missing) > width else missing)
+	def output_xml(self):
+		if __missing_files__:
+			message_xml = "\t\t<message>" + __missing_info_text__ + "</message>\n"
+			missing_xml = ""
 
-def output_xml():
-	if __missing_files__:
-		message_xml = "\t\t<message>" + __missing_info_text__ + "</message>\n"
-		missing_xml = ""
+			for missing in __missing_files__:
+				missing_xml += "\t\t\t<file>" + missing + "</file>\n"
 
-		for missing in __missing_files__:
-			missing_xml += "\t\t\t<file>" + missing + "</file>\n"
-
-		print("\t<missing>\n" + message_xml + "\t\t<files>\n" + missing_xml + "\t\t</files>\n\t</missing>")
+			print("\t<missing>\n" + message_xml + "\t\t<files>\n" + missing_xml + "\t\t</files>\n\t</missing>")
diff --git a/outputable.py b/outputable.py
new file mode 100644
index 0000000..648b26d
--- /dev/null
+++ b/outputable.py
@@ -0,0 +1,39 @@
+# coding: utf-8
+#
+# Copyright © 2012 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/>.
+
+from __future__ import print_function
+import format
+
+class Outputable(object):
+	def output_html(self):
+		print("HTML output not yet supported in " + self + ".")
+
+	def output_text(self):
+		print("Text output not yet supported in " + self + ".")
+
+	def output_xml(self):
+		print("XML output not yet supported in " + self + ".")
+
+def output(outputable):
+	if format.get_selected() == "html":
+		outputable.output_html()
+	elif format.get_selected() == "text":
+		outputable.output_text()
+	else:
+		outputable.output_xml()
diff --git a/responsibilities.py b/responsibilities.py
index bf7b05e..1ae9d82 100644
--- a/responsibilities.py
+++ b/responsibilities.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 import blame
 import terminal
 import textwrap
@@ -41,46 +42,49 @@ class Responsibilities:
 __responsibilities_info_text__ = ("The following repsonsibilties, by author, were found in the current "
                                   "revision of the repository (comments are exluded from the line count, "
                                   "if possible)")
-def output_html(hard):
-	print("HTML output not yet supported.")
 
-def output_text(hard):
-	print("\n" + textwrap.fill(__responsibilities_info_text__ + ":", width=terminal.get_size()[0]))
+class ResponsibilitiesOutput(Outputable):
+	def __init__(self, hard):
+		self.hard = hard
+		Outputable.__init__(self)
 
-	for i in sorted(set(i[0] for i in blame.get(hard).blames)):
-		print("\n" + i, "is mostly responsible for:")
-		responsibilities = sorted(((i[1], i[0]) for i in Responsibilities.get(hard, i)), reverse=True)
+	def output_text(self):
+		print("\n" + textwrap.fill(__responsibilities_info_text__ + ":", width=terminal.get_size()[0]))
 
-		for j, entry in enumerate(responsibilities):
-			(width, _) = terminal.get_size()
-			width -= 7
+		for i in sorted(set(i[0] for i in blame.get(self.hard).blames)):
+			print("\n" + i, "is mostly responsible for:")
+			responsibilities = sorted(((i[1], i[0]) for i in Responsibilities.get(self.hard, i)), reverse=True)
 
-			print(str(entry[0]).rjust(6), end=" ")
-			print("...%s" % entry[1][-width+3:] if len(entry[1]) > width else entry[1])
+			for j, entry in enumerate(responsibilities):
+				(width, _) = terminal.get_size()
+				width -= 7
 
-			if j >= 9:
-				break
+				print(str(entry[0]).rjust(6), end=" ")
+				print("...%s" % entry[1][-width+3:] if len(entry[1]) > width else entry[1])
 
-def output_xml(hard):
-	message_xml = "\t\t<message>" + __responsibilities_info_text__ + "</message>\n"
-	resp_xml = ""
+				if j >= 9:
+					break
 
-	for i in sorted(set(i[0] for i in blame.get(hard).blames)):
-		resp_xml += "\t\t\t<author>\n"
-		resp_xml += "\t\t\t\t<name>" + i + "</name>\n"
-		resp_xml += "\t\t\t\t<files>\n"
-		responsibilities = sorted(((i[1], i[0]) for i in Responsibilities.get(hard, i)), reverse=True)
+	def output_xml(self):
+		message_xml = "\t\t<message>" + __responsibilities_info_text__ + "</message>\n"
+		resp_xml = ""
 
-		for j, entry in enumerate(responsibilities):
-			resp_xml += "\t\t\t\t\t<file>\n"
-			resp_xml += "\t\t\t\t\t\t<name>" + entry[1] + "</name>\n"
-			resp_xml += "\t\t\t\t\t\t<rows>" + str(entry[0]) + "</rows>\n"
-			resp_xml += "\t\t\t\t\t</file>\n"
+		for i in sorted(set(i[0] for i in blame.get(self.hard).blames)):
+			resp_xml += "\t\t\t<author>\n"
+			resp_xml += "\t\t\t\t<name>" + i + "</name>\n"
+			resp_xml += "\t\t\t\t<files>\n"
+			responsibilities = sorted(((i[1], i[0]) for i in Responsibilities.get(self.hard, i)), reverse=True)
 
-			if j >= 9:
-				break
+			for j, entry in enumerate(responsibilities):
+				resp_xml += "\t\t\t\t\t<file>\n"
+				resp_xml += "\t\t\t\t\t\t<name>" + entry[1] + "</name>\n"
+				resp_xml += "\t\t\t\t\t\t<rows>" + str(entry[0]) + "</rows>\n"
+				resp_xml += "\t\t\t\t\t</file>\n"
 
-		resp_xml += "\t\t\t\t</files>\n"
-		resp_xml += "\t\t\t</author>\n"
+				if j >= 9:
+					break
 
-	print("\t<responsibilities>\n" + message_xml + "\t\t<authors>\n" + resp_xml + "\t\t</authors>\n\t</responsibilities>")
+			resp_xml += "\t\t\t\t</files>\n"
+			resp_xml += "\t\t\t</author>\n"
+
+		print("\t<responsibilities>\n" + message_xml + "\t\t<authors>\n" + resp_xml + "\t\t</authors>\n\t</responsibilities>")
diff --git a/timeline.py b/timeline.py
index 40cc5ef..e5ee59f 100644
--- a/timeline.py
+++ b/timeline.py
@@ -18,6 +18,7 @@
 # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
 
 from __future__ import print_function
+from outputable import Outputable
 import datetime
 import terminal
 import textwrap
@@ -93,9 +94,6 @@ class TimelineData:
 
 __timeline_info_text__ = "The following history timeline has been gathered from the repository"
 
-def output_html(changes, useweeks):
-	print("HTML output not yet supported.")
-
 def __output_row__text__(timeline_data, periods, names):
 	print("\n" + terminal.__bold__ + "Author".ljust(20), end=" ")
 	
@@ -114,45 +112,51 @@ def __output_row__text__(timeline_data, periods, names):
 			               len(signs_str) == 0 else signs_str).rjust(10), end=" ")
 		print("")
 
-def output_text(changes, useweeks):
-	if changes.get_commits():
-		print("\n" + textwrap.fill(__timeline_info_text__ + ":", width=terminal.get_size()[0]))
+class Timeline(Outputable):
+	def __init__(self, changes, useweeks):
+		self.changes = changes
+		self.useweeks = useweeks
+		Outputable.__init__(self)
 
-		timeline_data = TimelineData(changes, useweeks)
-		periods = timeline_data.get_periods()
-		names = timeline_data.get_authors()
-		(width, _) = terminal.get_size()
-		max_periods_per_row = int((width - 21) / 11)
+	def output_text(self):
+		if self.changes.get_commits():
+			print("\n" + textwrap.fill(__timeline_info_text__ + ":", width=terminal.get_size()[0]))
 
-		for i in range(0, len(periods), max_periods_per_row):
-			__output_row__text__(timeline_data, periods[i:i+max_periods_per_row], names)
+			timeline_data = TimelineData(self.changes, self.useweeks)
+			periods = timeline_data.get_periods()
+			names = timeline_data.get_authors()
+			(width, _) = terminal.get_size()
+			max_periods_per_row = int((width - 21) / 11)
 
-def output_xml(changes, useweeks):
-	if changes.get_commits():
-		message_xml = "\t\t<message>" + __timeline_info_text__ + "</message>\n"
-		timeline_xml = ""
-		periods_xml = "\t\t<periods length=\"{0}\">\n".format("week" if useweeks else "month")
+			for i in range(0, len(periods), max_periods_per_row):
+				__output_row__text__(timeline_data, periods[i:i+max_periods_per_row], names)
 
-		timeline_data = TimelineData(changes, useweeks)
-		periods = timeline_data.get_periods()
-		names = timeline_data.get_authors()
+	def output_xml(self):
+		if self.changes.get_commits():
+			message_xml = "\t\t<message>" + __timeline_info_text__ + "</message>\n"
+			timeline_xml = ""
+			periods_xml = "\t\t<periods length=\"{0}\">\n".format("week" if self.useweeks else "month")
 
-		for period in periods:
-			name_xml = "\t\t\t\t<name>" + str(period) + "</name>\n"
-			authors_xml = ""
+			timeline_data = TimelineData(self.changes, self.useweeks)
+			periods = timeline_data.get_periods()
+			names = timeline_data.get_authors()
 
-			for name in names:
-				authors_xml += "\t\t\t\t<authors>\n"
-				multiplier = timeline_data.get_multiplier(period, 24)
-				signs = timeline_data.get_author_signs_in_period(name, period, multiplier)
-				signs_str = (signs[1] * "-" + signs[0] * "+")
+			for period in periods:
+				name_xml = "\t\t\t\t<name>" + str(period) + "</name>\n"
+				authors_xml = ""
 
-				if not len(signs_str) == 0:
-					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\t<work>" + signs_str + "</work>\n\t\t\t\t\t</author>\n"
+				for name in names:
+					authors_xml += "\t\t\t\t<authors>\n"
+					multiplier = timeline_data.get_multiplier(period, 24)
+					signs = timeline_data.get_author_signs_in_period(name, period, multiplier)
+					signs_str = (signs[1] * "-" + signs[0] * "+")
 
-				authors_xml += "\t\t\t\t</authors>\n"
+					if not len(signs_str) == 0:
+						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\t<work>" + signs_str + "</work>\n\t\t\t\t\t</author>\n"
 
-			timeline_xml += "\t\t\t<period>\n" + name_xml + authors_xml + "\t\t\t</period>\n"
+					authors_xml += "\t\t\t\t</authors>\n"
 
-		print("\t<timeline>\n" + message_xml + periods_xml + timeline_xml + "\t\t</periods>\n\t</timeline>")
+				timeline_xml += "\t\t\t<period>\n" + name_xml + authors_xml + "\t\t\t</period>\n"
+
+			print("\t<timeline>\n" + message_xml + periods_xml + timeline_xml + "\t\t</periods>\n\t</timeline>")