From 89d80214b3d8e93bea99b9ec93f649acf8bd12f6 Mon Sep 17 00:00:00 2001 From: zhangjianqiang Date: Mon, 10 Dec 2018 19:27:18 +0800 Subject: [PATCH 1/3] add csv format with combine change and blame add csv format with combine change and blame --- gitinspector/format.py | 15 ++++++++- gitinspector/gitinspector.py | 44 ++++++++++++++++---------- gitinspector/output/filteringoutput.py | 15 +++++++++ gitinspector/output/outputable.py | 5 +++ 4 files changed, 61 insertions(+), 18 deletions(-) mode change 100644 => 100755 gitinspector/gitinspector.py diff --git a/gitinspector/format.py b/gitinspector/format.py index 505da033..8e06c8c0 100644 --- a/gitinspector/format.py +++ b/gitinspector/format.py @@ -27,12 +27,14 @@ from .localization import N_ from . import basedir, localization, terminal, version -__available_formats__ = ["html", "htmlembedded", "json", "text", "xml"] +__available_formats__ = ["html", "htmlembedded", "json", "text", "xml", "csv"] DEFAULT_FORMAT = __available_formats__[3] __selected_format__ = DEFAULT_FORMAT +__selected_format_tag__ = "" + class InvalidFormatError(Exception): def __init__(self, msg): super(InvalidFormatError, self).__init__(msg) @@ -47,6 +49,17 @@ def select(format): def get_selected(): return __selected_format__ + +def set_tag(format): + global __selected_format_tag__ + __selected_format_tag__ = format + + +def get_tag(): + return __selected_format_tag__ + + + def is_interactive_format(): return __selected_format__ == "text" diff --git a/gitinspector/gitinspector.py b/gitinspector/gitinspector.py old mode 100644 new mode 100755 index 71492943..ef7f15f3 --- a/gitinspector/gitinspector.py +++ b/gitinspector/gitinspector.py @@ -32,6 +32,7 @@ from .output import outputable from .output.blameoutput import BlameOutput from .output.changesoutput import ChangesOutput +from .output.changesblameoutput import ChangesBlameOutput from .output.extensionsoutput import ExtensionsOutput from .output.filteringoutput import FilteringOutput from .output.metricsoutput import MetricsOutput @@ -79,29 +80,34 @@ def process(self, repos): else: os.chdir(previous_directory) - format.output_header(repos) - outputable.output(ChangesOutput(summed_changes)) - - if summed_changes.get_commits(): - outputable.output(BlameOutput(summed_changes, summed_blames)) + # print("current format: " + format.get_selected()) + if format.get_selected() == "csv": + # print("output format: " + format.get_selected()) + outputable.output(ChangesBlameOutput(summed_changes, summed_blames)) + else: + format.output_header(repos) + outputable.output(ChangesOutput(summed_changes)) - if self.timeline: - outputable.output(TimelineOutput(summed_changes, self.useweeks)) + if summed_changes.get_commits(): + outputable.output(BlameOutput(summed_changes, summed_blames)) - if self.include_metrics: - outputable.output(MetricsOutput(summed_metrics)) + if self.timeline: + outputable.output(TimelineOutput(summed_changes, self.useweeks)) - if self.responsibilities: - outputable.output(ResponsibilitiesOutput(summed_changes, summed_blames)) + if self.include_metrics: + outputable.output(MetricsOutput(summed_metrics)) - outputable.output(FilteringOutput()) + if self.responsibilities: + outputable.output(ResponsibilitiesOutput(summed_changes, summed_blames)) - if self.list_file_types: - outputable.output(ExtensionsOutput()) + outputable.output(FilteringOutput()) - format.output_footer() + if self.list_file_types: + outputable.output(ExtensionsOutput()) + format.output_footer() os.chdir(previous_directory) + def __check_python_version__(): if sys.version_info < (2, 6): python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1]) @@ -133,12 +139,13 @@ def main(): repos = [] try: - opts, args = optval.gnu_getopt(argv[1:], "f:F:hHlLmrTwx:", ["exclude=", "file-types=", "format=", + opts, args = optval.gnu_getopt(argv[1:], "f:t:F:hHlLmrTwx:", ["exclude=", "file-types=", "tag=" ,"format=", "hard:true", "help", "list-file-types:true", "localize-output:true", "metrics:true", "responsibilities:true", "since=", "grading:true", "timeline:true", "until=", "version", "weeks:true"]) + # print("begin git clone") repos = __get_validated_git_repos__(set(args)) - + # print("end git clone") #We need the repos above to be set before we read the git config. GitConfig(run, repos[-1].location).read() clear_x_on_next_pass = True @@ -149,6 +156,8 @@ def main(): sys.exit(0) elif o in("-f", "--file-types"): extensions.define(a) + elif o in("-t", "--tag"): + format.set_tag(a) elif o in("-F", "--format"): if not format.select(a): raise format.InvalidFormatError(_("specified output format not supported.")) @@ -213,6 +222,7 @@ def main(): @atexit.register def cleanup(): clone.delete() + # pass if __name__ == "__main__": main() diff --git a/gitinspector/output/filteringoutput.py b/gitinspector/output/filteringoutput.py index f2c06c96..4a34a32e 100644 --- a/gitinspector/output/filteringoutput.py +++ b/gitinspector/output/filteringoutput.py @@ -119,3 +119,18 @@ def output_xml(self): FilteringOutput.__output_xml_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1], "emails") FilteringOutput.__output_xml_section__(_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1], "revision") print("\t") + + @staticmethod + def __output_csv_section__(info_string, filtered): + if filtered: + print("\n" + textwrap.fill(info_string + ":", width=terminal.get_size()[0])) + + for i in filtered: + (width, _unused) = terminal.get_size() + print("...%s" % i[-width+3:] if len(i) > width else i) + + def output_csv(self): + FilteringOutput.__output_csv_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1]) + FilteringOutput.__output_csv_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1]) + FilteringOutput.__output_csv_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1]) + FilteringOutput.__output_csv_section__(_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1]) \ No newline at end of file diff --git a/gitinspector/output/outputable.py b/gitinspector/output/outputable.py index 500fbe2e..47ab2021 100644 --- a/gitinspector/output/outputable.py +++ b/gitinspector/output/outputable.py @@ -34,6 +34,9 @@ def output_text(self): def output_xml(self): raise NotImplementedError(_("XML output not yet supported in") + " \"" + self.__class__.__name__ + "\".") + def output_csv(self): + raise NotImplementedError(_("CSV output not yet supported in") + " \"" + self.__class__.__name__ + "\".") + def output(outputable): if format.get_selected() == "html" or format.get_selected() == "htmlembedded": outputable.output_html() @@ -41,5 +44,7 @@ def output(outputable): outputable.output_json() elif format.get_selected() == "text": outputable.output_text() + elif format.get_selected() == "csv": + outputable.output_csv() else: outputable.output_xml() From 08528af70f8918569644fbd9cdb56566ba843148 Mon Sep 17 00:00:00 2001 From: zhangjianqiang Date: Tue, 11 Dec 2018 01:04:01 +0800 Subject: [PATCH 2/3] add csv format with combine change and blame add csv format with combine change and blame --- gitinspector/output/changesblameoutput.py | 239 ++++++++++++++++++++++ 1 file changed, 239 insertions(+) create mode 100644 gitinspector/output/changesblameoutput.py diff --git a/gitinspector/output/changesblameoutput.py b/gitinspector/output/changesblameoutput.py new file mode 100644 index 00000000..5a417c3b --- /dev/null +++ b/gitinspector/output/changesblameoutput.py @@ -0,0 +1,239 @@ +# coding: utf-8 +# +# Copyright © 2012-2015 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 . + +from __future__ import print_function +from __future__ import unicode_literals +import json +import textwrap +from ..localization import N_ +from .. import format, gravatar, terminal +from .outputable import Outputable +from ..blame import Blame + +HISTORICAL_INFO_TEXT = N_("The following historical commit information, by author, was found") +NO_COMMITED_FILES_TEXT = N_("No commited files with the specified extensions were found") + +class ChangesBlameOutput(Outputable): + def __init__(self, changes, blame): + if format.is_interactive_format(): + print("") + + self.changes = changes + self.blame = blame + Outputable.__init__(self) + + def output_html(self): + authorinfo_list = self.changes.get_authorinfo_list() + total_changes = 0.0 + changes_xml = "
" + chart_data = "" + + for i in authorinfo_list: + total_changes += authorinfo_list.get(i).insertions + total_changes += authorinfo_list.get(i).deletions + + if authorinfo_list: + changes_xml += "

" + _(HISTORICAL_INFO_TEXT) + ".

" + changes_xml += "".format( + _("Author"), _("Commits"), _("Insertions"), _("Deletions"), _("% of changes")) + changes_xml += "" + + 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 + + changes_xml += "" if i % 2 == 1 else ">") + + if format.get_selected() == "html": + changes_xml += "".format( + gravatar.get_url(self.changes.get_latest_email_by_author(entry)), entry) + else: + changes_xml += "" + + changes_xml += "" + changes_xml += "" + changes_xml += "" + changes_xml += "" + changes_xml += "" + chart_data += "{{label: {0}, data: {1}}}".format(json.dumps(entry), "{0:.2f}".format(percentage)) + + if sorted(authorinfo_list)[-1] != entry: + chart_data += ", " + + changes_xml += ("
{0} {1} {2} {3} {4}
{1}" + entry + "" + str(authorinfo.commits) + "" + str(authorinfo.insertions) + "" + str(authorinfo.deletions) + "" + "{0:.2f}".format(percentage) + "
 
") + changes_xml += "
" + changes_xml += "" + else: + changes_xml += "

" + _(NO_COMMITED_FILES_TEXT) + ".

" + + changes_xml += "
" + print(changes_xml) + + def output_json(self): + authorinfo_list = self.changes.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_json = "\t\t\t\"message\": \"" + _(HISTORICAL_INFO_TEXT) + "\",\n" + changes_json = "" + + for i in sorted(authorinfo_list): + author_email = self.changes.get_latest_email_by_author(i) + authorinfo = authorinfo_list.get(i) + + percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 + name_json = "\t\t\t\t\"name\": \"" + i + "\",\n" + email_json = "\t\t\t\t\"email\": \"" + author_email + "\",\n" + gravatar_json = "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n" + commits_json = "\t\t\t\t\"commits\": " + str(authorinfo.commits) + ",\n" + insertions_json = "\t\t\t\t\"insertions\": " + str(authorinfo.insertions) + ",\n" + deletions_json = "\t\t\t\t\"deletions\": " + str(authorinfo.deletions) + ",\n" + percentage_json = "\t\t\t\t\"percentage_of_changes\": " + "{0:.2f}".format(percentage) + "\n" + + changes_json += ("{\n" + name_json + email_json + gravatar_json + commits_json + + insertions_json + deletions_json + percentage_json + "\t\t\t}") + changes_json += "," + else: + changes_json = changes_json[:-1] + + print("\t\t\"changes\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + changes_json + "]\n\t\t}", end="") + else: + print("\t\t\"exception\": \"" + _(NO_COMMITED_FILES_TEXT) + "\"") + + def output_text(self): + authorinfo_list = self.changes.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: + print(textwrap.fill(_(HISTORICAL_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n") + terminal.printb(terminal.ljust(_("Author"), 21) + terminal.rjust(_("Commits"), 13) + + terminal.rjust(_("Insertions"), 14) + terminal.rjust(_("Deletions"), 15) + + terminal.rjust(_("% of changes"), 16) + + terminal.rjust(_("Author"), 21) + terminal.rjust(_("Rows"), 10) + terminal.rjust(_("Stability"), + 15) + + terminal.rjust(_("Age"), 13) + terminal.rjust(_("% in comments"), 20)) + + for i,j in zip(sorted(authorinfo_list),sorted(self.blame.get_summed_blames().items())): + authorinfo = authorinfo_list.get(i) + percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 + + print(terminal.ljust(i, 20)[0:20 - terminal.get_excess_column_count(i)], end=" ") + print(str(authorinfo.commits).rjust(13), end=" ") + print(str(authorinfo.insertions).rjust(13), end=" ") + print(str(authorinfo.deletions).rjust(14), end=" ") + print("{0:.2f}".format(percentage).rjust(15), end=" ") + # blame + #print(terminal.ljust(j[0], 20)[0:20 - terminal.get_excess_column_count(j[0])], end=" ") + print(j[0].rjust(21), end=" ") + print(str(j[1].rows).rjust(10), end=" ") + print("{0:.1f}".format(Blame.get_stability(j[0], j[1].rows, self.changes)).rjust(14), end=" ") + print("{0:.1f}".format(float(j[1].skew) / j[1].rows).rjust(12), end=" ") + print("{0:.2f}".format(100.0 * j[1].comments / j[1].rows).rjust(19)) + else: + print(_(NO_COMMITED_FILES_TEXT) + ".") + + def output_csv(self): + authorinfo_list = self.changes.get_authorinfo_list() + total_changes = 0.0 + + #format.get_tag().split(',') + tagstr = format.get_tag() + if tagstr <> "": + tagstr += "," + + + 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(tagstr + "Author,Commits,Insertions,Deletions,% of changes,Author,Rows,Stability,Age,% in comments") + + for i,j in zip(sorted(authorinfo_list),sorted(self.blame.get_summed_blames().items())): + authorinfo = authorinfo_list.get(i) + percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 + line = i \ + + "," + str(authorinfo.commits) \ + + "," + str(authorinfo.insertions) \ + + "," + str(authorinfo.deletions) \ + + "," + str(percentage) \ + + "," + j[0] \ + + "," + str(j[1].rows) \ + + "," + str(Blame.get_stability(j[0], j[1].rows, self.changes)) \ + + "," + str(float(j[1].skew) / j[1].rows) \ + + "," + str(100.0 * j[1].comments / j[1].rows) + print(tagstr + line) + else: + print(_(NO_COMMITED_FILES_TEXT) + ".") + + def output_xml(self): + authorinfo_list = self.changes.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" + _(HISTORICAL_INFO_TEXT) + "\n" + changes_xml = "" + + for i in sorted(authorinfo_list): + author_email = self.changes.get_latest_email_by_author(i) + 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" + email_xml = "\t\t\t\t" + author_email + "\n" + gravatar_xml = "\t\t\t\t" + gravatar.get_url(author_email) + "\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 + email_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") + else: + print("\t\n\t\t" + _(NO_COMMITED_FILES_TEXT) + "\n\t") From 06238a3e562fcb0840533a757ce53fae5784323e Mon Sep 17 00:00:00 2001 From: zhangjianqiang Date: Tue, 11 Dec 2018 01:07:14 +0800 Subject: [PATCH 3/3] generate report from a config file generate report from a config file --- gitreport.py | 81 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 gitreport.py diff --git a/gitreport.py b/gitreport.py new file mode 100644 index 00000000..182693ec --- /dev/null +++ b/gitreport.py @@ -0,0 +1,81 @@ + +import os +import re +import sys +import getopt +import datetime + +helpinfo = ("""Usage: gitreport [OPTION]... + -r, --repo= a space separated list of git repositories + line formart likes: + tag repository since until + gitinspectory https://github.com/ejwa/gitinspector.git 2018-01-01 2018-12-01 + gitinspectorycvs https://github.com/de2sl2pfds/gitinspector.git 2018-01-01 2018-12-01 + -o, --output= output filename of csv format + -h, --help display this help and exit +""") + +def report(repo,output): + if not os.path.isfile(repo): + print("repo config file not exists") + sys.exit(-3) + if os.path.isfile(output): + output = output + "." + datetime.datetime.today().strftime('%Y%m%d%H%M%S') + print("output file exists. new file name is : " + output) + else: + print("output file name is : " + output) + os.system("> " + output) + i = 0 + for line in open(repo): + a = re.compile("\s+").split(line.strip()) + if a[0].lower().strip() == 'tag': + print("title: " + line.strip()) + continue + if len(a) > 3 and a[1].lower().find('.git') > -1: + print("No.[" + str(i) + "] : " + line.strip()) + print(" repository info: tag=" + a[0] + " since=" + a[2] + " until=" + a[3] + " gitrepo=" + a[1]) + cmd = ("python gitinspector.py --since="+a[2]+" --until="+a[3]+" --tag="+a[0]+" -F csv "+ a[1] + ">> " + output) + print(" command: " + cmd) + os.system(cmd) + i += 1 + print("") + + + if i < 1: + print("no repository found.") + os.unlink(output) + else: + print("processed " + str(i) + " repositories.") + +def main(argv): + repofile = "repo.txt" + outputfile = "report.csv" + + try: + opts, args = getopt.getopt(argv, "h:r:o:", + ["help=","repo=","output="]) + except getopt.GetoptError: + print helpinfo + sys.exit(-1) + if len(argv) == 0: + print helpinfo + sys.exit(-2) + + for o, a in opts: + if o in("-h", "--help"): + print(helpinfo) + sys.exit(0) + elif o in("-r", "--repo"): + repofile = a + elif o in("-o", "--output"): + outputfile = a + else: + print(helpinfo) + sys.exit(0) + report(repofile,outputfile) + +if __name__ == "__main__": + main(sys.argv[1:]) + + +