├── gitinspector ├── html │ ├── html.footer │ ├── flot.zip │ ├── gitinspector_piclet.png │ ├── jquery.tablesorter.min.js.zip │ └── html.header ├── translations │ ├── messages_de.mo │ ├── messages_es.mo │ ├── messages_fr.mo │ ├── messages_it.mo │ ├── messages_pl.mo │ ├── messages_sv.mo │ ├── messages_zh.mo │ └── messages.pot ├── __init__.py ├── output │ ├── __init__.py │ ├── outputable.py │ ├── extensionsoutput.py │ ├── filteringoutput.py │ ├── responsibilitiesoutput.py │ ├── blameoutput.py │ ├── metricsoutput.py │ ├── changesoutput.py │ └── timelineoutput.py ├── responsibilities.py ├── version.py ├── extensions.py ├── gravatar.py ├── interval.py ├── clone.py ├── basedir.py ├── optval.py ├── filtering.py ├── config.py ├── localization.py ├── timeline.py ├── comment.py ├── help.py ├── terminal.py ├── metrics.py ├── format.py ├── blame.py ├── gitinspector.py └── changes.py ├── docs ├── gitinspector.pdf ├── docbook-xsl.css ├── gitinspector.txt ├── gitinspector.1 └── gitinspector.html ├── MANIFEST.in ├── .gitignore ├── stdeb.cfg ├── .pylintrc ├── README.txt ├── gitinspector.js ├── tests ├── __init__.py ├── test_comment.py └── resources │ ├── commented_file.cpp │ └── commented_file.tex ├── gitinspector.py ├── DESCRIPTION.txt ├── setup.py ├── package.json ├── CHANGES.txt └── README.md /gitinspector/html/html.footer: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/gitinspector.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/docs/gitinspector.pdf -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include gitinspector/html/* 2 | include gitinspector/translations/* 3 | include *.txt 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | debian 3 | deb_dist 4 | dist 5 | node_modules 6 | *.egg-info 7 | *.pyc 8 | *.tgz 9 | -------------------------------------------------------------------------------- /gitinspector/html/flot.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/html/flot.zip -------------------------------------------------------------------------------- /gitinspector/translations/messages_de.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/translations/messages_de.mo -------------------------------------------------------------------------------- /gitinspector/translations/messages_es.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/translations/messages_es.mo -------------------------------------------------------------------------------- /gitinspector/translations/messages_fr.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/translations/messages_fr.mo -------------------------------------------------------------------------------- /gitinspector/translations/messages_it.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/translations/messages_it.mo -------------------------------------------------------------------------------- /gitinspector/translations/messages_pl.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/translations/messages_pl.mo -------------------------------------------------------------------------------- /gitinspector/translations/messages_sv.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/translations/messages_sv.mo -------------------------------------------------------------------------------- /gitinspector/translations/messages_zh.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/translations/messages_zh.mo -------------------------------------------------------------------------------- /gitinspector/html/gitinspector_piclet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/html/gitinspector_piclet.png -------------------------------------------------------------------------------- /stdeb.cfg: -------------------------------------------------------------------------------- 1 | [DEFAULT] 2 | Suite: testing 3 | Section: devel 4 | XS-Python-Version: >= 2.6 5 | Package: gitinspector 6 | Depends: git (>= 1.7) 7 | -------------------------------------------------------------------------------- /gitinspector/html/jquery.tablesorter.min.js.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ejwa/gitinspector/HEAD/gitinspector/html/jquery.tablesorter.min.js.zip -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [REPORTS] 2 | include-ids=yes 3 | comment=yes 4 | 5 | [MESSAGES CONTROL] 6 | disable=C0111,R0801,W0232,W0603,W0622,W0702,W0141 7 | 8 | 9 | [DESIGN] 10 | min-public-methods=0 11 | 12 | [FORMAT] 13 | max-line-length=130 14 | indent-string='\t' 15 | 16 | [VARIABLES] 17 | additional-builtins=_ 18 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | Copyright © 2012-2015 Ejwa Software. All rights reserved. 2 | 3 | This program comes with ABSOLUTELY NO WARRANTY. 4 | This is free software, and you are welcome to redistribute it under 5 | certain conditions; see the accompanying LICENSE.txt file for further details. 6 | 7 | For questions regarding gitinspector you can contact the current maintainer 8 | in charge at gitinspector@ejwa.se. 9 | 10 | To run gitinspector; please start it via the gitinspector.py script. Use 11 | the -h or --help flags to get help about available options. 12 | 13 | It is also possible to set gitinspector options using the "git config" 14 | command. Refer to the project page at https://github.com/ejwa/gitinspector 15 | for more information. 16 | -------------------------------------------------------------------------------- /gitinspector.js: -------------------------------------------------------------------------------- 1 | var PythonShell = require('python-shell'); 2 | 3 | var options = { 4 | // The main python script is in the same directory as this file 5 | scriptPath: __dirname, 6 | 7 | // Get command line arguments, skipping the default node args: 8 | // arg0 == node executable, arg1 == this file 9 | args: process.argv.slice(2) 10 | }; 11 | 12 | 13 | // Set encoding used by stdin etc manually. Without this, gitinspector may fail to run. 14 | process.env.PYTHONIOENCODING = 'utf8'; 15 | 16 | // Start inspector 17 | var inspector = new PythonShell('gitinspector.py', options); 18 | 19 | // Handle stdout 20 | inspector.on('message', function(message) { 21 | console.log(message); 22 | }); 23 | 24 | // Let the inspector run, catching any error at the end 25 | inspector.end(function (err) { 26 | if (err) { 27 | throw err; 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | # This file was intentionally left blank. 21 | -------------------------------------------------------------------------------- /gitinspector/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | # This file was intentionally left blank. 21 | -------------------------------------------------------------------------------- /gitinspector/output/__init__.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | # This file was intentionally left blank. 21 | -------------------------------------------------------------------------------- /gitinspector.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | # 4 | # Copyright © 2015 Ejwa Software. All rights reserved. 5 | # 6 | # This file is part of gitinspector. 7 | # 8 | # gitinspector is free software: you can redistribute it and/or modify 9 | # it under the terms of the GNU General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | # 13 | # gitinspector is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU General Public License for more details. 17 | # 18 | # You should have received a copy of the GNU General Public License 19 | # along with gitinspector. If not, see . 20 | 21 | from gitinspector import gitinspector 22 | 23 | if __name__ == "__main__": 24 | gitinspector.main() 25 | -------------------------------------------------------------------------------- /DESCRIPTION.txt: -------------------------------------------------------------------------------- 1 | gitnspector is a statistical analysis tool for git repositories. 2 | 3 | The defaut analysis shows general statistics per author, which can be 4 | complemented with a timeline analysis that shows the workload and activity of 5 | each author. Under normal operation, it filters the results to only show 6 | statistics about a number of given extensions and by default only includes 7 | source files in the statistical analysis. 8 | 9 | This tool was originally written to help fetch repository statistics from 10 | student projects in the course Object-oriented Programming Project 11 | (TDA367/DIT211) at Chalmers University of Technology and Gothenburg University. 12 | 13 | - Shows cumulative work by each author in the history. 14 | - Filters results by extension (default: java,c,cpp,h,hpp,py,glsl,rb,js,sql). 15 | - Can display a statistical timeline analysis. 16 | - Scans for all filetypes (by extension) found in the repository. 17 | - Multi-threaded; uses multiple instances of git to speed up analysis. 18 | - Supports HTML, XML and plain text (terminal) output. 19 | -------------------------------------------------------------------------------- /gitinspector/responsibilities.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | 23 | class ResponsibiltyEntry(object): 24 | blames = {} 25 | 26 | class Responsibilities(object): 27 | @staticmethod 28 | def get(blame, author_name): 29 | author_blames = {} 30 | 31 | for i in blame.blames.items(): 32 | if author_name == i[0][0]: 33 | total_rows = i[1].rows - i[1].comments 34 | if total_rows > 0: 35 | author_blames[i[0][1]] = total_rows 36 | 37 | return sorted(author_blames.items()) 38 | -------------------------------------------------------------------------------- /gitinspector/version.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | from . import localization 23 | localization.init() 24 | 25 | __version__ = "0.5.0dev" 26 | 27 | __doc__ = _("""Copyright © 2012-2015 Ejwa Software. All rights reserved. 28 | License GPLv3+: GNU GPL version 3 or later . 29 | This is free software: you are free to change and redistribute it. 30 | There is NO WARRANTY, to the extent permitted by law. 31 | 32 | Written by Adam Waldenberg.""") 33 | def output(): 34 | print("gitinspector {0}\n".format(__version__) + __doc__) 35 | -------------------------------------------------------------------------------- /gitinspector/extensions.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | 22 | DEFAULT_EXTENSIONS = ["java", "c", "cc", "cpp", "h", "hh", "hpp", "py", "glsl", "rb", "js", "sql"] 23 | 24 | __extensions__ = DEFAULT_EXTENSIONS 25 | __located_extensions__ = set() 26 | 27 | def get(): 28 | return __extensions__ 29 | 30 | def define(string): 31 | global __extensions__ 32 | __extensions__ = string.split(",") 33 | 34 | def add_located(string): 35 | if len(string) == 0: 36 | __located_extensions__.add("*") 37 | else: 38 | __located_extensions__.add(string) 39 | 40 | def get_located(): 41 | return __located_extensions__ 42 | -------------------------------------------------------------------------------- /gitinspector/gravatar.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013-2014 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import hashlib 22 | 23 | try: 24 | from urllib.parse import urlencode 25 | except: 26 | from urllib import urlencode 27 | 28 | from . import format 29 | 30 | def get_url(email, size=20): 31 | md5hash = hashlib.md5(email.encode("utf-8").lower().strip()).hexdigest() 32 | base_url = "https://www.gravatar.com/avatar/" + md5hash 33 | params = None 34 | 35 | if format.get_selected() == "html": 36 | params = {"default": "identicon", "size": size} 37 | elif format.get_selected() == "xml" or format.get_selected() == "json": 38 | params = {"default": "identicon"} 39 | 40 | return base_url + "?" + urlencode(params) 41 | -------------------------------------------------------------------------------- /gitinspector/interval.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | 22 | try: 23 | from shlex import quote 24 | except ImportError: 25 | from pipes import quote 26 | 27 | __since__ = "" 28 | 29 | __until__ = "" 30 | 31 | __ref__ = "HEAD" 32 | 33 | def has_interval(): 34 | return __since__ + __until__ != "" 35 | 36 | def get_since(): 37 | return __since__ 38 | 39 | def set_since(since): 40 | global __since__ 41 | __since__ = "--since=" + quote(since) 42 | 43 | def get_until(): 44 | return __until__ 45 | 46 | def set_until(until): 47 | global __until__ 48 | __until__ = "--until=" + quote(until) 49 | 50 | def get_ref(): 51 | return __ref__ 52 | 53 | def set_ref(ref): 54 | global __ref__ 55 | __ref__ = ref 56 | -------------------------------------------------------------------------------- /gitinspector/output/outputable.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2013 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | from .. import format 23 | 24 | class Outputable(object): 25 | def output_html(self): 26 | raise NotImplementedError(_("HTML output not yet supported in") + " \"" + self.__class__.__name__ + "\".") 27 | 28 | def output_json(self): 29 | raise NotImplementedError(_("JSON output not yet supported in") + " \"" + self.__class__.__name__ + "\".") 30 | 31 | def output_text(self): 32 | raise NotImplementedError(_("Text output not yet supported in") + " \"" + self.__class__.__name__ + "\".") 33 | 34 | def output_xml(self): 35 | raise NotImplementedError(_("XML output not yet supported in") + " \"" + self.__class__.__name__ + "\".") 36 | 37 | def output(outputable): 38 | if format.get_selected() == "html" or format.get_selected() == "htmlembedded": 39 | outputable.output_html() 40 | elif format.get_selected() == "json": 41 | outputable.output_json() 42 | elif format.get_selected() == "text": 43 | outputable.output_text() 44 | else: 45 | outputable.output_xml() 46 | -------------------------------------------------------------------------------- /tests/test_comment.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import os 22 | import sys 23 | import unittest2 24 | import gitinspector.comment 25 | 26 | def __test_extension__(commented_file, extension): 27 | base = os.path.dirname(os.path.realpath(__file__)) 28 | tex_file = open(base + commented_file, "r") 29 | tex = tex_file.readlines() 30 | tex_file.close() 31 | 32 | is_inside_comment = False 33 | comment_counter = 0 34 | for i in tex: 35 | i = i.decode("utf-8", "replace") 36 | (_, is_inside_comment) = gitinspector.comment.handle_comment_block(is_inside_comment, extension, i) 37 | if is_inside_comment or gitinspector.comment.is_comment(extension, i): 38 | comment_counter += 1 39 | 40 | return comment_counter 41 | 42 | class TexFileTest(unittest2.TestCase): 43 | def test(self): 44 | comment_counter = __test_extension__("/resources/commented_file.tex", "tex") 45 | self.assertEqual(comment_counter, 30) 46 | 47 | class CppFileTest(unittest2.TestCase): 48 | def test(self): 49 | comment_counter = __test_extension__("/resources/commented_file.cpp", "cpp") 50 | self.assertEqual(comment_counter, 25) 51 | -------------------------------------------------------------------------------- /gitinspector/clone.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2014 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import os 22 | import shutil 23 | import subprocess 24 | import sys 25 | import tempfile 26 | 27 | try: 28 | from urllib.parse import urlparse 29 | except: 30 | from urlparse import urlparse 31 | 32 | __cloned_paths__ = [] 33 | 34 | def create(url): 35 | class Repository(object): 36 | def __init__(self, name, location): 37 | self.name = name 38 | self.location = location 39 | 40 | parsed_url = urlparse(url) 41 | 42 | if parsed_url.scheme == "file" or parsed_url.scheme == "git" or parsed_url.scheme == "http" or \ 43 | parsed_url.scheme == "https" or parsed_url.scheme == "ssh": 44 | path = tempfile.mkdtemp(suffix=".gitinspector") 45 | git_clone = subprocess.Popen(["git", "clone", url, path], bufsize=1, stdout=sys.stderr) 46 | git_clone.wait() 47 | 48 | if git_clone.returncode != 0: 49 | sys.exit(git_clone.returncode) 50 | 51 | __cloned_paths__.append(path) 52 | return Repository(os.path.basename(parsed_url.path), path) 53 | 54 | return Repository(None, os.path.abspath(url)) 55 | 56 | def delete(): 57 | for path in __cloned_paths__: 58 | shutil.rmtree(path, ignore_errors=True) 59 | -------------------------------------------------------------------------------- /tests/resources/commented_file.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright © 2013 Ejwa Software. All rights reserved. 3 | * 4 | * This file is part of gitinspector. 5 | * 6 | * gitinspector is free software: you can redistribute it and/or modify 7 | * it under the terms of the GNU General Public License as published by 8 | * the Free Software Foundation, either version 3 of the License, or 9 | * (at your option) any later version. 10 | * 11 | * gitinspector is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU General Public License 17 | * along with gitinspector. If not, see . 18 | */ 19 | #include 20 | #include 21 | 22 | struct Dimensions { 23 | double width; 24 | double height; 25 | }; 26 | 27 | /* 28 | * A class for a rectangle 29 | */ 30 | class Rectangle { 31 | private: 32 | Dimensions dimensions; 33 | public: 34 | Rectangle(Dimensions dimensions); 35 | void print(); 36 | }; 37 | 38 | Rectangle::Rectangle(Dimensions dimensions) { 39 | this->dimensions = dimensions; 40 | } 41 | 42 | void Rectangle::print() { 43 | /* 44 | * Print some stuff (testing comments) 45 | */ 46 | std::cout << "\nCharacteristics of this rectangle"; 47 | std::cout << "\nWidth = " << this->dimensions.width; 48 | std::cout << "\nHeight = " << this->dimensions.height; 49 | std::cout << "\nArea = " << this->dimensions.width * this->dimensions.height << "\n"; // ^2 50 | } 51 | 52 | int main(int argc, char *argv[]) { 53 | Dimensions dimensions; 54 | 55 | std::cout << "Provide the dimensions of a rectangle\n"; 56 | std::cout << "Width: "; 57 | std::cin >> dimensions.width; 58 | std::cout << "Height: "; 59 | std::cin >> dimensions.height; 60 | 61 | // Create rectanlge and wait for user-input. 62 | Rectangle rectangle(dimensions); 63 | rectangle.print(); 64 | getchar(); 65 | 66 | return 0; 67 | } 68 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | import os 21 | import sys 22 | from gitinspector.version import __version__ 23 | from glob import glob 24 | from setuptools import setup, find_packages 25 | 26 | def read(fname): 27 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 28 | 29 | setup( 30 | name = "gitinspector", 31 | version = __version__, 32 | author = "Ejwa Software", 33 | author_email = "gitinspector@ejwa.se", 34 | description = ("A statistical analysis tool for git repositories."), 35 | license = "GNU GPL v3", 36 | keywords = "analysis analyzer git python statistics stats vc vcs timeline", 37 | url = "https://github.com/ejwa/gitinspector", 38 | long_description = read("DESCRIPTION.txt"), 39 | classifiers = [ 40 | "Development Status :: 4 - Beta", 41 | "Environment :: Console", 42 | "Intended Audience :: Developers", 43 | "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", 44 | "Topic :: Software Development :: Version Control", 45 | "Topic :: Utilities" 46 | ], 47 | packages = find_packages(exclude = ['tests']), 48 | package_data = {"": ["html/*", "translations/*"]}, 49 | data_files = [("share/doc/gitinspector", glob("*.txt"))], 50 | entry_points = {"console_scripts": ["gitinspector = gitinspector.gitinspector:main"]}, 51 | zip_safe = False 52 | ) 53 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitinspector", 3 | "version": "0.5.0-dev-1", 4 | "description": "Gitinspector is a statistical analysis tool for git repositories. The default analysis shows general statistics per author, which can be complemented with a timeline analysis that shows the workload and activity of each author.", 5 | "preferGlobal": true, 6 | "main": "gitinspector.py", 7 | "directories": { 8 | "doc": "docs", 9 | "test": "tests" 10 | }, 11 | "scripts": { 12 | "clean": "rimraf **/*.pyc", 13 | "crlf": "crlf --set=LF **/*.py", 14 | "prepublish": "npm run clean && npm run crlf", 15 | "release": "with-package git commit -am pkg.version && with-package git tag pkg.version && git push && npm publish && git push --tags", 16 | "release:beta": "npm run release && npm run tag:beta", 17 | "tag:beta": "with-package npm dist-tag add pkg.name@pkg.version beta", 18 | "test": "echo \"Error: no test specified\" && exit 1" 19 | }, 20 | "bin": { 21 | "gitinspector": "gitinspector.py" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/ejwa/gitinspector.git" 26 | }, 27 | "keywords": [ 28 | "git", 29 | "statistics", 30 | "stats", 31 | "analytics", 32 | "grading" 33 | ], 34 | "author": { 35 | "name": "Adam Waldenberg", 36 | "email": "adam.waldenberg@ejwa.se", 37 | "url": "https://github.com/adam-waldenberg" 38 | }, 39 | "contributors": [ 40 | "Agustín Cañas", 41 | "Bart van Andel ", 42 | "Bill Wang", 43 | "Christian Kastner", 44 | "Jiwon Kim", 45 | "Kamila Chyla", 46 | "Luca Motta", 47 | "Philipp Nowak", 48 | "Sergei Lomakov", 49 | "Yannick Moy" 50 | ], 51 | "license": "GPL-3.0", 52 | "bugs": { 53 | "url": "https://github.com/ejwa/gitinspector/issues" 54 | }, 55 | "homepage": "https://github.com/ejwa/gitinspector#readme", 56 | "devDependencies": { 57 | "crlf": "^1.1.0", 58 | "rimraf": "^2.5.4", 59 | "with-package": "^0.2.0" 60 | }, 61 | "dependencies": { 62 | "python-shell": "^0.4.0" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /gitinspector/basedir.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | import os 21 | import subprocess 22 | import sys 23 | 24 | def get_basedir(): 25 | if hasattr(sys, "frozen"): # exists when running via py2exe 26 | return sys.prefix 27 | else: 28 | return os.path.dirname(os.path.realpath(__file__)) 29 | 30 | def get_basedir_git(path=None): 31 | previous_directory = None 32 | 33 | if path != None: 34 | previous_directory = os.getcwd() 35 | os.chdir(path) 36 | 37 | bare_command = subprocess.Popen(["git", "rev-parse", "--is-bare-repository"], bufsize=1, 38 | stdout=subprocess.PIPE, stderr=open(os.devnull, "w")) 39 | 40 | isbare = bare_command.stdout.readlines() 41 | bare_command.wait() 42 | 43 | if bare_command.returncode != 0: 44 | sys.exit(_("Error processing git repository at \"%s\"." % os.getcwd())) 45 | 46 | isbare = (isbare[0].decode("utf-8", "replace").strip() == "true") 47 | absolute_path = None 48 | 49 | if isbare: 50 | absolute_path = subprocess.Popen(["git", "rev-parse", "--git-dir"], bufsize=1, stdout=subprocess.PIPE).stdout 51 | else: 52 | absolute_path = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], bufsize=1, 53 | stdout=subprocess.PIPE).stdout 54 | 55 | absolute_path = absolute_path.readlines() 56 | 57 | if len(absolute_path) == 0: 58 | sys.exit(_("Unable to determine absolute path of git repository.")) 59 | 60 | if path != None: 61 | os.chdir(previous_directory) 62 | 63 | return absolute_path[0].decode("utf-8", "replace").strip() 64 | -------------------------------------------------------------------------------- /gitinspector/optval.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import getopt 22 | 23 | class InvalidOptionArgument(Exception): 24 | def __init__(self, msg): 25 | super(InvalidOptionArgument, self).__init__(msg) 26 | self.msg = msg 27 | 28 | def __find_arg_in_options__(arg, options): 29 | for opt in options: 30 | if opt[0].find(arg) == 0: 31 | return opt 32 | 33 | return None 34 | 35 | def __find_options_to_extend__(long_options): 36 | options_to_extend = [] 37 | 38 | for num, arg in enumerate(long_options): 39 | arg = arg.split(":") 40 | if len(arg) == 2: 41 | long_options[num] = arg[0] + "=" 42 | options_to_extend.append(("--" + arg[0], arg[1])) 43 | 44 | return options_to_extend 45 | 46 | # This is a duplicate of gnu_getopt, but with support for optional arguments in long options, in the form; "arg:default_value". 47 | 48 | def gnu_getopt(args, options, long_options): 49 | options_to_extend = __find_options_to_extend__(long_options) 50 | 51 | for num, arg in enumerate(args): 52 | opt = __find_arg_in_options__(arg, options_to_extend) 53 | if opt: 54 | args[num] = arg + "=" + opt[1] 55 | 56 | return getopt.gnu_getopt(args, options, long_options) 57 | 58 | def get_boolean_argument(arg): 59 | if isinstance(arg, bool): 60 | return arg 61 | elif arg == None or arg.lower() == "false" or arg.lower() == "f" or arg == "0": 62 | return False 63 | elif arg.lower() == "true" or arg.lower() == "t" or arg == "1": 64 | return True 65 | 66 | raise InvalidOptionArgument(_("The given option argument is not a valid boolean.")) 67 | -------------------------------------------------------------------------------- /gitinspector/filtering.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import re 22 | import subprocess 23 | 24 | __filters__ = {"file": [set(), set()], "author": [set(), set()], "email": [set(), set()], "revision": [set(), set()], 25 | "message" : [set(), None]} 26 | 27 | class InvalidRegExpError(ValueError): 28 | def __init__(self, msg): 29 | super(InvalidRegExpError, self).__init__(msg) 30 | self.msg = msg 31 | 32 | def get(): 33 | return __filters__ 34 | 35 | def __add_one__(string): 36 | for i in __filters__: 37 | if (i + ":").lower() == string[0:len(i) + 1].lower(): 38 | __filters__[i][0].add(string[len(i) + 1:]) 39 | return 40 | __filters__["file"][0].add(string) 41 | 42 | def add(string): 43 | rules = string.split(",") 44 | for rule in rules: 45 | __add_one__(rule) 46 | 47 | def clear(): 48 | for i in __filters__: 49 | __filters__[i][0] = set() 50 | 51 | def get_filered(filter_type="file"): 52 | return __filters__[filter_type][1] 53 | 54 | def has_filtered(): 55 | for i in __filters__: 56 | if __filters__[i][1]: 57 | return True 58 | return False 59 | 60 | def __find_commit_message__(sha): 61 | git_show_r = subprocess.Popen(filter(None, ["git", "show", "-s", "--pretty=%B", "-w", sha]), bufsize=1, 62 | stdout=subprocess.PIPE).stdout 63 | 64 | commit_message = git_show_r.read() 65 | git_show_r.close() 66 | 67 | commit_message = commit_message.strip().decode("unicode_escape", "ignore") 68 | commit_message = commit_message.encode("latin-1", "replace") 69 | return commit_message.decode("utf-8", "replace") 70 | 71 | def set_filtered(string, filter_type="file"): 72 | string = string.strip() 73 | 74 | if len(string) > 0: 75 | for i in __filters__[filter_type][0]: 76 | search_for = string 77 | 78 | if filter_type == "message": 79 | search_for = __find_commit_message__(string) 80 | try: 81 | if re.search(i, search_for) != None: 82 | if filter_type == "message": 83 | __add_one__("revision:" + string) 84 | else: 85 | __filters__[filter_type][1].add(string) 86 | return True 87 | except: 88 | raise InvalidRegExpError(_("invalid regular expression specified")) 89 | return False 90 | -------------------------------------------------------------------------------- /gitinspector/config.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import os 22 | import subprocess 23 | from . import extensions, filtering, format, interval, optval 24 | 25 | class GitConfig(object): 26 | def __init__(self, run, repo, global_only=False): 27 | self.run = run 28 | self.repo = repo 29 | self.global_only = global_only 30 | 31 | def __read_git_config__(self, variable): 32 | previous_directory = os.getcwd() 33 | os.chdir(self.repo) 34 | setting = subprocess.Popen(filter(None, ["git", "config", "--global" if self.global_only else "", 35 | "inspector." + variable]), bufsize=1, stdout=subprocess.PIPE).stdout 36 | os.chdir(previous_directory) 37 | 38 | try: 39 | setting = setting.readlines()[0] 40 | setting = setting.decode("utf-8", "replace").strip() 41 | except IndexError: 42 | setting = "" 43 | 44 | return setting 45 | 46 | def __read_git_config_bool__(self, variable): 47 | try: 48 | variable = self.__read_git_config__(variable) 49 | return optval.get_boolean_argument(False if variable == "" else variable) 50 | except optval.InvalidOptionArgument: 51 | return False 52 | 53 | def __read_git_config_string__(self, variable): 54 | string = self.__read_git_config__(variable) 55 | return (True, string) if len(string) > 0 else (False, None) 56 | 57 | def read(self): 58 | var = self.__read_git_config_string__("file-types") 59 | if var[0]: 60 | extensions.define(var[1]) 61 | 62 | var = self.__read_git_config_string__("exclude") 63 | if var[0]: 64 | filtering.add(var[1]) 65 | 66 | var = self.__read_git_config_string__("format") 67 | if var[0] and not format.select(var[1]): 68 | raise format.InvalidFormatError(_("specified output format not supported.")) 69 | 70 | self.run.hard = self.__read_git_config_bool__("hard") 71 | self.run.list_file_types = self.__read_git_config_bool__("list-file-types") 72 | self.run.localize_output = self.__read_git_config_bool__("localize-output") 73 | self.run.metrics = self.__read_git_config_bool__("metrics") 74 | self.run.responsibilities = self.__read_git_config_bool__("responsibilities") 75 | self.run.useweeks = self.__read_git_config_bool__("weeks") 76 | 77 | var = self.__read_git_config_string__("since") 78 | if var[0]: 79 | interval.set_since(var[1]) 80 | 81 | var = self.__read_git_config_string__("until") 82 | if var[0]: 83 | interval.set_until(var[1]) 84 | 85 | self.run.timeline = self.__read_git_config_bool__("timeline") 86 | 87 | if self.__read_git_config_bool__("grading"): 88 | self.run.hard = True 89 | self.run.list_file_types = True 90 | self.run.metrics = True 91 | self.run.responsibilities = True 92 | self.run.timeline = True 93 | self.run.useweeks = True 94 | -------------------------------------------------------------------------------- /gitinspector/localization.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2013-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import gettext 23 | import locale 24 | import os 25 | import re 26 | import sys 27 | import time 28 | from . import basedir 29 | 30 | __enabled__ = False 31 | __installed__ = False 32 | __translation__ = None 33 | 34 | #Dummy function used to handle string constants 35 | def N_(message): 36 | return message 37 | 38 | def init(): 39 | global __enabled__ 40 | global __installed__ 41 | global __translation__ 42 | 43 | if not __installed__: 44 | try: 45 | locale.setlocale(locale.LC_ALL, "") 46 | except locale.Error: 47 | __translation__ = gettext.NullTranslations() 48 | else: 49 | lang = locale.getlocale() 50 | 51 | #Fix for non-POSIX-compliant systems (Windows et al.). 52 | if os.getenv('LANG') is None: 53 | lang = locale.getdefaultlocale() 54 | 55 | if lang[0]: 56 | os.environ['LANG'] = lang[0] 57 | 58 | if lang[0] is not None: 59 | filename = basedir.get_basedir() + "/translations/messages_%s.mo" % lang[0][0:2] 60 | 61 | try: 62 | __translation__ = gettext.GNUTranslations(open(filename, "rb")) 63 | except IOError: 64 | __translation__ = gettext.NullTranslations() 65 | else: 66 | print("WARNING: Localization disabled because the system language could not be determined.", file=sys.stderr) 67 | __translation__ = gettext.NullTranslations() 68 | 69 | __enabled__ = True 70 | __installed__ = True 71 | __translation__.install(True) 72 | 73 | def check_compatibility(version): 74 | if isinstance(__translation__, gettext.GNUTranslations): 75 | header_pattern = re.compile("^([^:\n]+): *(.*?) *$", re.MULTILINE) 76 | header_entries = dict(header_pattern.findall(_(""))) 77 | 78 | if header_entries["Project-Id-Version"] != "gitinspector {0}".format(version): 79 | print("WARNING: The translation for your system locale is not up to date with the current gitinspector " 80 | "version. The current maintainer of this locale is {0}.".format(header_entries["Last-Translator"]), 81 | file=sys.stderr) 82 | 83 | def get_date(): 84 | if __enabled__ and isinstance(__translation__, gettext.GNUTranslations): 85 | date = time.strftime("%x") 86 | 87 | if hasattr(date, 'decode'): 88 | date = date.decode("utf-8", "replace") 89 | 90 | return date 91 | else: 92 | return time.strftime("%Y/%m/%d") 93 | 94 | def enable(): 95 | if isinstance(__translation__, gettext.GNUTranslations): 96 | __translation__.install(True) 97 | 98 | global __enabled__ 99 | __enabled__ = True 100 | 101 | def disable(): 102 | global __enabled__ 103 | __enabled__ = False 104 | 105 | if __installed__: 106 | gettext.NullTranslations().install(True) 107 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | gitinspector (0.4.4) 2 | 3 | Minor release with some minor fixes. 4 | 5 | * Better support for additional terminals. 6 | * -f "**" now properly ignores binary files. 7 | 8 | gitinspector (0.4.3) 9 | 10 | Minor release with updated language locales. 11 | 12 | 13 | gitinspector (0.4.2) 14 | 15 | Minor release with many small, but great improvements. 16 | 17 | * The changes module is now threaded and uses all available cores. This 18 | results in a significant speed up. 19 | * Printed extensions are now alphabetically sorted. 20 | * Ability to exclude commits with certain comments from statistics. 21 | * Support for C# code and comments. 22 | * Extensionless files can now be included in statistics. 23 | * Ability to include all file extensions by specifying "**". 24 | * Spanish translation. 25 | 26 | 27 | gitinspector (0.4.1) 28 | 29 | Maintenance release fixing several issues found in the previous version. 30 | 31 | 32 | gitinspector (0.4.0) 33 | 34 | Major update with many fixes and new features. 35 | 36 | * Support for remote repositories (git://, http(s)://, ssh://). 37 | * Code stability and age shown in blame output. 38 | * Support for cyclomatic complexity and cyclomatic complexity density 39 | metrics. 40 | * Ability to exclude commit hashes (revisions) from statistics. 41 | * Improved HTML output. 42 | * Support for ADA, OCaml and Haskell comments. 43 | * Improved localization support for languages with multi-column 44 | characters. 45 | * Many (big and small) bug fixes. 46 | * French translation. 47 | * German translation. 48 | 49 | 50 | gitinspector (0.3.2) 51 | 52 | Maintenance release that adds several new features and bug fixes. 53 | 54 | * Better handling of terminals that have no encoding configured. 55 | * File extensions .cc and .hh scanned by default. 56 | * Support for bare repositories. 57 | * Support for comments in .xhtml, .jspx, and .scala files. 58 | * The ability to filter out statistics from specific authors or 59 | emails. 60 | * The flag --since now behaves correctly. 61 | * Italian translation. 62 | * Polish translation. 63 | 64 | 65 | gitinspector (0.3.1) 66 | 67 | Minor release which adds support for gravatars. 68 | 69 | * A few minor bug fixes. 70 | * Chinese translation. 71 | * Support for gravatars in the HTML and XML outputs. 72 | * Improved responsibilities section in the HTML output. 73 | * Debian package now properly depends on git. 74 | 75 | 76 | gitinspector (0.3.0) 77 | 78 | This is a major release with many new features. 79 | 80 | * Support for comments in LaTex (.tex) and PHP files. 81 | * Many bugfixes. 82 | * Localization support (English and Swedish for now). 83 | * Support for setting gitinspector options via git-config. 84 | * Support for regular expressions in conjunction with -x/--exclude. 85 | * New output format; "htmlembedded". 86 | * Support for Python packaging via setuptools. 87 | * Improved HTML output with new JQuery and flot versions. 88 | 89 | 90 | gitinspector (0.2.2) 91 | 92 | This is a maintenance release that fixes several bugs; many related to 93 | character encoding. This version also adds table sorting to the HTML 94 | output. 95 | 96 | 97 | gitinspector (0.2.1) 98 | 99 | Maintenance release that fixes compatibility with Python 3 and 100 | introduces the --since and --until interval flags. 101 | 102 | 103 | gitinspector (0.2.0) 104 | 105 | This release includes several bug fixes and introduces HTML and XML 106 | output formats. 107 | 108 | 109 | gitinspector (0.1.0) 110 | 111 | First public release. While incomplete, this release fulfills the most 112 | important feature requirements that were originally outlined. 113 | -------------------------------------------------------------------------------- /gitinspector/timeline.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import datetime 22 | 23 | class TimelineData(object): 24 | def __init__(self, changes, useweeks): 25 | authordateinfo_list = sorted(changes.get_authordateinfo_list().items()) 26 | self.changes = changes 27 | self.entries = {} 28 | self.total_changes_by_period = {} 29 | self.useweeks = useweeks 30 | 31 | for i in authordateinfo_list: 32 | key = None 33 | 34 | if useweeks: 35 | yearweek = datetime.date(int(i[0][0][0:4]), int(i[0][0][5:7]), int(i[0][0][8:10])).isocalendar() 36 | key = (i[0][1], str(yearweek[0]) + "W" + "{0:02d}".format(yearweek[1])) 37 | else: 38 | key = (i[0][1], i[0][0][0:7]) 39 | 40 | if self.entries.get(key, None) == None: 41 | self.entries[key] = i[1] 42 | else: 43 | self.entries[key].insertions += i[1].insertions 44 | self.entries[key].deletions += i[1].deletions 45 | 46 | for period in self.get_periods(): 47 | total_insertions = 0 48 | total_deletions = 0 49 | 50 | for author in self.get_authors(): 51 | entry = self.entries.get((author[0], period), None) 52 | if entry != None: 53 | total_insertions += entry.insertions 54 | total_deletions += entry.deletions 55 | 56 | self.total_changes_by_period[period] = (total_insertions, total_deletions, 57 | total_insertions + total_deletions) 58 | 59 | def get_periods(self): 60 | return sorted(set([i[1] for i in self.entries])) 61 | 62 | def get_total_changes_in_period(self, period): 63 | return self.total_changes_by_period[period] 64 | 65 | def get_authors(self): 66 | return sorted(set([(i[0][0], self.changes.get_latest_email_by_author(i[0][0])) for i in self.entries.items()])) 67 | 68 | def get_author_signs_in_period(self, author, period, multiplier): 69 | authorinfo = self.entries.get((author, period), None) 70 | total = float(self.total_changes_by_period[period][2]) 71 | 72 | if authorinfo: 73 | i = multiplier * (self.entries[(author, period)].insertions / total) 74 | j = multiplier * (self.entries[(author, period)].deletions / total) 75 | return (int(i), int(j)) 76 | else: 77 | return (0, 0) 78 | 79 | def get_multiplier(self, period, max_width): 80 | multiplier = 0 81 | 82 | while True: 83 | for i in self.entries: 84 | entry = self.entries.get(i) 85 | 86 | if period == i[1]: 87 | changes_in_period = float(self.total_changes_by_period[i[1]][2]) 88 | if multiplier * (entry.insertions + entry.deletions) / changes_in_period > max_width: 89 | return multiplier 90 | 91 | multiplier += 0.25 92 | 93 | def is_author_in_period(self, period, author): 94 | return self.entries.get((author, period), None) != None 95 | 96 | def is_author_in_periods(self, periods, author): 97 | for period in periods: 98 | if self.is_author_in_period(period, author): 99 | return True 100 | return False 101 | -------------------------------------------------------------------------------- /gitinspector/comment.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | 22 | __comment_begining__ = {"java": "/*", "c": "/*", "cc": "/*", "cpp": "/*", "cs": "/*", "h": "/*", "hh": "/*", "hpp": "/*", 23 | "hs": "{-", "html": "", "php": "*/", "py": "\"\"\"", "glsl": "*/", "rb": "=end", "js": "*/", "jspx": "-->", 29 | "scala": "*/", "sql": "*/", "tex": "\\end{comment}", "xhtml": "-->", "xml": "-->", "ml": "*)", "mli": "*)", 30 | "go": "*/", "ly": "%}", "ily": "%}"} 31 | 32 | __comment__ = {"java": "//", "c": "//", "cc": "//", "cpp": "//", "cs": "//", "h": "//", "hh": "//", "hpp": "//", "hs": "--", 33 | "pl": "#", "php": "//", "py": "#", "glsl": "//", "rb": "#", "robot": "#", "rs": "//", "rlib": "//", "js": "//", 34 | "scala": "//", "sql": "--", "tex": "%", "ada": "--", "ads": "--", "adb": "--", "pot": "#", "po": "#", "go": "//", 35 | "ly": "%", "ily": "%"} 36 | 37 | __comment_markers_must_be_at_begining__ = {"tex": True} 38 | 39 | def __has_comment_begining__(extension, string): 40 | if __comment_markers_must_be_at_begining__.get(extension, None) == True: 41 | return string.find(__comment_begining__[extension]) == 0 42 | elif __comment_begining__.get(extension, None) != None and string.find(__comment_end__[extension], 2) == -1: 43 | return string.find(__comment_begining__[extension]) != -1 44 | 45 | return False 46 | 47 | def __has_comment_end__(extension, string): 48 | if __comment_markers_must_be_at_begining__.get(extension, None) == True: 49 | return string.find(__comment_end__[extension]) == 0 50 | elif __comment_end__.get(extension, None) != None: 51 | return string.find(__comment_end__[extension]) != -1 52 | 53 | return False 54 | 55 | def is_comment(extension, string): 56 | if __comment_begining__.get(extension, None) != None and string.strip().startswith(__comment_begining__[extension]): 57 | return True 58 | if __comment_end__.get(extension, None) != None and string.strip().endswith(__comment_end__[extension]): 59 | return True 60 | if __comment__.get(extension, None) != None and string.strip().startswith(__comment__[extension]): 61 | return True 62 | 63 | return False 64 | 65 | def handle_comment_block(is_inside_comment, extension, content): 66 | comments = 0 67 | 68 | if is_comment(extension, content): 69 | comments += 1 70 | if is_inside_comment: 71 | if __has_comment_end__(extension, content): 72 | is_inside_comment = False 73 | else: 74 | comments += 1 75 | elif __has_comment_begining__(extension, content) and not __has_comment_end__(extension, content): 76 | is_inside_comment = True 77 | 78 | return (comments, is_inside_comment) 79 | -------------------------------------------------------------------------------- /gitinspector/output/extensionsoutput.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import textwrap 23 | from ..localization import N_ 24 | from .. import extensions, terminal 25 | from .outputable import Outputable 26 | 27 | 28 | EXTENSIONS_INFO_TEXT = N_("The extensions below were found in the repository history") 29 | EXTENSIONS_MARKED_TEXT = N_("(extensions used during statistical analysis are marked)") 30 | 31 | class ExtensionsOutput(Outputable): 32 | @staticmethod 33 | def is_marked(extension): 34 | if extension in extensions.__extensions__ or "**" in extensions.__extensions__: 35 | return True 36 | 37 | return False 38 | 39 | def output_html(self): 40 | if extensions.__located_extensions__: 41 | extensions_xml = "
" 42 | extensions_xml += "

{0} {1}.

".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT)) 43 | 44 | for i in sorted(extensions.__located_extensions__): 45 | if ExtensionsOutput.is_marked(i): 46 | extensions_xml += "" + i + "" 47 | else: 48 | extensions_xml += i 49 | extensions_xml += " " 50 | 51 | extensions_xml += "

" 52 | print(extensions_xml) 53 | 54 | def output_json(self): 55 | if extensions.__located_extensions__: 56 | message_json = "\t\t\t\"message\": \"" + _(EXTENSIONS_INFO_TEXT) + "\",\n" 57 | used_extensions_json = "" 58 | unused_extensions_json = "" 59 | 60 | for i in sorted(extensions.__located_extensions__): 61 | if ExtensionsOutput.is_marked(i): 62 | used_extensions_json += "\"" + i + "\", " 63 | else: 64 | unused_extensions_json += "\"" + i + "\", " 65 | 66 | used_extensions_json = used_extensions_json[:-2] 67 | unused_extensions_json = unused_extensions_json[:-2] 68 | 69 | print(",\n\t\t\"extensions\": {\n" + message_json + "\t\t\t\"used\": [ " + used_extensions_json + 70 | " ],\n\t\t\t\"unused\": [ " + unused_extensions_json + " ]\n" + "\t\t}", end="") 71 | 72 | def output_text(self): 73 | if extensions.__located_extensions__: 74 | print("\n" + textwrap.fill("{0} {1}:".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT)), 75 | width=terminal.get_size()[0])) 76 | 77 | for i in sorted(extensions.__located_extensions__): 78 | if ExtensionsOutput.is_marked(i): 79 | print("[" + terminal.__bold__ + i + terminal.__normal__ + "]", end=" ") 80 | else: 81 | print (i, end=" ") 82 | print("") 83 | 84 | def output_xml(self): 85 | if extensions.__located_extensions__: 86 | message_xml = "\t\t" + _(EXTENSIONS_INFO_TEXT) + "\n" 87 | used_extensions_xml = "" 88 | unused_extensions_xml = "" 89 | 90 | for i in sorted(extensions.__located_extensions__): 91 | if ExtensionsOutput.is_marked(i): 92 | used_extensions_xml += "\t\t\t" + i + "\n" 93 | else: 94 | unused_extensions_xml += "\t\t\t" + i + "\n" 95 | 96 | print("\t\n" + message_xml + "\t\t\n" + used_extensions_xml + "\t\t\n" + 97 | "\t\t\n" + unused_extensions_xml + "\t\t\n" + "\t") 98 | -------------------------------------------------------------------------------- /tests/resources/commented_file.tex: -------------------------------------------------------------------------------- 1 | % Copyright © 2013 Ejwa Software. All rights reserved. 2 | % 3 | % This file is part of gitinspector. 4 | % 5 | % gitinspector is free software: you can redistribute it and/or modify 6 | % it under the terms of the GNU General Public License as published by 7 | % the Free Software Foundation, either version 3 of the License, or 8 | % (at your option) any later version. 9 | % 10 | % gitinspector is distributed in the hope that it will be useful, 11 | % but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | % GNU General Public License for more details. 14 | % 15 | % You should have received a copy of the GNU General Public License 16 | % along with gitinspector. If not, see . 17 | 18 | \documentclass{article} 19 | \usepackage{verbatim} 20 | 21 | \begin{document} 22 | \title{A test for gitinspector} 23 | \author{Ejwa Software} 24 | \maketitle 25 | 26 | \begin{abstract} 27 | Lorem ipsum dolor sit amet, qui morbi a nunc purus, turpis netus et, 28 | faucibus amet porttitor commodo luctus convallis sit, non sociosqu ut felis fusce eros, 29 | mi scelerisque vel tincidunt vivamus aliquet. Urna et commodo ut ipsum integer, 30 | eu nostra etiam lacinia odio praesent quis, facilisi odio, donec erat nunc dictumst et. 31 | Inceptos turpis vestibulum volutpat quis nulla elit, eros dictumst, duis rhoncus sem eget eget diam ultrices. 32 | Pharetra sed aliquam in, ultricies suscipit bibendum vivamus, quo amet. 33 | In nisl pellentesque mus suscipit magna, sem interdum dolores in, dolores risus at aliquet nisl, 34 | tincidunt faucibus. 35 | \end{abstract} 36 | 37 | \section{Introduction} 38 | Sit eros et arcu molestie suspendisse elit, suscipit maecenas quisque purus curabitur vitae suspendisse, 39 | vestibulum nunc morbi viverra id aliquam dolor. Nulla leo lacus id sem, iaculis metus faucibus lorem imperdiet a duis, 40 | ullamcorper dis condimentum diam, ullamcorper massa, vel ac venenatis nisl vitae. Sit in mauris. Lacus mollis quam, 41 | nec in, pellentesque integer nulla sit in sit, erat dolor a lectus dui, sed purus. 42 | Conubia ornare ridiculus interdum duis ut nisl, mauris semper rhoncus. Molestie leo diam lectus non dolor, 43 | adipiscing ut potenti aliquam suscipit torquent, vulputate sit vel cum. Felis et wisi et cras id, 44 | amet cum duis quis magnis urna, egestas et dui tortor, diam tellus. Elit auctor in at, urna nulla posuere, 45 | et blandit sodales porta felis, posuere venenatis. Vel hac neque dictum rhoncus. 46 | 47 | \begin{comment} 48 | Pellentesque vestibulum id, tincidunt blandit. Condimentum sed egestas in sit, 49 | a mattis in in purus erat sit. Sed turpis vestibulum aliquet, turpis ornare ultrices mi, 50 | nisl nulla dis luctus, nec quis. Luctus turpis neque arcu ante donec dui, mauris vel duis id proin consequat, 51 | provident duis integer feugiat blandit neque. Donec lectus sollicitudin at, 52 | \begin{comment} 53 | imperdiet nec elit in dolor lectus platea, ultricies pharetra vel etiam in odio, porta nascetur aenean, 54 | imperdiet nunc libero sit. Pellentesque tincidunt molestie, neque vivamus neque nec accumsan porttitor varius, 55 | \end{comment} % Testing nested comment behaviour 56 | vitae lacinia et molestie integer in vitae, elementum dolor vitae tempor sit, nunc justo praesent. 57 | Facilisis metus scelerisque sit pharetra, dictumst dui incididunt donec eu. 58 | \end{comment} 59 | 60 | \section{Conclusion} 61 | Nec proin cursus consequat consequatur ridiculus mauris, ultrices erat lacus tincidunt velit, 62 | laoreet egestas congue odio vestibulum sollicitudin. Dui luctus malesuada ac velit nec, sit ac, sed dolor vitae in. 63 | Aliquam maecenas, amet sem vitae nulla, nisl eros eleifend, commodo facilisi amet faucibus ut. 64 | Lorem urna est consectetuer mattis lorem, ultricies interdum quam urna et amet turpis. 65 | Ac condimentum leo dolor pellentesque, eu velit massa dictumst cubilia praesent curabitur, est varius, 66 | orci fringilla tortor ut non in. Aliquam sed curabitur eleifend, odio varius et eros dictum et, 67 | %pede vestibulum ut vel. Wisi ipsum potenti dictumst in congue. Feugiat nam nunc cras amet tellus eget, 68 | %cursus fringilla quis dui odio elit turpis, eu felis ac nec diam nibh, facilisis risus vel turpis risus. 69 | Ornare vestibulum diam facilisis porttitor nulla. Penatibus in lacus ac tempus in iaculis, 70 | praesent pede nunc pretium sodales, quisque felis, at vehicula sit, tempor vel lectus arcu porta integer cum. 71 | 72 | \end{document} 73 | -------------------------------------------------------------------------------- /gitinspector/help.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import sys 23 | from .extensions import DEFAULT_EXTENSIONS 24 | from .format import __available_formats__ 25 | 26 | 27 | __doc__ = _("""Usage: {0} [OPTION]... [REPOSITORY]... 28 | List information about the repository in REPOSITORY. If no repository is 29 | specified, the current directory is used. If multiple repositories are 30 | given, information will be merged into a unified statistical report. 31 | 32 | Mandatory arguments to long options are mandatory for short options too. 33 | Boolean arguments can only be given to long options. 34 | -f, --file-types=EXTENSIONS a comma separated list of file extensions to 35 | include when computing statistics. The 36 | default extensions used are: 37 | {1} 38 | Specifying * includes files with no 39 | extension, while ** includes all files 40 | -F, --format=FORMAT define in which format output should be 41 | generated; the default format is 'text' and 42 | the available formats are: 43 | {2} 44 | --grading[=BOOL] show statistics and information in a way that 45 | is formatted for grading of student 46 | projects; this is the same as supplying the 47 | options -HlmrTw 48 | -H, --hard[=BOOL] track rows and look for duplicates harder; 49 | this can be quite slow with big repositories 50 | -l, --list-file-types[=BOOL] list all the file extensions available in the 51 | current branch of the repository 52 | -L, --localize-output[=BOOL] localize the generated output to the selected 53 | system language if a translation is 54 | available 55 | -m --metrics[=BOOL] include checks for certain metrics during the 56 | analysis of commits 57 | -r --responsibilities[=BOOL] show which files the different authors seem 58 | most responsible for 59 | --since=DATE only show statistics for commits more recent 60 | than a specific date 61 | -T, --timeline[=BOOL] show commit timeline, including author names 62 | --until=DATE only show statistics for commits older than a 63 | specific date 64 | -w, --weeks[=BOOL] show all statistical information in weeks 65 | instead of in months 66 | -x, --exclude=PATTERN an exclusion pattern describing the file 67 | paths, revisions, revisions with certain 68 | commit messages, author names or author 69 | emails that should be excluded from the 70 | statistics; can be specified multiple times 71 | -h, --help display this help and exit 72 | --version output version information and exit 73 | 74 | gitinspector will filter statistics to only include commits that modify, 75 | add or remove one of the specified extensions, see -f or --file-types for 76 | more information. 77 | 78 | gitinspector requires that the git executable is available in your PATH. 79 | Report gitinspector bugs to gitinspector@ejwa.se.""") 80 | 81 | def output(): 82 | print(__doc__.format(sys.argv[0], ",".join(DEFAULT_EXTENSIONS), ",".join(__available_formats__))) 83 | -------------------------------------------------------------------------------- /gitinspector/terminal.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | import codecs 22 | import os 23 | import platform 24 | import sys 25 | import unicodedata 26 | 27 | __bold__ = "\033[1m" 28 | __normal__ = "\033[0;0m" 29 | 30 | DEFAULT_TERMINAL_SIZE = (80, 25) 31 | 32 | def __get_size_windows__(): 33 | res = None 34 | try: 35 | from ctypes import windll, create_string_buffer 36 | 37 | handler = windll.kernel32.GetStdHandle(-12) # stderr 38 | csbi = create_string_buffer(22) 39 | res = windll.kernel32.GetConsoleScreenBufferInfo(handler, csbi) 40 | except: 41 | return DEFAULT_TERMINAL_SIZE 42 | 43 | if res: 44 | import struct 45 | (_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack("hhhhHhhhhhh", csbi.raw) 46 | sizex = right - left + 1 47 | sizey = bottom - top + 1 48 | return sizex, sizey 49 | else: 50 | return DEFAULT_TERMINAL_SIZE 51 | 52 | def __get_size_linux__(): 53 | def ioctl_get_window_size(file_descriptor): 54 | try: 55 | import fcntl, termios, struct 56 | size = struct.unpack('hh', fcntl.ioctl(file_descriptor, termios.TIOCGWINSZ, "1234")) 57 | except: 58 | return DEFAULT_TERMINAL_SIZE 59 | 60 | return size 61 | 62 | size = ioctl_get_window_size(0) or ioctl_get_window_size(1) or ioctl_get_window_size(2) 63 | 64 | if not size: 65 | try: 66 | file_descriptor = os.open(os.ctermid(), os.O_RDONLY) 67 | size = ioctl_get_window_size(file_descriptor) 68 | os.close(file_descriptor) 69 | except: 70 | pass 71 | if not size: 72 | try: 73 | size = (os.environ["LINES"], os.environ["COLUMNS"]) 74 | except: 75 | return DEFAULT_TERMINAL_SIZE 76 | 77 | return int(size[1]), int(size[0]) 78 | 79 | def clear_row(): 80 | print("\r", end="") 81 | 82 | def skip_escapes(skip): 83 | if skip: 84 | global __bold__ 85 | global __normal__ 86 | __bold__ = "" 87 | __normal__ = "" 88 | 89 | def printb(string): 90 | print(__bold__ + string + __normal__) 91 | 92 | def get_size(): 93 | width = 0 94 | height = 0 95 | 96 | if sys.stdout.isatty(): 97 | current_os = platform.system() 98 | 99 | if current_os == "Windows": 100 | (width, height) = __get_size_windows__() 101 | elif current_os == "Linux" or current_os == "Darwin" or current_os.startswith("CYGWIN"): 102 | (width, height) = __get_size_linux__() 103 | 104 | if width > 0: 105 | return (width, height) 106 | 107 | return DEFAULT_TERMINAL_SIZE 108 | 109 | def set_stdout_encoding(): 110 | if not sys.stdout.isatty() and sys.version_info < (3,): 111 | sys.stdout = codecs.getwriter("utf-8")(sys.stdout) 112 | 113 | def set_stdin_encoding(): 114 | if not sys.stdin.isatty() and sys.version_info < (3,): 115 | sys.stdin = codecs.getreader("utf-8")(sys.stdin) 116 | 117 | def convert_command_line_to_utf8(): 118 | try: 119 | argv = [] 120 | 121 | for arg in sys.argv: 122 | argv.append(arg.decode(sys.stdin.encoding, "replace")) 123 | 124 | return argv 125 | except AttributeError: 126 | return sys.argv 127 | 128 | def check_terminal_encoding(): 129 | if sys.stdout.isatty() and (sys.stdout.encoding == None or sys.stdin.encoding == None): 130 | print(_("WARNING: The terminal encoding is not correctly configured. gitinspector might malfunction. " 131 | "The encoding can be configured with the environment variable 'PYTHONIOENCODING'."), file=sys.stderr) 132 | 133 | def get_excess_column_count(string): 134 | width_mapping = {'F': 2, 'H': 1, 'W': 2, 'Na': 1, 'N': 1, 'A': 1} 135 | result = 0 136 | 137 | for i in string: 138 | width = unicodedata.east_asian_width(i) 139 | result += width_mapping[width] 140 | 141 | return result - len(string) 142 | 143 | def ljust(string, pad): 144 | return string.ljust(pad - get_excess_column_count(string)) 145 | 146 | def rjust(string, pad): 147 | return string.rjust(pad - get_excess_column_count(string)) 148 | 149 | def output_progress(text, pos, length): 150 | if sys.stdout.isatty(): 151 | (width, _unused) = get_size() 152 | progress_text = text.format(100 * pos / length) 153 | 154 | if len(progress_text) > width: 155 | progress_text = "...%s" % progress_text[-width+3:] 156 | 157 | print("\r{0}\r{1}".format(" " * width, progress_text), end="") 158 | sys.stdout.flush() 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Latest release](https://img.shields.io/github/release/ejwa/gitinspector.svg?style=flat-square)](https://github.com/ejwa/gitinspector/releases/latest) 2 | [![License](https://img.shields.io/github/license/ejwa/gitinspector.svg?style=flat-square)](https://github.com/ejwa/gitinspector/blob/master/LICENSE.txt) 3 |

4 | 6 |  About Gitinspector 7 |

8 | 9 | Gitinspector is a statistical analysis tool for git repositories. The default analysis shows general statistics per author, which can be complemented with a timeline analysis that shows the workload and activity of each author. Under normal operation, it filters the results to only show statistics about a number of given extensions and by default only includes source files in the statistical analysis. 10 | 11 | This tool was originally written to help fetch repository statistics from student projects in the course Object-oriented Programming Project (TDA367/DIT211) at Chalmers University of Technology and Gothenburg University. 12 | 13 | Today, gitinspector is used as a grading aid by universities worldwide. 14 | 15 | A full [Documentation](https://github.com/ejwa/gitinspector/wiki/Documentation) of the usage and available options of gitinspector is available on the wiki. For help on the most common questions, please refer to the [FAQ](https://github.com/ejwa/gitinspector/wiki/FAQ) document. 16 | 17 | ### Some of the features 18 | * Shows cumulative work by each author in the history. 19 | * Filters results by extension (default: java,c,cc,cpp,h,hh,hpp,py,glsl,rb,js,sql). 20 | * Can display a statistical timeline analysis. 21 | * Scans for all filetypes (by extension) found in the repository. 22 | * Multi-threaded; uses multiple instances of git to speed up analysis when possible. 23 | * Supports HTML, JSON, XML and plain text output (console). 24 | * Can report violations of different code metrics. 25 | 26 | ### Example outputs 27 | Below are some example outputs for a number of famous open source projects. All the statistics were generated using the *"-HTlrm"* flags. 28 | 29 | | Project name | | | | | 30 | |---|---|---|---|---| 31 | | Django | [HTML](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/django_output.html) | [HTML Embedded](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/django_output.emb.html) | [Plain Text](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/django_output.txt) | [XML](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/django_output.xml) | 32 | | JQuery | [HTML](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/jquery_output.html) | [HTML Embedded](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/jquery_output.emb.html) | [Plain Text](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/jquery_output.txt) | [XML](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/jquery_output.xml) | 33 | | Pango | [HTML](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/pango_output.html) | [HTML Embedded](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/pango_output.emb.html) | [Plain Text](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/pango_output.txt) | [XML](http://githubproxy.ejwa.se/wiki/ejwa/gitinspector/examples/pango_output.xml) | 34 | 35 | ### The Team 36 | * Adam Waldenberg, Lead maintainer and Swedish translation 37 | * Agustín Cañas, Spanish translation 38 | * Bart van Andel, npm package maintainer 39 | * Bill Wang, Chinese translation 40 | * Christian Kastner, Debian package maintainer 41 | * Jiwon Kim, Korean translation 42 | * Kamila Chyla, Polish translation 43 | * Luca Motta, Italian translation 44 | * Philipp Nowak, German translation 45 | * Sergei Lomakov, Russian translation 46 | * Yannick Moy, French translation 47 | 48 | *We need translations for gitinspector!* If you are a gitinspector user, feel willing to help and have good language skills in any unsupported language we urge you to contact us. We also happily accept code patches. Please refer to [Contributing](https://github.com/ejwa/gitinspector/wiki/Contributing) for more information on how to contribute to the project. 49 | 50 | ### Packages 51 | The Debian packages offered with releases of gitinspector are unofficial and very simple packages generated with [stdeb](https://github.com/astraw/stdeb). Christian Kastner is maintaining the official Debian packages. You can check the current status on the [Debian Package Tracker](https://tracker.debian.org/pkg/gitinspector). Consequently, there are official packages for many Debian based distributions installable via *apt-get*. 52 | 53 | An [npm](https://npmjs.com) package is provided for convenience as well. To install it globally, execute `npm i -g gitinspector`. 54 | 55 | ### License 56 | gitinspector is licensed under the *GNU GPL v3*. The gitinspector logo is partly based on the git logo; based on the work of Jason Long. The logo is licensed under the *Creative Commons Attribution 3.0 Unported License*. 57 | -------------------------------------------------------------------------------- /gitinspector/metrics.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2017 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import unicode_literals 21 | import re 22 | import subprocess 23 | from .changes import FileDiff 24 | from . import comment, filtering, interval 25 | 26 | __metric_eloc__ = {"java": 500, "c": 500, "cpp": 500, "cs": 500, "h": 300, "hpp": 300, "php": 500, "py": 500, "glsl": 1000, 27 | "rb": 500, "js": 500, "sql": 1000, "xml": 1000} 28 | 29 | __metric_cc_tokens__ = [[["java", "js", "c", "cc", "cpp"], ["else", r"for\s+\(.*\)", r"if\s+\(.*\)", r"case\s+\w+:", 30 | "default:", r"while\s+\(.*\)"], 31 | ["assert", "break", "continue", "return"]], 32 | [["cs"], ["else", r"for\s+\(.*\)", r"foreach\s+\(.*\)", r"goto\s+\w+:", r"if\s+\(.*\)", r"case\s+\w+:", 33 | "default:", r"while\s+\(.*\)"], 34 | ["assert", "break", "continue", "return"]], 35 | [["py"], [r"^\s+elif .*:$", r"^\s+else:$", r"^\s+for .*:", r"^\s+if .*:$", r"^\s+while .*:$"], 36 | [r"^\s+assert", "break", "continue", "return"]]] 37 | 38 | METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD = 50 39 | METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD = 0.75 40 | 41 | class MetricsLogic(object): 42 | def __init__(self): 43 | self.eloc = {} 44 | self.cyclomatic_complexity = {} 45 | self.cyclomatic_complexity_density = {} 46 | 47 | ls_tree_p = subprocess.Popen(["git", "ls-tree", "--name-only", "-r", interval.get_ref()], bufsize=1, 48 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 49 | lines = ls_tree_p.communicate()[0].splitlines() 50 | ls_tree_p.stdout.close() 51 | 52 | if ls_tree_p.returncode == 0: 53 | for i in lines: 54 | i = i.strip().decode("unicode_escape", "ignore") 55 | i = i.encode("latin-1", "replace") 56 | i = i.decode("utf-8", "replace").strip("\"").strip("'").strip() 57 | 58 | if FileDiff.is_valid_extension(i) and not filtering.set_filtered(FileDiff.get_filename(i)): 59 | file_r = subprocess.Popen(["git", "show", interval.get_ref() + ":{0}".format(i.strip())], 60 | bufsize=1, stdout=subprocess.PIPE).stdout.readlines() 61 | 62 | extension = FileDiff.get_extension(i) 63 | lines = MetricsLogic.get_eloc(file_r, extension) 64 | cycc = MetricsLogic.get_cyclomatic_complexity(file_r, extension) 65 | 66 | if __metric_eloc__.get(extension, None) != None and __metric_eloc__[extension] < lines: 67 | self.eloc[i.strip()] = lines 68 | 69 | if METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD < cycc: 70 | self.cyclomatic_complexity[i.strip()] = cycc 71 | 72 | if lines > 0 and METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD < cycc / float(lines): 73 | self.cyclomatic_complexity_density[i.strip()] = cycc / float(lines) 74 | 75 | def __iadd__(self, other): 76 | try: 77 | self.eloc.update(other.eloc) 78 | self.cyclomatic_complexity.update(other.cyclomatic_complexity) 79 | self.cyclomatic_complexity_density.update(other.cyclomatic_complexity_density) 80 | return self 81 | except AttributeError: 82 | return other; 83 | 84 | @staticmethod 85 | def get_cyclomatic_complexity(file_r, extension): 86 | is_inside_comment = False 87 | cc_counter = 0 88 | 89 | entry_tokens = None 90 | exit_tokens = None 91 | 92 | for i in __metric_cc_tokens__: 93 | if extension in i[0]: 94 | entry_tokens = i[1] 95 | exit_tokens = i[2] 96 | 97 | if entry_tokens or exit_tokens: 98 | for i in file_r: 99 | i = i.decode("utf-8", "replace") 100 | (_, is_inside_comment) = comment.handle_comment_block(is_inside_comment, extension, i) 101 | 102 | if not is_inside_comment and not comment.is_comment(extension, i): 103 | for j in entry_tokens: 104 | if re.search(j, i, re.DOTALL): 105 | cc_counter += 2 106 | for j in exit_tokens: 107 | if re.search(j, i, re.DOTALL): 108 | cc_counter += 1 109 | return cc_counter 110 | 111 | return -1 112 | 113 | @staticmethod 114 | def get_eloc(file_r, extension): 115 | is_inside_comment = False 116 | eloc_counter = 0 117 | 118 | for i in file_r: 119 | i = i.decode("utf-8", "replace") 120 | (_, is_inside_comment) = comment.handle_comment_block(is_inside_comment, extension, i) 121 | 122 | if not is_inside_comment and not comment.is_comment(extension, i): 123 | eloc_counter += 1 124 | 125 | return eloc_counter 126 | -------------------------------------------------------------------------------- /gitinspector/output/filteringoutput.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import textwrap 23 | from ..localization import N_ 24 | from ..filtering import __filters__, has_filtered 25 | from .. import terminal 26 | from .outputable import Outputable 27 | 28 | FILTERING_INFO_TEXT = N_("The following files were excluded from the statistics due to the specified exclusion patterns") 29 | FILTERING_AUTHOR_INFO_TEXT = N_("The following authors were excluded from the statistics due to the specified exclusion patterns") 30 | FILTERING_EMAIL_INFO_TEXT = N_("The authors with the following emails were excluded from the statistics due to the specified " \ 31 | "exclusion patterns") 32 | FILTERING_COMMIT_INFO_TEXT = N_("The following commit revisions were excluded from the statistics due to the specified " \ 33 | "exclusion patterns") 34 | 35 | class FilteringOutput(Outputable): 36 | @staticmethod 37 | def __output_html_section__(info_string, filtered): 38 | filtering_xml = "" 39 | 40 | if filtered: 41 | filtering_xml += "

" + info_string + "."+ "

" 42 | 43 | for i in filtered: 44 | filtering_xml += "

" + i + "

" 45 | 46 | return filtering_xml 47 | 48 | def output_html(self): 49 | if has_filtered(): 50 | filtering_xml = "
" 51 | FilteringOutput.__output_html_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1]) 52 | FilteringOutput.__output_html_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1]) 53 | FilteringOutput.__output_html_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1]) 54 | FilteringOutput.__output_html_section__(_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1]) 55 | filtering_xml += "
" 56 | 57 | print(filtering_xml) 58 | 59 | @staticmethod 60 | def __output_json_section__(info_string, filtered, container_tagname): 61 | if filtered: 62 | message_json = "\t\t\t\t\"message\": \"" + info_string + "\",\n" 63 | filtering_json = "" 64 | 65 | for i in filtered: 66 | filtering_json += "\t\t\t\t\t\"" + i + "\",\n" 67 | else: 68 | filtering_json = filtering_json[:-3] 69 | 70 | return "\n\t\t\t\"{0}\": {{\n".format(container_tagname) + message_json + \ 71 | "\t\t\t\t\"entries\": [\n" + filtering_json + "\"\n\t\t\t\t]\n\t\t\t}," 72 | 73 | return "" 74 | 75 | def output_json(self): 76 | if has_filtered(): 77 | output = ",\n\t\t\"filtering\": {" 78 | output += FilteringOutput.__output_json_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1], "files") 79 | output += FilteringOutput.__output_json_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1], "authors") 80 | output += FilteringOutput.__output_json_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1], "emails") 81 | output += FilteringOutput.__output_json_section__(_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1], "revision") 82 | output = output[:-1] 83 | output += "\n\t\t}" 84 | print(output, end="") 85 | 86 | @staticmethod 87 | def __output_text_section__(info_string, filtered): 88 | if filtered: 89 | print("\n" + textwrap.fill(info_string + ":", width=terminal.get_size()[0])) 90 | 91 | for i in filtered: 92 | (width, _unused) = terminal.get_size() 93 | print("...%s" % i[-width+3:] if len(i) > width else i) 94 | 95 | def output_text(self): 96 | FilteringOutput.__output_text_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1]) 97 | FilteringOutput.__output_text_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1]) 98 | FilteringOutput.__output_text_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1]) 99 | FilteringOutput.__output_text_section__(_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1]) 100 | 101 | @staticmethod 102 | def __output_xml_section__(info_string, filtered, container_tagname): 103 | if filtered: 104 | message_xml = "\t\t\t" + info_string + "\n" 105 | filtering_xml = "" 106 | 107 | for i in filtered: 108 | filtering_xml += "\t\t\t\t" + i + "\n" 109 | 110 | print("\t\t<{0}>".format(container_tagname)) 111 | print(message_xml + "\t\t\t\n" + filtering_xml + "\t\t\t\n") 112 | print("\t\t".format(container_tagname)) 113 | 114 | def output_xml(self): 115 | if has_filtered(): 116 | print("\t") 117 | FilteringOutput.__output_xml_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1], "files") 118 | FilteringOutput.__output_xml_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1], "authors") 119 | FilteringOutput.__output_xml_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1], "emails") 120 | FilteringOutput.__output_xml_section__(_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1], "revision") 121 | print("\t") 122 | -------------------------------------------------------------------------------- /gitinspector/output/responsibilitiesoutput.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import textwrap 23 | from ..localization import N_ 24 | from .. import format, gravatar, terminal 25 | from .. import responsibilities as resp 26 | from .outputable import Outputable 27 | 28 | RESPONSIBILITIES_INFO_TEXT = N_("The following responsibilities, by author, were found in the current " 29 | "revision of the repository (comments are excluded from the line count, " 30 | "if possible)") 31 | MOSTLY_RESPONSIBLE_FOR_TEXT = N_("is mostly responsible for") 32 | 33 | class ResponsibilitiesOutput(Outputable): 34 | def __init__(self, changes, blame): 35 | self.changes = changes 36 | self.blame = blame 37 | Outputable.__init__(self) 38 | 39 | def output_text(self): 40 | print("\n" + textwrap.fill(_(RESPONSIBILITIES_INFO_TEXT) + ":", width=terminal.get_size()[0])) 41 | 42 | for i in sorted(set(i[0] for i in self.blame.blames)): 43 | responsibilities = sorted(((i[1], i[0]) for i in resp.Responsibilities.get(self.blame, i)), reverse=True) 44 | 45 | if responsibilities: 46 | print("\n" + i, _(MOSTLY_RESPONSIBLE_FOR_TEXT) + ":") 47 | 48 | for j, entry in enumerate(responsibilities): 49 | (width, _unused) = terminal.get_size() 50 | width -= 7 51 | 52 | print(str(entry[0]).rjust(6), end=" ") 53 | print("...%s" % entry[1][-width+3:] if len(entry[1]) > width else entry[1]) 54 | 55 | if j >= 9: 56 | break 57 | 58 | def output_html(self): 59 | resp_xml = "
" 60 | resp_xml += "

" + _(RESPONSIBILITIES_INFO_TEXT) + ".

" 61 | 62 | for i in sorted(set(i[0] for i in self.blame.blames)): 63 | responsibilities = sorted(((i[1], i[0]) for i in resp.Responsibilities.get(self.blame, i)), reverse=True) 64 | 65 | if responsibilities: 66 | resp_xml += "
" 67 | 68 | if format.get_selected() == "html": 69 | author_email = self.changes.get_latest_email_by_author(i) 70 | resp_xml += "

{1} {2}

".format(gravatar.get_url(author_email, size=32), 71 | i, _(MOSTLY_RESPONSIBLE_FOR_TEXT)) 72 | else: 73 | resp_xml += "

{0} {1}

".format(i, _(MOSTLY_RESPONSIBLE_FOR_TEXT)) 74 | 75 | for j, entry in enumerate(responsibilities): 76 | resp_xml += "" if j % 2 == 1 else ">") + entry[1] + \ 77 | " (" + str(entry[0]) + " eloc)
" 78 | if j >= 9: 79 | break 80 | 81 | resp_xml += "
" 82 | resp_xml += "
" 83 | print(resp_xml) 84 | 85 | def output_json(self): 86 | message_json = "\t\t\t\"message\": \"" + _(RESPONSIBILITIES_INFO_TEXT) + "\",\n" 87 | resp_json = "" 88 | 89 | for i in sorted(set(i[0] for i in self.blame.blames)): 90 | responsibilities = sorted(((i[1], i[0]) for i in resp.Responsibilities.get(self.blame, i)), reverse=True) 91 | 92 | if responsibilities: 93 | author_email = self.changes.get_latest_email_by_author(i) 94 | 95 | resp_json += "{\n" 96 | resp_json += "\t\t\t\t\"name\": \"" + i + "\",\n" 97 | resp_json += "\t\t\t\t\"email\": \"" + author_email + "\",\n" 98 | resp_json += "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n" 99 | resp_json += "\t\t\t\t\"files\": [\n\t\t\t\t" 100 | 101 | for j, entry in enumerate(responsibilities): 102 | resp_json += "{\n" 103 | resp_json += "\t\t\t\t\t\"name\": \"" + entry[1] + "\",\n" 104 | resp_json += "\t\t\t\t\t\"rows\": " + str(entry[0]) + "\n" 105 | resp_json += "\t\t\t\t}," 106 | 107 | if j >= 9: 108 | break 109 | 110 | resp_json = resp_json[:-1] 111 | resp_json += "]\n\t\t\t}," 112 | 113 | resp_json = resp_json[:-1] 114 | print(",\n\t\t\"responsibilities\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + resp_json + "]\n\t\t}", end="") 115 | 116 | def output_xml(self): 117 | message_xml = "\t\t" + _(RESPONSIBILITIES_INFO_TEXT) + "\n" 118 | resp_xml = "" 119 | 120 | for i in sorted(set(i[0] for i in self.blame.blames)): 121 | responsibilities = sorted(((i[1], i[0]) for i in resp.Responsibilities.get(self.blame, i)), reverse=True) 122 | if responsibilities: 123 | author_email = self.changes.get_latest_email_by_author(i) 124 | 125 | resp_xml += "\t\t\t\n" 126 | resp_xml += "\t\t\t\t" + i + "\n" 127 | resp_xml += "\t\t\t\t" + author_email + "\n" 128 | resp_xml += "\t\t\t\t" + gravatar.get_url(author_email) + "\n" 129 | resp_xml += "\t\t\t\t\n" 130 | 131 | for j, entry in enumerate(responsibilities): 132 | resp_xml += "\t\t\t\t\t\n" 133 | resp_xml += "\t\t\t\t\t\t" + entry[1] + "\n" 134 | resp_xml += "\t\t\t\t\t\t" + str(entry[0]) + "\n" 135 | resp_xml += "\t\t\t\t\t\n" 136 | 137 | if j >= 9: 138 | break 139 | 140 | resp_xml += "\t\t\t\t\n" 141 | resp_xml += "\t\t\t\n" 142 | 143 | print("\t\n" + message_xml + "\t\t\n" + resp_xml + "\t\t\n\t") 144 | -------------------------------------------------------------------------------- /gitinspector/format.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import base64 23 | import os 24 | import textwrap 25 | import time 26 | import zipfile 27 | from .localization import N_ 28 | from . import basedir, localization, terminal, version 29 | 30 | __available_formats__ = ["html", "htmlembedded", "json", "text", "xml"] 31 | 32 | DEFAULT_FORMAT = __available_formats__[3] 33 | 34 | __selected_format__ = DEFAULT_FORMAT 35 | 36 | class InvalidFormatError(Exception): 37 | def __init__(self, msg): 38 | super(InvalidFormatError, self).__init__(msg) 39 | self.msg = msg 40 | 41 | def select(format): 42 | global __selected_format__ 43 | __selected_format__ = format 44 | 45 | return format in __available_formats__ 46 | 47 | def get_selected(): 48 | return __selected_format__ 49 | 50 | def is_interactive_format(): 51 | return __selected_format__ == "text" 52 | 53 | def __output_html_template__(name): 54 | template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), name) 55 | file_r = open(template_path, "rb") 56 | template = file_r.read().decode("utf-8", "replace") 57 | 58 | file_r.close() 59 | return template 60 | 61 | def __get_zip_file_content__(name, file_name="/html/flot.zip"): 62 | zip_file = zipfile.ZipFile(basedir.get_basedir() + file_name, "r") 63 | content = zip_file.read(name) 64 | 65 | zip_file.close() 66 | return content.decode("utf-8", "replace") 67 | 68 | INFO_ONE_REPOSITORY = N_("Statistical information for the repository '{0}' was gathered on {1}.") 69 | INFO_MANY_REPOSITORIES = N_("Statistical information for the repositories '{0}' was gathered on {1}.") 70 | 71 | def output_header(repos): 72 | repos_string = ", ".join([repo.name for repo in repos]) 73 | 74 | if __selected_format__ == "html" or __selected_format__ == "htmlembedded": 75 | base = basedir.get_basedir() 76 | html_header = __output_html_template__(base + "/html/html.header") 77 | tablesorter_js = __get_zip_file_content__("jquery.tablesorter.min.js", 78 | "/html/jquery.tablesorter.min.js.zip").encode("latin-1", "replace") 79 | tablesorter_js = tablesorter_js.decode("utf-8", "ignore") 80 | flot_js = __get_zip_file_content__("jquery.flot.js") 81 | pie_js = __get_zip_file_content__("jquery.flot.pie.js") 82 | resize_js = __get_zip_file_content__("jquery.flot.resize.js") 83 | 84 | logo_file = open(base + "/html/gitinspector_piclet.png", "rb") 85 | logo = logo_file.read() 86 | logo_file.close() 87 | logo = base64.b64encode(logo) 88 | 89 | if __selected_format__ == "htmlembedded": 90 | jquery_js = ">" + __get_zip_file_content__("jquery.js") 91 | else: 92 | jquery_js = " src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\">" 93 | 94 | print(html_header.format(title=_("Repository statistics for '{0}'").format(repos_string), 95 | jquery=jquery_js, 96 | jquery_tablesorter=tablesorter_js, 97 | jquery_flot=flot_js, 98 | jquery_flot_pie=pie_js, 99 | jquery_flot_resize=resize_js, 100 | logo=logo.decode("utf-8", "replace"), 101 | logo_text=_("The output has been generated by {0} {1}. The statistical analysis tool" 102 | " for git repositories.").format( 103 | "gitinspector", 104 | version.__version__), 105 | repo_text=_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format( 106 | repos_string, localization.get_date()), 107 | show_minor_authors=_("Show minor authors"), 108 | hide_minor_authors=_("Hide minor authors"), 109 | show_minor_rows=_("Show rows with minor work"), 110 | hide_minor_rows=_("Hide rows with minor work"))) 111 | elif __selected_format__ == "json": 112 | print("{\n\t\"gitinspector\": {") 113 | print("\t\t\"version\": \"" + version.__version__ + "\",") 114 | 115 | if len(repos) <= 1: 116 | print("\t\t\"repository\": \"" + repos_string + "\",") 117 | else: 118 | repos_json = "\t\t\"repositories\": [ " 119 | 120 | for repo in repos: 121 | repos_json += "\"" + repo.name + "\", " 122 | 123 | print(repos_json[:-2] + " ],") 124 | 125 | print("\t\t\"report_date\": \"" + time.strftime("%Y/%m/%d") + "\",") 126 | 127 | elif __selected_format__ == "xml": 128 | print("") 129 | print("\t" + version.__version__ + "") 130 | 131 | if len(repos) <= 1: 132 | print("\t" + repos_string + "") 133 | else: 134 | print("\t") 135 | 136 | for repo in repos: 137 | print("\t\t" + repo.name + "") 138 | 139 | print("\t") 140 | 141 | print("\t" + time.strftime("%Y/%m/%d") + "") 142 | else: 143 | print(textwrap.fill(_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format( 144 | repos_string, localization.get_date()), width=terminal.get_size()[0])) 145 | 146 | def output_footer(): 147 | if __selected_format__ == "html" or __selected_format__ == "htmlembedded": 148 | base = basedir.get_basedir() 149 | html_footer = __output_html_template__(base + "/html/html.footer") 150 | print(html_footer) 151 | elif __selected_format__ == "json": 152 | print("\n\t}\n}") 153 | elif __selected_format__ == "xml": 154 | print("") 155 | -------------------------------------------------------------------------------- /docs/docbook-xsl.css: -------------------------------------------------------------------------------- 1 | /* 2 | CSS stylesheet for XHTML produced by DocBook XSL stylesheets. 3 | */ 4 | 5 | body { 6 | font-family: Georgia,serif; 7 | } 8 | 9 | code, pre { 10 | font-family: "Courier New", Courier, monospace; 11 | } 12 | 13 | span.strong { 14 | font-weight: bold; 15 | } 16 | 17 | body blockquote { 18 | margin-top: .75em; 19 | line-height: 1.5; 20 | margin-bottom: .75em; 21 | } 22 | 23 | html body { 24 | margin: 1em 5% 1em 5%; 25 | line-height: 1.2; 26 | } 27 | 28 | body div { 29 | margin: 0; 30 | } 31 | 32 | h1, h2, h3, h4, h5, h6 33 | { 34 | color: #527bbd; 35 | font-family: Arial,Helvetica,sans-serif; 36 | } 37 | 38 | div.toc p:first-child, 39 | div.list-of-figures p:first-child, 40 | div.list-of-tables p:first-child, 41 | div.list-of-examples p:first-child, 42 | div.example p.title, 43 | div.sidebar p.title 44 | { 45 | font-weight: bold; 46 | color: #527bbd; 47 | font-family: Arial,Helvetica,sans-serif; 48 | margin-bottom: 0.2em; 49 | } 50 | 51 | body h1 { 52 | margin: .0em 0 0 -4%; 53 | line-height: 1.3; 54 | border-bottom: 2px solid silver; 55 | } 56 | 57 | body h2 { 58 | margin: 0.5em 0 0 -4%; 59 | line-height: 1.3; 60 | border-bottom: 2px solid silver; 61 | } 62 | 63 | body h3 { 64 | margin: .8em 0 0 -3%; 65 | line-height: 1.3; 66 | } 67 | 68 | body h4 { 69 | margin: .8em 0 0 -3%; 70 | line-height: 1.3; 71 | } 72 | 73 | body h5 { 74 | margin: .8em 0 0 -2%; 75 | line-height: 1.3; 76 | } 77 | 78 | body h6 { 79 | margin: .8em 0 0 -1%; 80 | line-height: 1.3; 81 | } 82 | 83 | body hr { 84 | border: none; /* Broken on IE6 */ 85 | } 86 | div.footnotes hr { 87 | border: 1px solid silver; 88 | } 89 | 90 | div.navheader th, div.navheader td, div.navfooter td { 91 | font-family: Arial,Helvetica,sans-serif; 92 | font-size: 0.9em; 93 | font-weight: bold; 94 | color: #527bbd; 95 | } 96 | div.navheader img, div.navfooter img { 97 | border-style: none; 98 | } 99 | div.navheader a, div.navfooter a { 100 | font-weight: normal; 101 | } 102 | div.navfooter hr { 103 | border: 1px solid silver; 104 | } 105 | 106 | body td { 107 | line-height: 1.2 108 | } 109 | 110 | body th { 111 | line-height: 1.2; 112 | } 113 | 114 | ol { 115 | line-height: 1.2; 116 | } 117 | 118 | ul, body dir, body menu { 119 | line-height: 1.2; 120 | } 121 | 122 | html { 123 | margin: 0; 124 | padding: 0; 125 | } 126 | 127 | body h1, body h2, body h3, body h4, body h5, body h6 { 128 | margin-left: 0 129 | } 130 | 131 | body pre { 132 | margin: 0.5em 10% 0.5em 1em; 133 | line-height: 1.0; 134 | color: navy; 135 | } 136 | 137 | tt.literal, code.literal { 138 | color: navy; 139 | } 140 | 141 | .programlisting, .screen { 142 | border: 1px solid silver; 143 | background: #f4f4f4; 144 | margin: 0.5em 10% 0.5em 0; 145 | padding: 0.5em 1em; 146 | } 147 | 148 | div.sidebar { 149 | background: #ffffee; 150 | margin: 1.0em 10% 0.5em 0; 151 | padding: 0.5em 1em; 152 | border: 1px solid silver; 153 | } 154 | div.sidebar * { padding: 0; } 155 | div.sidebar div { margin: 0; } 156 | div.sidebar p.title { 157 | margin-top: 0.5em; 158 | margin-bottom: 0.2em; 159 | } 160 | 161 | div.bibliomixed { 162 | margin: 0.5em 5% 0.5em 1em; 163 | } 164 | 165 | div.glossary dt { 166 | font-weight: bold; 167 | } 168 | div.glossary dd p { 169 | margin-top: 0.2em; 170 | } 171 | 172 | dl { 173 | margin: .8em 0; 174 | line-height: 1.2; 175 | } 176 | 177 | dt { 178 | margin-top: 0.5em; 179 | } 180 | 181 | dt span.term { 182 | font-style: normal; 183 | color: navy; 184 | } 185 | 186 | div.variablelist dd p { 187 | margin-top: 0; 188 | } 189 | 190 | div.itemizedlist li, div.orderedlist li { 191 | margin-left: -0.8em; 192 | margin-top: 0.5em; 193 | } 194 | 195 | ul, ol { 196 | list-style-position: outside; 197 | } 198 | 199 | div.sidebar ul, div.sidebar ol { 200 | margin-left: 2.8em; 201 | } 202 | 203 | div.itemizedlist p.title, 204 | div.orderedlist p.title, 205 | div.variablelist p.title 206 | { 207 | margin-bottom: -0.8em; 208 | } 209 | 210 | div.revhistory table { 211 | border-collapse: collapse; 212 | border: none; 213 | } 214 | div.revhistory th { 215 | border: none; 216 | color: #527bbd; 217 | font-family: Arial,Helvetica,sans-serif; 218 | } 219 | div.revhistory td { 220 | border: 1px solid silver; 221 | } 222 | 223 | /* Keep TOC and index lines close together. */ 224 | div.toc dl, div.toc dt, 225 | div.list-of-figures dl, div.list-of-figures dt, 226 | div.list-of-tables dl, div.list-of-tables dt, 227 | div.indexdiv dl, div.indexdiv dt 228 | { 229 | line-height: normal; 230 | margin-top: 0; 231 | margin-bottom: 0; 232 | } 233 | 234 | /* 235 | Table styling does not work because of overriding attributes in 236 | generated HTML. 237 | */ 238 | div.table table, 239 | div.informaltable table 240 | { 241 | margin-left: 0; 242 | margin-right: 5%; 243 | margin-bottom: 0.8em; 244 | } 245 | div.informaltable table 246 | { 247 | margin-top: 0.4em 248 | } 249 | div.table thead, 250 | div.table tfoot, 251 | div.table tbody, 252 | div.informaltable thead, 253 | div.informaltable tfoot, 254 | div.informaltable tbody 255 | { 256 | /* No effect in IE6. */ 257 | border-top: 3px solid #527bbd; 258 | border-bottom: 3px solid #527bbd; 259 | } 260 | div.table thead, div.table tfoot, 261 | div.informaltable thead, div.informaltable tfoot 262 | { 263 | font-weight: bold; 264 | } 265 | 266 | div.mediaobject img { 267 | margin-bottom: 0.8em; 268 | } 269 | div.figure p.title, 270 | div.table p.title 271 | { 272 | margin-top: 1em; 273 | margin-bottom: 0.4em; 274 | } 275 | 276 | div.calloutlist p 277 | { 278 | margin-top: 0em; 279 | margin-bottom: 0.4em; 280 | } 281 | 282 | a img { 283 | border-style: none; 284 | } 285 | 286 | @media print { 287 | div.navheader, div.navfooter { display: none; } 288 | } 289 | 290 | span.aqua { color: aqua; } 291 | span.black { color: black; } 292 | span.blue { color: blue; } 293 | span.fuchsia { color: fuchsia; } 294 | span.gray { color: gray; } 295 | span.green { color: green; } 296 | span.lime { color: lime; } 297 | span.maroon { color: maroon; } 298 | span.navy { color: navy; } 299 | span.olive { color: olive; } 300 | span.purple { color: purple; } 301 | span.red { color: red; } 302 | span.silver { color: silver; } 303 | span.teal { color: teal; } 304 | span.white { color: white; } 305 | span.yellow { color: yellow; } 306 | 307 | span.aqua-background { background: aqua; } 308 | span.black-background { background: black; } 309 | span.blue-background { background: blue; } 310 | span.fuchsia-background { background: fuchsia; } 311 | span.gray-background { background: gray; } 312 | span.green-background { background: green; } 313 | span.lime-background { background: lime; } 314 | span.maroon-background { background: maroon; } 315 | span.navy-background { background: navy; } 316 | span.olive-background { background: olive; } 317 | span.purple-background { background: purple; } 318 | span.red-background { background: red; } 319 | span.silver-background { background: silver; } 320 | span.teal-background { background: teal; } 321 | span.white-background { background: white; } 322 | span.yellow-background { background: yellow; } 323 | 324 | span.big { font-size: 2em; } 325 | span.small { font-size: 0.6em; } 326 | 327 | span.underline { text-decoration: underline; } 328 | span.overline { text-decoration: overline; } 329 | span.line-through { text-decoration: line-through; } 330 | -------------------------------------------------------------------------------- /gitinspector/blame.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2017 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import datetime 23 | import multiprocessing 24 | import re 25 | import subprocess 26 | import threading 27 | from .localization import N_ 28 | from .changes import FileDiff 29 | from . import comment, extensions, filtering, format, interval, terminal 30 | 31 | NUM_THREADS = multiprocessing.cpu_count() 32 | 33 | class BlameEntry(object): 34 | rows = 0 35 | skew = 0 # Used when calculating average code age. 36 | comments = 0 37 | 38 | __thread_lock__ = threading.BoundedSemaphore(NUM_THREADS) 39 | __blame_lock__ = threading.Lock() 40 | 41 | AVG_DAYS_PER_MONTH = 30.4167 42 | 43 | class BlameThread(threading.Thread): 44 | def __init__(self, useweeks, changes, blame_command, extension, blames, filename): 45 | __thread_lock__.acquire() # Lock controlling the number of threads running 46 | threading.Thread.__init__(self) 47 | 48 | self.useweeks = useweeks 49 | self.changes = changes 50 | self.blame_command = blame_command 51 | self.extension = extension 52 | self.blames = blames 53 | self.filename = filename 54 | 55 | self.is_inside_comment = False 56 | 57 | def __clear_blamechunk_info__(self): 58 | self.blamechunk_email = None 59 | self.blamechunk_is_last = False 60 | self.blamechunk_is_prior = False 61 | self.blamechunk_revision = None 62 | self.blamechunk_time = None 63 | 64 | def __handle_blamechunk_content__(self, content): 65 | author = None 66 | (comments, self.is_inside_comment) = comment.handle_comment_block(self.is_inside_comment, self.extension, content) 67 | 68 | if self.blamechunk_is_prior and interval.get_since(): 69 | return 70 | try: 71 | author = self.changes.get_latest_author_by_email(self.blamechunk_email) 72 | except KeyError: 73 | return 74 | 75 | if not filtering.set_filtered(author, "author") and not \ 76 | filtering.set_filtered(self.blamechunk_email, "email") and not \ 77 | filtering.set_filtered(self.blamechunk_revision, "revision"): 78 | 79 | __blame_lock__.acquire() # Global lock used to protect calls from here... 80 | 81 | if self.blames.get((author, self.filename), None) == None: 82 | self.blames[(author, self.filename)] = BlameEntry() 83 | 84 | self.blames[(author, self.filename)].comments += comments 85 | self.blames[(author, self.filename)].rows += 1 86 | 87 | if (self.blamechunk_time - self.changes.first_commit_date).days > 0: 88 | self.blames[(author, self.filename)].skew += ((self.changes.last_commit_date - self.blamechunk_time).days / 89 | (7.0 if self.useweeks else AVG_DAYS_PER_MONTH)) 90 | 91 | __blame_lock__.release() # ...to here. 92 | 93 | def run(self): 94 | git_blame_r = subprocess.Popen(self.blame_command, bufsize=1, stdout=subprocess.PIPE).stdout 95 | rows = git_blame_r.readlines() 96 | git_blame_r.close() 97 | 98 | self.__clear_blamechunk_info__() 99 | 100 | #pylint: disable=W0201 101 | for j in range(0, len(rows)): 102 | row = rows[j].decode("utf-8", "replace").strip() 103 | keyval = row.split(" ", 2) 104 | 105 | if self.blamechunk_is_last: 106 | self.__handle_blamechunk_content__(row) 107 | self.__clear_blamechunk_info__() 108 | elif keyval[0] == "boundary": 109 | self.blamechunk_is_prior = True 110 | elif keyval[0] == "author-mail": 111 | self.blamechunk_email = keyval[1].lstrip("<").rstrip(">") 112 | elif keyval[0] == "author-time": 113 | self.blamechunk_time = datetime.date.fromtimestamp(int(keyval[1])) 114 | elif keyval[0] == "filename": 115 | self.blamechunk_is_last = True 116 | elif Blame.is_revision(keyval[0]): 117 | self.blamechunk_revision = keyval[0] 118 | 119 | __thread_lock__.release() # Lock controlling the number of threads running 120 | 121 | PROGRESS_TEXT = N_("Checking how many rows belong to each author (2 of 2): {0:.0f}%") 122 | 123 | class Blame(object): 124 | def __init__(self, repo, hard, useweeks, changes): 125 | self.blames = {} 126 | ls_tree_p = subprocess.Popen(["git", "ls-tree", "--name-only", "-r", interval.get_ref()], bufsize=1, 127 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 128 | lines = ls_tree_p.communicate()[0].splitlines() 129 | ls_tree_p.stdout.close() 130 | 131 | if ls_tree_p.returncode == 0: 132 | progress_text = _(PROGRESS_TEXT) 133 | 134 | if repo != None: 135 | progress_text = "[%s] " % repo.name + progress_text 136 | 137 | for i, row in enumerate(lines): 138 | row = row.strip().decode("unicode_escape", "ignore") 139 | row = row.encode("latin-1", "replace") 140 | row = row.decode("utf-8", "replace").strip("\"").strip("'").strip() 141 | 142 | if FileDiff.get_extension(row) in extensions.get_located() and \ 143 | FileDiff.is_valid_extension(row) and not filtering.set_filtered(FileDiff.get_filename(row)): 144 | blame_command = filter(None, ["git", "blame", "--line-porcelain", "-w"] + \ 145 | (["-C", "-C", "-M"] if hard else []) + 146 | [interval.get_since(), interval.get_ref(), "--", row]) 147 | thread = BlameThread(useweeks, changes, blame_command, FileDiff.get_extension(row), 148 | self.blames, row.strip()) 149 | thread.daemon = True 150 | thread.start() 151 | 152 | if format.is_interactive_format(): 153 | terminal.output_progress(progress_text, i, len(lines)) 154 | 155 | # Make sure all threads have completed. 156 | for i in range(0, NUM_THREADS): 157 | __thread_lock__.acquire() 158 | 159 | # We also have to release them for future use. 160 | for i in range(0, NUM_THREADS): 161 | __thread_lock__.release() 162 | 163 | def __iadd__(self, other): 164 | try: 165 | self.blames.update(other.blames) 166 | return self; 167 | except AttributeError: 168 | return other; 169 | 170 | @staticmethod 171 | def is_revision(string): 172 | revision = re.search("([0-9a-f]{40})", string) 173 | 174 | if revision == None: 175 | return False 176 | 177 | return revision.group(1).strip() 178 | 179 | @staticmethod 180 | def get_stability(author, blamed_rows, changes): 181 | if author in changes.get_authorinfo_list(): 182 | author_insertions = changes.get_authorinfo_list()[author].insertions 183 | return 100 if author_insertions == 0 else 100.0 * blamed_rows / author_insertions 184 | return 100 185 | 186 | @staticmethod 187 | def get_time(string): 188 | time = re.search(r" \(.*?(\d\d\d\d-\d\d-\d\d)", string) 189 | return time.group(1).strip() 190 | 191 | def get_summed_blames(self): 192 | summed_blames = {} 193 | for i in self.blames.items(): 194 | if summed_blames.get(i[0][0], None) == None: 195 | summed_blames[i[0][0]] = BlameEntry() 196 | 197 | summed_blames[i[0][0]].rows += i[1].rows 198 | summed_blames[i[0][0]].skew += i[1].skew 199 | summed_blames[i[0][0]].comments += i[1].comments 200 | 201 | return summed_blames 202 | -------------------------------------------------------------------------------- /gitinspector/output/blameoutput.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import json 23 | import sys 24 | import textwrap 25 | from ..localization import N_ 26 | from .. import format, gravatar, terminal 27 | from ..blame import Blame 28 | from .outputable import Outputable 29 | 30 | BLAME_INFO_TEXT = N_("Below are the number of rows from each author that have survived and are still " 31 | "intact in the current revision") 32 | 33 | class BlameOutput(Outputable): 34 | def __init__(self, changes, blame): 35 | if format.is_interactive_format(): 36 | print("") 37 | 38 | self.changes = changes 39 | self.blame = blame 40 | Outputable.__init__(self) 41 | 42 | def output_html(self): 43 | blame_xml = "
" 44 | blame_xml += "

" + _(BLAME_INFO_TEXT) + ".

" 45 | blame_xml += "".format( 46 | _("Author"), _("Rows"), _("Stability"), _("Age"), _("% in comments")) 47 | blame_xml += "" 48 | chart_data = "" 49 | blames = sorted(self.blame.get_summed_blames().items()) 50 | total_blames = 0 51 | 52 | for i in blames: 53 | total_blames += i[1].rows 54 | 55 | for i, entry in enumerate(blames): 56 | work_percentage = str("{0:.2f}".format(100.0 * entry[1].rows / total_blames)) 57 | blame_xml += "" if i % 2 == 1 else ">") 58 | 59 | if format.get_selected() == "html": 60 | author_email = self.changes.get_latest_email_by_author(entry[0]) 61 | blame_xml += "".format(gravatar.get_url(author_email), entry[0]) 62 | else: 63 | blame_xml += "" 64 | 65 | blame_xml += "" 66 | blame_xml += "") 67 | blame_xml += "" 68 | blame_xml += "" 69 | blame_xml += "" 70 | blame_xml += "" 71 | chart_data += "{{label: {0}, data: {1}}}".format(json.dumps(entry[0]), work_percentage) 72 | 73 | if blames[-1] != entry: 74 | chart_data += ", " 75 | 76 | blame_xml += "
{0} {1} {2} {3} {4}
{1}" + entry[0] + "" + str(entry[1].rows) + "" + ("{0:.1f}".format(Blame.get_stability(entry[0], entry[1].rows, self.changes)) + "" + "{0:.1f}".format(float(entry[1].skew) / entry[1].rows) + "" + "{0:.2f}".format(100.0 * entry[1].comments / entry[1].rows) + "" + work_percentage + "
 
" 77 | blame_xml += "
" 78 | blame_xml += "
" 94 | 95 | print(blame_xml) 96 | 97 | def output_json(self): 98 | message_json = "\t\t\t\"message\": \"" + _(BLAME_INFO_TEXT) + "\",\n" 99 | blame_json = "" 100 | 101 | for i in sorted(self.blame.get_summed_blames().items()): 102 | author_email = self.changes.get_latest_email_by_author(i[0]) 103 | 104 | name_json = "\t\t\t\t\"name\": \"" + i[0] + "\",\n" 105 | email_json = "\t\t\t\t\"email\": \"" + author_email + "\",\n" 106 | gravatar_json = "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n" 107 | rows_json = "\t\t\t\t\"rows\": " + str(i[1].rows) + ",\n" 108 | stability_json = ("\t\t\t\t\"stability\": " + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, 109 | self.changes)) + ",\n") 110 | age_json = ("\t\t\t\t\"age\": " + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + ",\n") 111 | percentage_in_comments_json = ("\t\t\t\t\"percentage_in_comments\": " + 112 | "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + "\n") 113 | blame_json += ("{\n" + name_json + email_json + gravatar_json + rows_json + stability_json + age_json + 114 | percentage_in_comments_json + "\t\t\t},") 115 | else: 116 | blame_json = blame_json[:-1] 117 | 118 | print(",\n\t\t\"blame\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + blame_json + "]\n\t\t}", end="") 119 | 120 | def output_text(self): 121 | if sys.stdout.isatty() and format.is_interactive_format(): 122 | terminal.clear_row() 123 | 124 | print(textwrap.fill(_(BLAME_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n") 125 | terminal.printb(terminal.ljust(_("Author"), 21) + terminal.rjust(_("Rows"), 10) + terminal.rjust(_("Stability"), 15) + 126 | terminal.rjust(_("Age"), 13) + terminal.rjust(_("% in comments"), 20)) 127 | 128 | for i in sorted(self.blame.get_summed_blames().items()): 129 | print(terminal.ljust(i[0], 20)[0:20 - terminal.get_excess_column_count(i[0])], end=" ") 130 | print(str(i[1].rows).rjust(10), end=" ") 131 | print("{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, self.changes)).rjust(14), end=" ") 132 | print("{0:.1f}".format(float(i[1].skew) / i[1].rows).rjust(12), end=" ") 133 | print("{0:.2f}".format(100.0 * i[1].comments / i[1].rows).rjust(19)) 134 | 135 | def output_xml(self): 136 | message_xml = "\t\t" + _(BLAME_INFO_TEXT) + "\n" 137 | blame_xml = "" 138 | 139 | for i in sorted(self.blame.get_summed_blames().items()): 140 | author_email = self.changes.get_latest_email_by_author(i[0]) 141 | 142 | name_xml = "\t\t\t\t" + i[0] + "\n" 143 | email_xml = "\t\t\t\t" + author_email + "\n" 144 | gravatar_xml = "\t\t\t\t" + gravatar.get_url(author_email) + "\n" 145 | rows_xml = "\t\t\t\t" + str(i[1].rows) + "\n" 146 | stability_xml = ("\t\t\t\t" + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, 147 | self.changes)) + "\n") 148 | age_xml = ("\t\t\t\t" + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + "\n") 149 | percentage_in_comments_xml = ("\t\t\t\t" + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + 150 | "\n") 151 | blame_xml += ("\t\t\t\n" + name_xml + email_xml + gravatar_xml + rows_xml + stability_xml + 152 | age_xml + percentage_in_comments_xml + "\t\t\t\n") 153 | 154 | print("\t\n" + message_xml + "\t\t\n" + blame_xml + "\t\t\n\t") 155 | -------------------------------------------------------------------------------- /gitinspector/gitinspector.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import atexit 23 | import getopt 24 | import os 25 | import sys 26 | from .blame import Blame 27 | from .changes import Changes 28 | from .config import GitConfig 29 | from .metrics import MetricsLogic 30 | from . import (basedir, clone, extensions, filtering, format, help, interval, 31 | localization, optval, terminal, version) 32 | from .output import outputable 33 | from .output.blameoutput import BlameOutput 34 | from .output.changesoutput import ChangesOutput 35 | from .output.extensionsoutput import ExtensionsOutput 36 | from .output.filteringoutput import FilteringOutput 37 | from .output.metricsoutput import MetricsOutput 38 | from .output.responsibilitiesoutput import ResponsibilitiesOutput 39 | from .output.timelineoutput import TimelineOutput 40 | 41 | localization.init() 42 | 43 | class Runner(object): 44 | def __init__(self): 45 | self.hard = False 46 | self.include_metrics = False 47 | self.list_file_types = False 48 | self.localize_output = False 49 | self.responsibilities = False 50 | self.grading = False 51 | self.timeline = False 52 | self.useweeks = False 53 | 54 | def process(self, repos): 55 | localization.check_compatibility(version.__version__) 56 | 57 | if not self.localize_output: 58 | localization.disable() 59 | 60 | terminal.skip_escapes(not sys.stdout.isatty()) 61 | terminal.set_stdout_encoding() 62 | previous_directory = os.getcwd() 63 | summed_blames = Blame.__new__(Blame) 64 | summed_changes = Changes.__new__(Changes) 65 | summed_metrics = MetricsLogic.__new__(MetricsLogic) 66 | 67 | for repo in repos: 68 | os.chdir(repo.location) 69 | repo = repo if len(repos) > 1 else None 70 | changes = Changes(repo, self.hard) 71 | summed_blames += Blame(repo, self.hard, self.useweeks, changes) 72 | summed_changes += changes 73 | 74 | if self.include_metrics: 75 | summed_metrics += MetricsLogic() 76 | 77 | if sys.stdout.isatty() and format.is_interactive_format(): 78 | terminal.clear_row() 79 | else: 80 | os.chdir(previous_directory) 81 | 82 | format.output_header(repos) 83 | outputable.output(ChangesOutput(summed_changes)) 84 | 85 | if summed_changes.get_commits(): 86 | outputable.output(BlameOutput(summed_changes, summed_blames)) 87 | 88 | if self.timeline: 89 | outputable.output(TimelineOutput(summed_changes, self.useweeks)) 90 | 91 | if self.include_metrics: 92 | outputable.output(MetricsOutput(summed_metrics)) 93 | 94 | if self.responsibilities: 95 | outputable.output(ResponsibilitiesOutput(summed_changes, summed_blames)) 96 | 97 | outputable.output(FilteringOutput()) 98 | 99 | if self.list_file_types: 100 | outputable.output(ExtensionsOutput()) 101 | 102 | format.output_footer() 103 | os.chdir(previous_directory) 104 | 105 | def __check_python_version__(): 106 | if sys.version_info < (2, 6): 107 | python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1]) 108 | sys.exit(_("gitinspector requires at least Python 2.6 to run (version {0} was found).").format(python_version)) 109 | 110 | def __get_validated_git_repos__(repos_relative): 111 | if not repos_relative: 112 | repos_relative = "." 113 | 114 | repos = [] 115 | 116 | #Try to clone the repos or return the same directory and bail out. 117 | for repo in repos_relative: 118 | cloned_repo = clone.create(repo) 119 | 120 | if cloned_repo.name == None: 121 | cloned_repo.location = basedir.get_basedir_git(cloned_repo.location) 122 | cloned_repo.name = os.path.basename(cloned_repo.location) 123 | 124 | repos.append(cloned_repo) 125 | 126 | return repos 127 | 128 | def main(): 129 | terminal.check_terminal_encoding() 130 | terminal.set_stdin_encoding() 131 | argv = terminal.convert_command_line_to_utf8() 132 | run = Runner() 133 | repos = [] 134 | 135 | try: 136 | opts, args = optval.gnu_getopt(argv[1:], "f:F:hHlLmrTwx:", ["exclude=", "file-types=", "format=", 137 | "hard:true", "help", "list-file-types:true", "localize-output:true", 138 | "metrics:true", "responsibilities:true", "since=", "grading:true", 139 | "timeline:true", "until=", "version", "weeks:true"]) 140 | repos = __get_validated_git_repos__(set(args)) 141 | 142 | #We need the repos above to be set before we read the git config. 143 | GitConfig(run, repos[-1].location).read() 144 | clear_x_on_next_pass = True 145 | 146 | for o, a in opts: 147 | if o in("-h", "--help"): 148 | help.output() 149 | sys.exit(0) 150 | elif o in("-f", "--file-types"): 151 | extensions.define(a) 152 | elif o in("-F", "--format"): 153 | if not format.select(a): 154 | raise format.InvalidFormatError(_("specified output format not supported.")) 155 | elif o == "-H": 156 | run.hard = True 157 | elif o == "--hard": 158 | run.hard = optval.get_boolean_argument(a) 159 | elif o == "-l": 160 | run.list_file_types = True 161 | elif o == "--list-file-types": 162 | run.list_file_types = optval.get_boolean_argument(a) 163 | elif o == "-L": 164 | run.localize_output = True 165 | elif o == "--localize-output": 166 | run.localize_output = optval.get_boolean_argument(a) 167 | elif o == "-m": 168 | run.include_metrics = True 169 | elif o == "--metrics": 170 | run.include_metrics = optval.get_boolean_argument(a) 171 | elif o == "-r": 172 | run.responsibilities = True 173 | elif o == "--responsibilities": 174 | run.responsibilities = optval.get_boolean_argument(a) 175 | elif o == "--since": 176 | interval.set_since(a) 177 | elif o == "--version": 178 | version.output() 179 | sys.exit(0) 180 | elif o == "--grading": 181 | grading = optval.get_boolean_argument(a) 182 | run.include_metrics = grading 183 | run.list_file_types = grading 184 | run.responsibilities = grading 185 | run.grading = grading 186 | run.hard = grading 187 | run.timeline = grading 188 | run.useweeks = grading 189 | elif o == "-T": 190 | run.timeline = True 191 | elif o == "--timeline": 192 | run.timeline = optval.get_boolean_argument(a) 193 | elif o == "--until": 194 | interval.set_until(a) 195 | elif o == "-w": 196 | run.useweeks = True 197 | elif o == "--weeks": 198 | run.useweeks = optval.get_boolean_argument(a) 199 | elif o in("-x", "--exclude"): 200 | if clear_x_on_next_pass: 201 | clear_x_on_next_pass = False 202 | filtering.clear() 203 | filtering.add(a) 204 | 205 | __check_python_version__() 206 | run.process(repos) 207 | 208 | except (filtering.InvalidRegExpError, format.InvalidFormatError, optval.InvalidOptionArgument, getopt.error) as exception: 209 | print(sys.argv[0], "\b:", exception.msg, file=sys.stderr) 210 | print(_("Try `{0} --help' for more information.").format(sys.argv[0]), file=sys.stderr) 211 | sys.exit(2) 212 | 213 | @atexit.register 214 | def cleanup(): 215 | clone.delete() 216 | 217 | if __name__ == "__main__": 218 | main() 219 | -------------------------------------------------------------------------------- /docs/gitinspector.txt: -------------------------------------------------------------------------------- 1 | // a2x -v gitinspector.txt; a2x -v -f manpage gitinspector.txt; a2x -v -f xhtml gitinspector.txt 2 | 3 | GITINSPECTOR(1) 4 | =============== 5 | :doctype: manpage 6 | :man version: 0.4.2 7 | :man source: gitinspector 8 | :man manual: The gitinspector Manual 9 | 10 | 11 | NAME 12 | ---- 13 | gitinspector - statistical analysis tool for git repositories 14 | 15 | 16 | SYNOPSIS 17 | -------- 18 | *gitinspector* [OPTION]... [REPOSITORY] 19 | 20 | 21 | DESCRIPTION 22 | ----------- 23 | Analyze and gather statistics about a git repository. The default analysis shows general statistics per author, which can be complemented with a timeline analysis that shows the workload and activity of each author. Under normal operation, gitinspector filters the results to only show statistics about a number of given extensions and by default only includes source files in the statistical analysis. 24 | 25 | Several output formats are supported, including plain text, HTML, JSON and XML. 26 | 27 | 28 | OPTIONS 29 | ------- 30 | List information about the repository in REPOSITORY. If no repository is specified, the current directory is used. If multiple repositories are given, information will be fetched from the last repository specified. 31 | 32 | Mandatory arguments to long options are mandatory for short options too. Boolean arguments can only be given to long options. 33 | 34 | *-f, --file-types*=EXTENSIONS:: 35 | A comma separated list of file extensions to include when computing statistics. The default extensions used are: java,c,cc,cpp,h,hh,hpp,py,glsl,rb,js,sql. Specifying a single '\*' asterisk character includes files with no extension. Specifying two consecutive '**' asterisk characters includes all files regardless of extension. 36 | 37 | *-F, --format*=FORMAT:: 38 | Defines in which format output should be generated; the default format is 'text' and the available formats are: html,htmlembedded,json,text,xml (see <>) 39 | 40 | *--grading*[=BOOL]:: 41 | Show statistics and information in a way that is formatted for grading of student projects; this is the same as supplying the options *-HlmrTw* 42 | 43 | *-H, --hard*[=BOOL]:: 44 | Track rows and look for duplicates harder; this can be quite slow with big repositories 45 | 46 | *-l, --list-file-types*[=BOOL]:: 47 | List all the file extensions available in the current branch of the repository 48 | 49 | *-L, --localize-output*[=BOOL]:: 50 | Localize the generated output to the selected system language if a translation is available 51 | 52 | *-m, --metrics*[=BOOL]:: 53 | Include checks for certain metrics during the analysis of commits 54 | 55 | *-r --responsibilities*[=BOOL]:: 56 | Show which files the different authors seem most responsible for 57 | 58 | *--since*=DATE:: 59 | Only show statistics for commits more recent than a specific date 60 | 61 | *-T, --timeline*[=BOOL]:: 62 | Show commit timeline, including author names 63 | 64 | *--until*=DATE:: 65 | Only show statistics for commits older than a specific date 66 | 67 | *-w, --weeks*[=BOOL]:: 68 | Show all statistical information in weeks instead of in months 69 | 70 | *-x, --exclude*=PATTERN:: 71 | An exclusion pattern describing the file paths, revisions, author names or author emails that should be excluded from the statistics; can be specified multiple times (see <>) 72 | 73 | *-h, --help*:: 74 | Display help and exit 75 | 76 | *--version*:: 77 | Output version information and exit 78 | 79 | 80 | [[X1]] 81 | OUTPUT FORMATS 82 | -------------- 83 | There is support for multiple output formats in gitinspector. They can be selected using the *-F*/*--format* flags when running the main gitinspector script. 84 | 85 | *text (plain text)*:: 86 | Plain text with some very simple ANSI formatting, suitable for console output. This is the format chosen by default in gitinspector. 87 | 88 | *html*:: 89 | HTML with external links. The generated HTML page links to some external resources; such as the JavaScript library JQuery. It requires an active internet connection to properly function. This output format will most likely also link to additional external resources in the future. 90 | 91 | *htmlembedded*:: 92 | HTML with no external links. Similar to the HTML output format, but requires no active internet connection. As a consequence; the generated pages are bigger (as certain scripts have to be embedded into the generated output). 93 | 94 | *json*:: 95 | JSON suitable for machine consumption. If you want to parse the output generated by gitinspector in a script or application of your own; this format is suitable. 96 | 97 | *xml*:: 98 | XML suitable for machine consumption. If you want to parse the output generated by gitinspector in a script or application of your own; this format is suitable. 99 | 100 | 101 | [[X2]] 102 | FILTERING 103 | --------- 104 | gitinspector offers several different ways of filtering out unwanted information from the generated statistics: 105 | 106 | * *gitinspector -x myfile*, filter out and exclude statistics from all files (or paths) with the string "myfile" 107 | * *gitinspector -x file:myfile*, filter out and exclude statistics from all files (or paths) with the string "myfile" 108 | * *gitinspector -x author:John*, filter out and exclude statistics from all authors containing the string "John" 109 | * *gitinspector -x email:@gmail.com*, filter out and exclude statistics from all authors with a gmail account 110 | * *gitinspector -x revision:8755fb33*, filter out and exclude statistics from all revisions containing the hash "8755fb33" 111 | * *gitinspector -x message:BUGFIX*, filter out and exclude statistics from all revisions containing "BUGFIX" in the commit message. 112 | 113 | The gitinspector command also lets you add multiple filtering rules by simply specifying the -x options several times or by separating each filtering rule with a comma; 114 | 115 | * *gitinspector -x author:John -x email:@gmail.com* 116 | * *gitinspector -x author:John,email:@gmail.com* 117 | 118 | Sometimes, sub-string matching (as described above) is simply not enough. Therefore, gitinspector let's you specify regular expressions as filtering rules. This makes filtering much more flexible: 119 | 120 | * *gitinspector -x "author:\^(?!(John Smith))"*, only show statistics from author "John Smith" 121 | * *gitinspector -x "author:\^(?!([A-C]))"*, only show statistics from authors starting with the letters A/B/C 122 | * *gitinspector -x "email:.com$"*, filter out statistics from all email addresses ending with ".com" 123 | 124 | 125 | USING GIT TO CONFIGURE GITINSPECTOR 126 | ----------------------------------- 127 | Options in gitinspector can be set using *git config*. Consequently, it is possible to configure gitinspector behavior globally (in all git repositories) or locally (in a specific git repository). It also means that settings will be permanently stored. All the long options that can be given to gitinspector can also be configured via git config (and take the same arguments). 128 | 129 | To configure how gitinspector should behave in all git repositories, execute the following git command: 130 | 131 | *git config --global inspector.option setting* 132 | 133 | To configure how gitinspector should behave in a specific git repository, execute the following git command (with the current directory standing inside the repository in question): 134 | 135 | *git config inspector.option setting* 136 | 137 | 138 | AUTHOR 139 | ------ 140 | Originally written by Adam Waldenberg. 141 | 142 | 143 | REPORTING BUGS 144 | -------------- 145 | Report gitinspector bugs to gitinspector@ejwa.se 146 | 147 | The gitinspector project page: 148 | 149 | If you encounter problems, be sure to read the FAQ first: 150 | 151 | There is also an issue tracker at: 152 | 153 | COPYRIGHT 154 | --------- 155 | Copyright (C) 2012-2015 Ejwa Software. All rights reserved. License GPLv3+: GNU GPL version 3 or later . 156 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. 157 | 158 | 159 | SEE ALSO 160 | -------- 161 | *git*(1) 162 | -------------------------------------------------------------------------------- /gitinspector/output/metricsoutput.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | from ..changes import FileDiff 23 | from ..localization import N_ 24 | from ..metrics import (__metric_eloc__, METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD) 25 | from .outputable import Outputable 26 | 27 | ELOC_INFO_TEXT = N_("The following files are suspiciously big (in order of severity)") 28 | CYCLOMATIC_COMPLEXITY_TEXT = N_("The following files have an elevated cyclomatic complexity (in order of severity)") 29 | CYCLOMATIC_COMPLEXITY_DENSITY_TEXT = N_("The following files have an elevated cyclomatic complexity density " \ 30 | "(in order of severity)") 31 | METRICS_MISSING_INFO_TEXT = N_("No metrics violations were found in the repository") 32 | 33 | METRICS_VIOLATION_SCORES = [[1.0, "minimal"], [1.25, "minor"], [1.5, "medium"], [2.0, "bad"], [3.0, "severe"]] 34 | 35 | def __get_metrics_score__(ceiling, value): 36 | for i in reversed(METRICS_VIOLATION_SCORES): 37 | if value > ceiling * i[0]: 38 | return i[1] 39 | 40 | class MetricsOutput(Outputable): 41 | def __init__(self, metrics): 42 | self.metrics = metrics 43 | Outputable.__init__(self) 44 | 45 | def output_text(self): 46 | if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density: 47 | print("\n" + _(METRICS_MISSING_INFO_TEXT) + ".") 48 | 49 | if self.metrics.eloc: 50 | print("\n" + _(ELOC_INFO_TEXT) + ":") 51 | for i in sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True): 52 | print(_("{0} ({1} estimated lines of code)").format(i[1], str(i[0]))) 53 | 54 | if self.metrics.cyclomatic_complexity: 55 | print("\n" + _(CYCLOMATIC_COMPLEXITY_TEXT) + ":") 56 | for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True): 57 | print(_("{0} ({1} in cyclomatic complexity)").format(i[1], str(i[0]))) 58 | 59 | if self.metrics.cyclomatic_complexity_density: 60 | print("\n" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + ":") 61 | for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True): 62 | print(_("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0])) 63 | 64 | def output_html(self): 65 | metrics_xml = "
" 66 | 67 | if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density: 68 | metrics_xml += "

" + _(METRICS_MISSING_INFO_TEXT) + ".

" 69 | 70 | if self.metrics.eloc: 71 | metrics_xml += "

" + _(ELOC_INFO_TEXT) + ".

" 72 | for num, i in enumerate(sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True)): 73 | metrics_xml += "
" if num % 2 == 1 else "\">") + \ 75 | _("{0} ({1} estimated lines of code)").format(i[1], str(i[0])) + "
" 76 | metrics_xml += "
" 77 | 78 | if self.metrics.cyclomatic_complexity: 79 | metrics_xml += "

" + _(CYCLOMATIC_COMPLEXITY_TEXT) + "

" 80 | for num, i in enumerate(sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True)): 81 | metrics_xml += "
" if num % 2 == 1 else "\">") + \ 83 | _("{0} ({1} in cyclomatic complexity)").format(i[1], str(i[0])) + "
" 84 | metrics_xml += "
" 85 | 86 | if self.metrics.cyclomatic_complexity_density: 87 | metrics_xml += "

" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + "

" 88 | for num, i in enumerate(sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True)): 89 | metrics_xml += "
" if num % 2 == 1 else "\">") + \ 91 | _("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0]) + "
" 92 | metrics_xml += "
" 93 | 94 | metrics_xml += "
" 95 | print(metrics_xml) 96 | 97 | def output_json(self): 98 | if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density: 99 | print(",\n\t\t\"metrics\": {\n\t\t\t\"message\": \"" + _(METRICS_MISSING_INFO_TEXT) + "\"\n\t\t}", end="") 100 | else: 101 | eloc_json = "" 102 | 103 | if self.metrics.eloc: 104 | for i in sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True): 105 | eloc_json += "{\n\t\t\t\t\"type\": \"estimated-lines-of-code\",\n" 106 | eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n" 107 | eloc_json += "\t\t\t\t\"value\": " + str(i[0]) + "\n" 108 | eloc_json += "\t\t\t}," 109 | else: 110 | if not self.metrics.cyclomatic_complexity: 111 | eloc_json = eloc_json[:-1] 112 | 113 | if self.metrics.cyclomatic_complexity: 114 | for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True): 115 | eloc_json += "{\n\t\t\t\t\"type\": \"cyclomatic-complexity\",\n" 116 | eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n" 117 | eloc_json += "\t\t\t\t\"value\": " + str(i[0]) + "\n" 118 | eloc_json += "\t\t\t}," 119 | else: 120 | if not self.metrics.cyclomatic_complexity_density: 121 | eloc_json = eloc_json[:-1] 122 | 123 | if self.metrics.cyclomatic_complexity_density: 124 | for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True): 125 | eloc_json += "{\n\t\t\t\t\"type\": \"cyclomatic-complexity-density\",\n" 126 | eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n" 127 | eloc_json += "\t\t\t\t\"value\": {0:.3f}\n".format(i[0]) 128 | eloc_json += "\t\t\t}," 129 | else: 130 | eloc_json = eloc_json[:-1] 131 | 132 | print(",\n\t\t\"metrics\": {\n\t\t\t\"violations\": [\n\t\t\t" + eloc_json + "]\n\t\t}", end="") 133 | def output_xml(self): 134 | if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density: 135 | print("\t\n\t\t" + _(METRICS_MISSING_INFO_TEXT) + "\n\t") 136 | else: 137 | eloc_xml = "" 138 | 139 | if self.metrics.eloc: 140 | for i in sorted(set([(j, i) for (i, j) in self.metrics.eloc.items()]), reverse=True): 141 | eloc_xml += "\t\t\t\n" 142 | eloc_xml += "\t\t\t\t" + i[1] + "\n" 143 | eloc_xml += "\t\t\t\t" + str(i[0]) + "\n" 144 | eloc_xml += "\t\t\t\n" 145 | 146 | if self.metrics.cyclomatic_complexity: 147 | for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity.items()]), reverse=True): 148 | eloc_xml += "\t\t\t\n" 149 | eloc_xml += "\t\t\t\t" + i[1] + "\n" 150 | eloc_xml += "\t\t\t\t" + str(i[0]) + "\n" 151 | eloc_xml += "\t\t\t\n" 152 | 153 | if self.metrics.cyclomatic_complexity_density: 154 | for i in sorted(set([(j, i) for (i, j) in self.metrics.cyclomatic_complexity_density.items()]), reverse=True): 155 | eloc_xml += "\t\t\t\n" 156 | eloc_xml += "\t\t\t\t" + i[1] + "\n" 157 | eloc_xml += "\t\t\t\t{0:.3f}\n".format(i[0]) 158 | eloc_xml += "\t\t\t\n" 159 | 160 | print("\t\n\t\t\n" + eloc_xml + "\t\t\n\t") 161 | -------------------------------------------------------------------------------- /gitinspector/output/changesoutput.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import json 23 | import textwrap 24 | from ..localization import N_ 25 | from .. import format, gravatar, terminal 26 | from .outputable import Outputable 27 | 28 | HISTORICAL_INFO_TEXT = N_("The following historical commit information, by author, was found") 29 | NO_COMMITED_FILES_TEXT = N_("No commited files with the specified extensions were found") 30 | 31 | class ChangesOutput(Outputable): 32 | def __init__(self, changes): 33 | self.changes = changes 34 | Outputable.__init__(self) 35 | 36 | def output_html(self): 37 | authorinfo_list = self.changes.get_authorinfo_list() 38 | total_changes = 0.0 39 | changes_xml = "
" 40 | chart_data = "" 41 | 42 | for i in authorinfo_list: 43 | total_changes += authorinfo_list.get(i).insertions 44 | total_changes += authorinfo_list.get(i).deletions 45 | 46 | if authorinfo_list: 47 | changes_xml += "

" + _(HISTORICAL_INFO_TEXT) + ".

" 48 | changes_xml += "".format( 49 | _("Author"), _("Commits"), _("Insertions"), _("Deletions"), _("% of changes")) 50 | changes_xml += "" 51 | 52 | for i, entry in enumerate(sorted(authorinfo_list)): 53 | authorinfo = authorinfo_list.get(entry) 54 | percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 55 | 56 | changes_xml += "" if i % 2 == 1 else ">") 57 | 58 | if format.get_selected() == "html": 59 | changes_xml += "".format( 60 | gravatar.get_url(self.changes.get_latest_email_by_author(entry)), entry) 61 | else: 62 | changes_xml += "" 63 | 64 | changes_xml += "" 65 | changes_xml += "" 66 | changes_xml += "" 67 | changes_xml += "" 68 | changes_xml += "" 69 | chart_data += "{{label: {0}, data: {1}}}".format(json.dumps(entry), "{0:.2f}".format(percentage)) 70 | 71 | if sorted(authorinfo_list)[-1] != entry: 72 | chart_data += ", " 73 | 74 | changes_xml += ("
{0} {1} {2} {3} {4}
{1}" + entry + "" + str(authorinfo.commits) + "" + str(authorinfo.insertions) + "" + str(authorinfo.deletions) + "" + "{0:.2f}".format(percentage) + "
 
") 75 | changes_xml += "
" 76 | changes_xml += "" 92 | else: 93 | changes_xml += "

" + _(NO_COMMITED_FILES_TEXT) + ".

" 94 | 95 | changes_xml += "
" 96 | print(changes_xml) 97 | 98 | def output_json(self): 99 | authorinfo_list = self.changes.get_authorinfo_list() 100 | total_changes = 0.0 101 | 102 | for i in authorinfo_list: 103 | total_changes += authorinfo_list.get(i).insertions 104 | total_changes += authorinfo_list.get(i).deletions 105 | 106 | if authorinfo_list: 107 | message_json = "\t\t\t\"message\": \"" + _(HISTORICAL_INFO_TEXT) + "\",\n" 108 | changes_json = "" 109 | 110 | for i in sorted(authorinfo_list): 111 | author_email = self.changes.get_latest_email_by_author(i) 112 | authorinfo = authorinfo_list.get(i) 113 | 114 | percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 115 | name_json = "\t\t\t\t\"name\": \"" + i + "\",\n" 116 | email_json = "\t\t\t\t\"email\": \"" + author_email + "\",\n" 117 | gravatar_json = "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n" 118 | commits_json = "\t\t\t\t\"commits\": " + str(authorinfo.commits) + ",\n" 119 | insertions_json = "\t\t\t\t\"insertions\": " + str(authorinfo.insertions) + ",\n" 120 | deletions_json = "\t\t\t\t\"deletions\": " + str(authorinfo.deletions) + ",\n" 121 | percentage_json = "\t\t\t\t\"percentage_of_changes\": " + "{0:.2f}".format(percentage) + "\n" 122 | 123 | changes_json += ("{\n" + name_json + email_json + gravatar_json + commits_json + 124 | insertions_json + deletions_json + percentage_json + "\t\t\t}") 125 | changes_json += "," 126 | else: 127 | changes_json = changes_json[:-1] 128 | 129 | print("\t\t\"changes\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + changes_json + "]\n\t\t}", end="") 130 | else: 131 | print("\t\t\"exception\": \"" + _(NO_COMMITED_FILES_TEXT) + "\"") 132 | 133 | def output_text(self): 134 | authorinfo_list = self.changes.get_authorinfo_list() 135 | total_changes = 0.0 136 | 137 | for i in authorinfo_list: 138 | total_changes += authorinfo_list.get(i).insertions 139 | total_changes += authorinfo_list.get(i).deletions 140 | 141 | if authorinfo_list: 142 | print(textwrap.fill(_(HISTORICAL_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n") 143 | terminal.printb(terminal.ljust(_("Author"), 21) + terminal.rjust(_("Commits"), 13) + 144 | terminal.rjust(_("Insertions"), 14) + terminal.rjust(_("Deletions"), 15) + 145 | terminal.rjust(_("% of changes"), 16)) 146 | 147 | for i in sorted(authorinfo_list): 148 | authorinfo = authorinfo_list.get(i) 149 | percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 150 | 151 | print(terminal.ljust(i, 20)[0:20 - terminal.get_excess_column_count(i)], end=" ") 152 | print(str(authorinfo.commits).rjust(13), end=" ") 153 | print(str(authorinfo.insertions).rjust(13), end=" ") 154 | print(str(authorinfo.deletions).rjust(14), end=" ") 155 | print("{0:.2f}".format(percentage).rjust(15)) 156 | else: 157 | print(_(NO_COMMITED_FILES_TEXT) + ".") 158 | 159 | def output_xml(self): 160 | authorinfo_list = self.changes.get_authorinfo_list() 161 | total_changes = 0.0 162 | 163 | for i in authorinfo_list: 164 | total_changes += authorinfo_list.get(i).insertions 165 | total_changes += authorinfo_list.get(i).deletions 166 | 167 | if authorinfo_list: 168 | message_xml = "\t\t" + _(HISTORICAL_INFO_TEXT) + "\n" 169 | changes_xml = "" 170 | 171 | for i in sorted(authorinfo_list): 172 | author_email = self.changes.get_latest_email_by_author(i) 173 | authorinfo = authorinfo_list.get(i) 174 | 175 | percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 176 | name_xml = "\t\t\t\t" + i + "\n" 177 | email_xml = "\t\t\t\t" + author_email + "\n" 178 | gravatar_xml = "\t\t\t\t" + gravatar.get_url(author_email) + "\n" 179 | commits_xml = "\t\t\t\t" + str(authorinfo.commits) + "\n" 180 | insertions_xml = "\t\t\t\t" + str(authorinfo.insertions) + "\n" 181 | deletions_xml = "\t\t\t\t" + str(authorinfo.deletions) + "\n" 182 | percentage_xml = "\t\t\t\t" + "{0:.2f}".format(percentage) + "\n" 183 | 184 | changes_xml += ("\t\t\t\n" + name_xml + email_xml + gravatar_xml + commits_xml + 185 | insertions_xml + deletions_xml + percentage_xml + "\t\t\t\n") 186 | 187 | print("\t\n" + message_xml + "\t\t\n" + changes_xml + "\t\t\n\t") 188 | else: 189 | print("\t\n\t\t" + _(NO_COMMITED_FILES_TEXT) + "\n\t") 190 | -------------------------------------------------------------------------------- /gitinspector/output/timelineoutput.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import print_function 21 | from __future__ import unicode_literals 22 | import textwrap 23 | from ..localization import N_ 24 | from .. import format, gravatar, terminal, timeline 25 | from .outputable import Outputable 26 | 27 | TIMELINE_INFO_TEXT = N_("The following history timeline has been gathered from the repository") 28 | MODIFIED_ROWS_TEXT = N_("Modified Rows:") 29 | 30 | def __output_row__text__(timeline_data, periods, names): 31 | print("\n" + terminal.__bold__ + terminal.ljust(_("Author"), 20), end=" ") 32 | 33 | for period in periods: 34 | print(terminal.rjust(period, 10), end=" ") 35 | 36 | print(terminal.__normal__) 37 | 38 | for name in names: 39 | if timeline_data.is_author_in_periods(periods, name[0]): 40 | print(terminal.ljust(name[0], 20)[0:20 - terminal.get_excess_column_count(name[0])], end=" ") 41 | 42 | for period in periods: 43 | multiplier = timeline_data.get_multiplier(period, 9) 44 | signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) 45 | signs_str = (signs[1] * "-" + signs[0] * "+") 46 | print (("." if timeline_data.is_author_in_period(period, name[0]) and 47 | len(signs_str) == 0 else signs_str).rjust(10), end=" ") 48 | print("") 49 | 50 | print(terminal.__bold__ + terminal.ljust(_(MODIFIED_ROWS_TEXT), 20) + terminal.__normal__, end=" ") 51 | 52 | for period in periods: 53 | total_changes = str(timeline_data.get_total_changes_in_period(period)[2]) 54 | 55 | if hasattr(total_changes, 'decode'): 56 | total_changes = total_changes.decode("utf-8", "replace") 57 | 58 | print(terminal.rjust(total_changes, 10), end=" ") 59 | 60 | print("") 61 | 62 | def __output_row__html__(timeline_data, periods, names): 63 | timeline_xml = "" 64 | 65 | for period in periods: 66 | timeline_xml += "" 67 | 68 | timeline_xml += "" 69 | i = 0 70 | 71 | for name in names: 72 | if timeline_data.is_author_in_periods(periods, name[0]): 73 | timeline_xml += "" if i % 2 == 1 else ">") 74 | 75 | if format.get_selected() == "html": 76 | timeline_xml += "".format(gravatar.get_url(name[1]), name[0]) 77 | else: 78 | timeline_xml += "" 79 | 80 | for period in periods: 81 | multiplier = timeline_data.get_multiplier(period, 18) 82 | signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) 83 | signs_str = (signs[1] * "
 
" + signs[0] * "
 
") 84 | 85 | timeline_xml += "" 87 | timeline_xml += "" 88 | i = i + 1 89 | 90 | timeline_xml += "" 91 | 92 | for period in periods: 93 | total_changes = timeline_data.get_total_changes_in_period(period) 94 | timeline_xml += "" 95 | 96 | timeline_xml += "
" + _("Author") + "" + str(period) + "
{1}" + name[0] + "" + ("." if timeline_data.is_author_in_period(period, name[0]) and len(signs_str) == 0 else signs_str) 86 | timeline_xml += "
" + _(MODIFIED_ROWS_TEXT) + "" + str(total_changes[2]) + "
" 97 | print(timeline_xml) 98 | 99 | class TimelineOutput(Outputable): 100 | def __init__(self, changes, useweeks): 101 | self.changes = changes 102 | self.useweeks = useweeks 103 | Outputable.__init__(self) 104 | 105 | def output_text(self): 106 | if self.changes.get_commits(): 107 | print("\n" + textwrap.fill(_(TIMELINE_INFO_TEXT) + ":", width=terminal.get_size()[0])) 108 | 109 | timeline_data = timeline.TimelineData(self.changes, self.useweeks) 110 | periods = timeline_data.get_periods() 111 | names = timeline_data.get_authors() 112 | (width, _unused) = terminal.get_size() 113 | max_periods_per_row = int((width - 21) / 11) 114 | 115 | for i in range(0, len(periods), max_periods_per_row): 116 | __output_row__text__(timeline_data, periods[i:i+max_periods_per_row], names) 117 | 118 | def output_html(self): 119 | if self.changes.get_commits(): 120 | timeline_data = timeline.TimelineData(self.changes, self.useweeks) 121 | periods = timeline_data.get_periods() 122 | names = timeline_data.get_authors() 123 | max_periods_per_row = 8 124 | 125 | timeline_xml = "
" 126 | timeline_xml += "

" + _(TIMELINE_INFO_TEXT) + ".

" 127 | print(timeline_xml) 128 | 129 | for i in range(0, len(periods), max_periods_per_row): 130 | __output_row__html__(timeline_data, periods[i:i+max_periods_per_row], names) 131 | 132 | timeline_xml = "
" 133 | print(timeline_xml) 134 | 135 | def output_json(self): 136 | if self.changes.get_commits(): 137 | message_json = "\t\t\t\"message\": \"" + _(TIMELINE_INFO_TEXT) + "\",\n" 138 | timeline_json = "" 139 | periods_json = "\t\t\t\"period_length\": \"{0}\",\n".format("week" if self.useweeks else "month") 140 | periods_json += "\t\t\t\"periods\": [\n\t\t\t" 141 | 142 | timeline_data = timeline.TimelineData(self.changes, self.useweeks) 143 | periods = timeline_data.get_periods() 144 | names = timeline_data.get_authors() 145 | 146 | for period in periods: 147 | name_json = "\t\t\t\t\"name\": \"" + str(period) + "\",\n" 148 | authors_json = "\t\t\t\t\"authors\": [\n\t\t\t\t" 149 | 150 | for name in names: 151 | if timeline_data.is_author_in_period(period, name[0]): 152 | multiplier = timeline_data.get_multiplier(period, 24) 153 | signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) 154 | signs_str = (signs[1] * "-" + signs[0] * "+") 155 | 156 | if len(signs_str) == 0: 157 | signs_str = "." 158 | 159 | authors_json += "{\n\t\t\t\t\t\"name\": \"" + name[0] + "\",\n" 160 | authors_json += "\t\t\t\t\t\"email\": \"" + name[1] + "\",\n" 161 | authors_json += "\t\t\t\t\t\"gravatar\": \"" + gravatar.get_url(name[1]) + "\",\n" 162 | authors_json += "\t\t\t\t\t\"work\": \"" + signs_str + "\"\n\t\t\t\t}," 163 | else: 164 | authors_json = authors_json[:-1] 165 | 166 | authors_json += "],\n" 167 | modified_rows_json = "\t\t\t\t\"modified_rows\": " + \ 168 | str(timeline_data.get_total_changes_in_period(period)[2]) + "\n" 169 | timeline_json += "{\n" + name_json + authors_json + modified_rows_json + "\t\t\t}," 170 | else: 171 | timeline_json = timeline_json[:-1] 172 | 173 | print(",\n\t\t\"timeline\": {\n" + message_json + periods_json + timeline_json + "]\n\t\t}", end="") 174 | 175 | def output_xml(self): 176 | if self.changes.get_commits(): 177 | message_xml = "\t\t" + _(TIMELINE_INFO_TEXT) + "\n" 178 | timeline_xml = "" 179 | periods_xml = "\t\t\n".format("week" if self.useweeks else "month") 180 | 181 | timeline_data = timeline.TimelineData(self.changes, self.useweeks) 182 | periods = timeline_data.get_periods() 183 | names = timeline_data.get_authors() 184 | 185 | for period in periods: 186 | name_xml = "\t\t\t\t" + str(period) + "\n" 187 | authors_xml = "\t\t\t\t\n" 188 | 189 | for name in names: 190 | if timeline_data.is_author_in_period(period, name[0]): 191 | multiplier = timeline_data.get_multiplier(period, 24) 192 | signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) 193 | signs_str = (signs[1] * "-" + signs[0] * "+") 194 | 195 | if len(signs_str) == 0: 196 | signs_str = "." 197 | 198 | authors_xml += "\t\t\t\t\t\n\t\t\t\t\t\t" + name[0] + "\n" 199 | authors_xml += "\t\t\t\t\t\t" + name[1] + "\n" 200 | authors_xml += "\t\t\t\t\t\t" + gravatar.get_url(name[1]) + "\n" 201 | authors_xml += "\t\t\t\t\t\t" + signs_str + "\n\t\t\t\t\t\n" 202 | 203 | authors_xml += "\t\t\t\t\n" 204 | modified_rows_xml = "\t\t\t\t" + \ 205 | str(timeline_data.get_total_changes_in_period(period)[2]) + "\n" 206 | timeline_xml += "\t\t\t\n" + name_xml + authors_xml + modified_rows_xml + "\t\t\t\n" 207 | 208 | print("\t\n" + message_xml + periods_xml + timeline_xml + "\t\t\n\t") 209 | -------------------------------------------------------------------------------- /gitinspector/translations/messages.pot: -------------------------------------------------------------------------------- 1 | # Copyright © 2012-2015 Ejwa Software. All rights reserved. 2 | # 3 | # This file is part of gitinspector. 4 | # 5 | # gitinspector is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # gitinspector is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with gitinspector. If not, see . 17 | # 18 | # This file was generated with the following command: 19 | # 20 | # xgettext --no-wrap -s -w 140 --language=Python --keyword="N_" 21 | # --no-location --output=messages.pot *.py 22 | # 23 | #, fuzzy 24 | 25 | msgid "" 26 | msgstr "" 27 | "Project-Id-Version: gitinspector 0.5.0dev\n" 28 | "Report-Msgid-Bugs-To: gitinspector@ejwa.se\n" 29 | "POT-Creation-Date: 2015-10-02 03:35+0200\n" 30 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 31 | "Last-Translator: Adam Waldenberg \n" 32 | "MIME-Version: 1.0\n" 33 | "Content-Type: text/plain; charset=UTF-8\n" 34 | "Content-Transfer-Encoding: 8bit\n" 35 | 36 | #, python-format 37 | msgid "% in comments" 38 | msgstr "" 39 | 40 | #, python-format 41 | msgid "% of changes" 42 | msgstr "" 43 | 44 | msgid "(extensions used during statistical analysis are marked)" 45 | msgstr "" 46 | 47 | msgid "Age" 48 | msgstr "" 49 | 50 | msgid "Author" 51 | msgstr "" 52 | 53 | msgid "Below are the number of rows from each author that have survived and are still intact in the current revision" 54 | msgstr "" 55 | 56 | #, python-brace-format 57 | msgid "Checking how many rows belong to each author (Progress): {0:.0f}%" 58 | msgstr "" 59 | 60 | msgid "Commits" 61 | msgstr "" 62 | 63 | msgid "" 64 | "Copyright © 2012-2015 Ejwa Software. All rights reserved.\n" 65 | "License GPLv3+: GNU GPL version 3 or later .\n" 66 | "This is free software: you are free to change and redistribute it.\n" 67 | "There is NO WARRANTY, to the extent permitted by law.\n" 68 | "\n" 69 | "Written by Adam Waldenberg." 70 | msgstr "" 71 | 72 | msgid "Deletions" 73 | msgstr "" 74 | 75 | #, python-format 76 | msgid "Error processing git repository at \"%s\"." 77 | msgstr "" 78 | 79 | msgid "HTML output not yet supported in" 80 | msgstr "" 81 | 82 | msgid "Hide minor authors" 83 | msgstr "" 84 | 85 | msgid "Hide rows with minor work" 86 | msgstr "" 87 | 88 | msgid "Insertions" 89 | msgstr "" 90 | 91 | msgid "Minor Authors" 92 | msgstr "" 93 | 94 | msgid "Modified Rows:" 95 | msgstr "" 96 | 97 | msgid "No commited files with the specified extensions were found" 98 | msgstr "" 99 | 100 | msgid "No metrics violations were found in the repository" 101 | msgstr "" 102 | 103 | #, python-brace-format 104 | msgid "Repository statistics for {0}" 105 | msgstr "" 106 | 107 | msgid "Rows" 108 | msgstr "" 109 | 110 | msgid "Show minor authors" 111 | msgstr "" 112 | 113 | msgid "Show rows with minor work" 114 | msgstr "" 115 | 116 | msgid "Stability" 117 | msgstr "" 118 | 119 | #, python-brace-format 120 | msgid "Statistical information for the repository '{0}' was gathered on {1}." 121 | msgstr "" 122 | 123 | msgid "Text output not yet supported in" 124 | msgstr "" 125 | 126 | msgid "The authors with the following emails were excluded from the statistics due to the specified exclusion patterns" 127 | msgstr "" 128 | 129 | msgid "The extensions below were found in the repository history" 130 | msgstr "" 131 | 132 | msgid "The following authors were excluded from the statistics due to the specified exclusion patterns" 133 | msgstr "" 134 | 135 | msgid "The following commit revisions were excluded from the statistics due to the specified exclusion patterns" 136 | msgstr "" 137 | 138 | msgid "The following files are suspiciously big (in order of severity)" 139 | msgstr "" 140 | 141 | msgid "The following files have an elevated cyclomatic complexity (in order of severity)" 142 | msgstr "" 143 | 144 | msgid "The following files have an elevated cyclomatic complexity density (in order of severity)" 145 | msgstr "" 146 | 147 | msgid "The following files were excluded from the statistics due to the specified exclusion patterns" 148 | msgstr "" 149 | 150 | msgid "The following historical commit information, by author, was found in the repository" 151 | msgstr "" 152 | 153 | msgid "The following history timeline has been gathered from the repository" 154 | msgstr "" 155 | 156 | msgid "The following responsibilities, by author, were found in the current revision of the repository (comments are excluded from the line count, if possible)" 157 | msgstr "" 158 | 159 | msgid "The given option argument is not a valid boolean." 160 | msgstr "" 161 | 162 | #, python-brace-format 163 | msgid "The output has been generated by {0} {1}. The statistical analysis tool for git repositories." 164 | msgstr "" 165 | 166 | #, python-brace-format 167 | msgid "Try `{0} --help' for more information." 168 | msgstr "" 169 | 170 | msgid "Unable to determine absolute path of git repository." 171 | msgstr "" 172 | 173 | #, python-brace-format 174 | msgid "" 175 | "Usage: {0} [OPTION]... [REPOSITORY]\n" 176 | "List information about the repository in REPOSITORY. If no repository is\n" 177 | "specified, the current directory is used. If multiple repositories are\n" 178 | "given, information will be fetched from the last repository specified.\n" 179 | "\n" 180 | "Mandatory arguments to long options are mandatory for short options too.\n" 181 | "Boolean arguments can only be given to long options.\n" 182 | " -f, --file-types=EXTENSIONS a comma separated list of file extensions to\n" 183 | " include when computing statistics. The\n" 184 | " default extensions used are:\n" 185 | " {1}\n" 186 | " Specifying * includes files with no\n" 187 | " extension, while ** includes all files\n" 188 | " -F, --format=FORMAT define in which format output should be\n" 189 | " generated; the default format is 'text' and\n" 190 | " the available formats are:\n" 191 | " {2}\n" 192 | " --grading[=BOOL] show statistics and information in a way that\n" 193 | " is formatted for grading of student\n" 194 | " projects; this is the same as supplying the\n" 195 | " options -HlmrTw\n" 196 | " -H, --hard[=BOOL] track rows and look for duplicates harder;\n" 197 | " this can be quite slow with big repositories\n" 198 | " -l, --list-file-types[=BOOL] list all the file extensions available in the\n" 199 | " current branch of the repository\n" 200 | " -L, --localize-output[=BOOL] localize the generated output to the selected\n" 201 | " system language if a translation is\n" 202 | " available\n" 203 | " -m --metrics[=BOOL] include checks for certain metrics during the\n" 204 | " analysis of commits\n" 205 | " -r --responsibilities[=BOOL] show which files the different authors seem\n" 206 | " most responsible for\n" 207 | " --since=DATE only show statistics for commits more recent\n" 208 | " than a specific date\n" 209 | " -T, --timeline[=BOOL] show commit timeline, including author names\n" 210 | " --until=DATE only show statistics for commits older than a\n" 211 | " specific date\n" 212 | " -w, --weeks[=BOOL] show all statistical information in weeks\n" 213 | " instead of in months\n" 214 | " -x, --exclude=PATTERN an exclusion pattern describing the file\n" 215 | " paths, revisions, revisions with certain\n" 216 | " commit messages, author names or author\n" 217 | " emails that should be excluded from the\n" 218 | " statistics; can be specified multiple times\n" 219 | " -h, --help display this help and exit\n" 220 | " --version output version information and exit\n" 221 | "\n" 222 | "gitinspector will filter statistics to only include commits that modify,\n" 223 | "add or remove one of the specified extensions, see -f or --file-types for\n" 224 | "more information.\n" 225 | "\n" 226 | "gitinspector requires that the git executable is available in your PATH.\n" 227 | "Report gitinspector bugs to gitinspector@ejwa.se." 228 | msgstr "" 229 | 230 | msgid "WARNING: The terminal encoding is not correctly configured. gitinspector might malfunction. The encoding can be configured with the environment variable 'PYTHONIOENCODING'." 231 | msgstr "" 232 | 233 | msgid "XML output not yet supported in" 234 | msgstr "" 235 | 236 | #, python-brace-format 237 | msgid "gitinspector requires at least Python 2.6 to run (version {0} was found)." 238 | msgstr "" 239 | 240 | msgid "invalid regular expression specified" 241 | msgstr "" 242 | 243 | msgid "is mostly responsible for" 244 | msgstr "" 245 | 246 | msgid "specified output format not supported." 247 | msgstr "" 248 | 249 | #, python-brace-format 250 | msgid "{0} ({1:.3f} in cyclomatic complexity density)" 251 | msgstr "" 252 | 253 | #, python-brace-format 254 | msgid "{0} ({1} estimated lines of code)" 255 | msgstr "" 256 | 257 | #, python-brace-format 258 | msgid "{0} ({1} in cyclomatic complexity)" 259 | msgstr "" 260 | -------------------------------------------------------------------------------- /gitinspector/changes.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | # 3 | # Copyright © 2012-2017 Ejwa Software. All rights reserved. 4 | # 5 | # This file is part of gitinspector. 6 | # 7 | # gitinspector is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # gitinspector is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with gitinspector. If not, see . 19 | 20 | from __future__ import division 21 | from __future__ import unicode_literals 22 | import bisect 23 | import datetime 24 | import multiprocessing 25 | import os 26 | import subprocess 27 | import threading 28 | from .localization import N_ 29 | from . import extensions, filtering, format, interval, terminal 30 | 31 | CHANGES_PER_THREAD = 200 32 | NUM_THREADS = multiprocessing.cpu_count() 33 | 34 | __thread_lock__ = threading.BoundedSemaphore(NUM_THREADS) 35 | __changes_lock__ = threading.Lock() 36 | 37 | class FileDiff(object): 38 | def __init__(self, string): 39 | commit_line = string.split("|") 40 | 41 | if commit_line.__len__() == 2: 42 | self.name = commit_line[0].strip() 43 | self.insertions = commit_line[1].count("+") 44 | self.deletions = commit_line[1].count("-") 45 | 46 | @staticmethod 47 | def is_filediff_line(string): 48 | string = string.split("|") 49 | return string.__len__() == 2 and string[1].find("Bin") == -1 and ('+' in string[1] or '-' in string[1]) 50 | 51 | @staticmethod 52 | def get_extension(string): 53 | string = string.split("|")[0].strip().strip("{}").strip("\"").strip("'") 54 | return os.path.splitext(string)[1][1:] 55 | 56 | @staticmethod 57 | def get_filename(string): 58 | return string.split("|")[0].strip().strip("{}").strip("\"").strip("'") 59 | 60 | @staticmethod 61 | def is_valid_extension(string): 62 | extension = FileDiff.get_extension(string) 63 | 64 | for i in extensions.get(): 65 | if (extension == "" and i == "*") or extension == i or i == '**': 66 | return True 67 | return False 68 | 69 | class Commit(object): 70 | def __init__(self, string): 71 | self.filediffs = [] 72 | commit_line = string.split("|") 73 | 74 | if commit_line.__len__() == 5: 75 | self.timestamp = commit_line[0] 76 | self.date = commit_line[1] 77 | self.sha = commit_line[2] 78 | self.author = commit_line[3].strip() 79 | self.email = commit_line[4].strip() 80 | 81 | def __lt__(self, other): 82 | return self.timestamp.__lt__(other.timestamp) # only used for sorting; we just consider the timestamp. 83 | 84 | def add_filediff(self, filediff): 85 | self.filediffs.append(filediff) 86 | 87 | def get_filediffs(self): 88 | return self.filediffs 89 | 90 | @staticmethod 91 | def get_author_and_email(string): 92 | commit_line = string.split("|") 93 | 94 | if commit_line.__len__() == 5: 95 | return (commit_line[3].strip(), commit_line[4].strip()) 96 | 97 | @staticmethod 98 | def is_commit_line(string): 99 | return string.split("|").__len__() == 5 100 | 101 | class AuthorInfo(object): 102 | email = None 103 | insertions = 0 104 | deletions = 0 105 | commits = 0 106 | 107 | class ChangesThread(threading.Thread): 108 | def __init__(self, hard, changes, first_hash, second_hash, offset): 109 | __thread_lock__.acquire() # Lock controlling the number of threads running 110 | threading.Thread.__init__(self) 111 | 112 | self.hard = hard 113 | self.changes = changes 114 | self.first_hash = first_hash 115 | self.second_hash = second_hash 116 | self.offset = offset 117 | 118 | @staticmethod 119 | def create(hard, changes, first_hash, second_hash, offset): 120 | thread = ChangesThread(hard, changes, first_hash, second_hash, offset) 121 | thread.daemon = True 122 | thread.start() 123 | 124 | def run(self): 125 | git_log_r = subprocess.Popen(filter(None, ["git", "log", "--reverse", "--pretty=%ct|%cd|%H|%aN|%aE", 126 | "--stat=100000,8192", "--no-merges", "-w", interval.get_since(), 127 | interval.get_until(), "--date=short"] + (["-C", "-C", "-M"] if self.hard else []) + 128 | [self.first_hash + self.second_hash]), bufsize=1, stdout=subprocess.PIPE).stdout 129 | lines = git_log_r.readlines() 130 | git_log_r.close() 131 | 132 | commit = None 133 | found_valid_extension = False 134 | is_filtered = False 135 | commits = [] 136 | 137 | __changes_lock__.acquire() # Global lock used to protect calls from here... 138 | 139 | for i in lines: 140 | j = i.strip().decode("unicode_escape", "ignore") 141 | j = j.encode("latin-1", "replace") 142 | j = j.decode("utf-8", "replace") 143 | 144 | if Commit.is_commit_line(j): 145 | (author, email) = Commit.get_author_and_email(j) 146 | self.changes.emails_by_author[author] = email 147 | self.changes.authors_by_email[email] = author 148 | 149 | if Commit.is_commit_line(j) or i is lines[-1]: 150 | if found_valid_extension: 151 | bisect.insort(commits, commit) 152 | 153 | found_valid_extension = False 154 | is_filtered = False 155 | commit = Commit(j) 156 | 157 | if Commit.is_commit_line(j) and \ 158 | (filtering.set_filtered(commit.author, "author") or \ 159 | filtering.set_filtered(commit.email, "email") or \ 160 | filtering.set_filtered(commit.sha, "revision") or \ 161 | filtering.set_filtered(commit.sha, "message")): 162 | is_filtered = True 163 | 164 | if FileDiff.is_filediff_line(j) and not \ 165 | filtering.set_filtered(FileDiff.get_filename(j)) and not is_filtered: 166 | extensions.add_located(FileDiff.get_extension(j)) 167 | 168 | if FileDiff.is_valid_extension(j): 169 | found_valid_extension = True 170 | filediff = FileDiff(j) 171 | commit.add_filediff(filediff) 172 | 173 | self.changes.commits[self.offset // CHANGES_PER_THREAD] = commits 174 | __changes_lock__.release() # ...to here. 175 | __thread_lock__.release() # Lock controlling the number of threads running 176 | 177 | PROGRESS_TEXT = N_("Fetching and calculating primary statistics (1 of 2): {0:.0f}%") 178 | 179 | class Changes(object): 180 | authors = {} 181 | authors_dateinfo = {} 182 | authors_by_email = {} 183 | emails_by_author = {} 184 | 185 | def __init__(self, repo, hard): 186 | self.commits = [] 187 | interval.set_ref("HEAD"); 188 | git_rev_list_p = subprocess.Popen(filter(None, ["git", "rev-list", "--reverse", "--no-merges", 189 | interval.get_since(), interval.get_until(), "HEAD"]), bufsize=1, 190 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) 191 | lines = git_rev_list_p.communicate()[0].splitlines() 192 | git_rev_list_p.stdout.close() 193 | 194 | if git_rev_list_p.returncode == 0 and len(lines) > 0: 195 | progress_text = _(PROGRESS_TEXT) 196 | if repo != None: 197 | progress_text = "[%s] " % repo.name + progress_text 198 | 199 | chunks = len(lines) // CHANGES_PER_THREAD 200 | self.commits = [None] * (chunks if len(lines) % CHANGES_PER_THREAD == 0 else chunks + 1) 201 | first_hash = "" 202 | 203 | for i, entry in enumerate(lines): 204 | if i % CHANGES_PER_THREAD == CHANGES_PER_THREAD - 1: 205 | entry = entry.decode("utf-8", "replace").strip() 206 | second_hash = entry 207 | ChangesThread.create(hard, self, first_hash, second_hash, i) 208 | first_hash = entry + ".." 209 | 210 | if format.is_interactive_format(): 211 | terminal.output_progress(progress_text, i, len(lines)) 212 | else: 213 | if CHANGES_PER_THREAD - 1 != i % CHANGES_PER_THREAD: 214 | entry = entry.decode("utf-8", "replace").strip() 215 | second_hash = entry 216 | ChangesThread.create(hard, self, first_hash, second_hash, i) 217 | 218 | # Make sure all threads have completed. 219 | for i in range(0, NUM_THREADS): 220 | __thread_lock__.acquire() 221 | 222 | # We also have to release them for future use. 223 | for i in range(0, NUM_THREADS): 224 | __thread_lock__.release() 225 | 226 | self.commits = [item for sublist in self.commits for item in sublist] 227 | 228 | if len(self.commits) > 0: 229 | if interval.has_interval(): 230 | interval.set_ref(self.commits[-1].sha) 231 | 232 | self.first_commit_date = datetime.date(int(self.commits[0].date[0:4]), int(self.commits[0].date[5:7]), 233 | int(self.commits[0].date[8:10])) 234 | self.last_commit_date = datetime.date(int(self.commits[-1].date[0:4]), int(self.commits[-1].date[5:7]), 235 | int(self.commits[-1].date[8:10])) 236 | 237 | def __iadd__(self, other): 238 | try: 239 | self.authors.update(other.authors) 240 | self.authors_dateinfo.update(other.authors_dateinfo) 241 | self.authors_by_email.update(other.authors_by_email) 242 | self.emails_by_author.update(other.emails_by_author) 243 | 244 | for commit in other.commits: 245 | bisect.insort(self.commits, commit) 246 | if not self.commits and not other.commits: 247 | self.commits = [] 248 | 249 | return self 250 | except AttributeError: 251 | return other 252 | 253 | def get_commits(self): 254 | return self.commits 255 | 256 | @staticmethod 257 | def modify_authorinfo(authors, key, commit): 258 | if authors.get(key, None) == None: 259 | authors[key] = AuthorInfo() 260 | 261 | if commit.get_filediffs(): 262 | authors[key].commits += 1 263 | 264 | for j in commit.get_filediffs(): 265 | authors[key].insertions += j.insertions 266 | authors[key].deletions += j.deletions 267 | 268 | def get_authorinfo_list(self): 269 | if not self.authors: 270 | for i in self.commits: 271 | Changes.modify_authorinfo(self.authors, i.author, i) 272 | 273 | return self.authors 274 | 275 | def get_authordateinfo_list(self): 276 | if not self.authors_dateinfo: 277 | for i in self.commits: 278 | Changes.modify_authorinfo(self.authors_dateinfo, (i.date, i.author), i) 279 | 280 | return self.authors_dateinfo 281 | 282 | def get_latest_author_by_email(self, name): 283 | if not hasattr(name, "decode"): 284 | name = str.encode(name) 285 | try: 286 | name = name.decode("unicode_escape", "ignore") 287 | except UnicodeEncodeError: 288 | pass 289 | 290 | return self.authors_by_email[name] 291 | 292 | def get_latest_email_by_author(self, name): 293 | return self.emails_by_author[name] 294 | -------------------------------------------------------------------------------- /docs/gitinspector.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Title: gitinspector 3 | .\" Author: [see the "AUTHOR" section] 4 | .\" Generator: DocBook XSL Stylesheets v1.78.1 5 | .\" Date: 12/14/2015 6 | .\" Manual: The gitinspector Manual 7 | .\" Source: gitinspector 0.4.2 8 | .\" Language: English 9 | .\" 10 | .TH "GITINSPECTOR" "1" "12/14/2015" "gitinspector 0\&.4\&.2" "The gitinspector Manual" 11 | .\" ----------------------------------------------------------------- 12 | .\" * Define some portability stuff 13 | .\" ----------------------------------------------------------------- 14 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 15 | .\" http://bugs.debian.org/507673 16 | .\" http://lists.gnu.org/archive/html/groff/2009-02/msg00013.html 17 | .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 18 | .ie \n(.g .ds Aq \(aq 19 | .el .ds Aq ' 20 | .\" ----------------------------------------------------------------- 21 | .\" * set default formatting 22 | .\" ----------------------------------------------------------------- 23 | .\" disable hyphenation 24 | .nh 25 | .\" disable justification (adjust text to left margin only) 26 | .ad l 27 | .\" ----------------------------------------------------------------- 28 | .\" * MAIN CONTENT STARTS HERE * 29 | .\" ----------------------------------------------------------------- 30 | .SH "NAME" 31 | gitinspector \- statistical analysis tool for git repositories 32 | .SH "SYNOPSIS" 33 | .sp 34 | \fBgitinspector\fR [OPTION]\&... [REPOSITORY] 35 | .SH "DESCRIPTION" 36 | .sp 37 | Analyze and gather statistics about a git repository\&. The defaut analysis shows general statistics per author, which can be complemented with a timeline analysis that shows the workload and activity of each author\&. Under normal operation, gitinspector filters the results to only show statistics about a number of given extensions and by default only includes source files in the statistical analysis\&. 38 | .sp 39 | Several output formats are supported, including plain text, HTML, JSON and XML\&. 40 | .SH "OPTIONS" 41 | .sp 42 | List information about the repository in REPOSITORY\&. If no repository is specified, the current directory is used\&. If multiple repositories are given, information will be fetched from the last repository specified\&. 43 | .sp 44 | Mandatory arguments to long options are mandatory for short options too\&. Boolean arguments can only be given to long options\&. 45 | .PP 46 | \fB\-f, \-\-file\-types\fR=EXTENSIONS 47 | .RS 4 48 | A comma separated list of file extensions to include when computing statistics\&. The default extensions used are: java,c,cc,cpp,h,hh,hpp,py,glsl,rb,js,sql\&. Specifying a single 49 | \fI*\fR 50 | asterisk character includes files with no extension\&. Specifying two consecutive 51 | \fI**\fR 52 | asterisk characters includes all files regardless of extension\&. 53 | .RE 54 | .PP 55 | \fB\-F, \-\-format\fR=FORMAT 56 | .RS 4 57 | Defines in which format output should be generated; the default format is 58 | \fItext\fR 59 | and the available formats are: html,htmlembedded,json,text,xml (see 60 | \fBOUTPUT FORMATS\fR) 61 | .RE 62 | .PP 63 | \fB\-\-grading\fR[=BOOL] 64 | .RS 4 65 | Show statistics and information in a way that is formatted for grading of student projects; this is the same as supplying the options 66 | \fB\-HlmrTw\fR 67 | .RE 68 | .PP 69 | \fB\-H, \-\-hard\fR[=BOOL] 70 | .RS 4 71 | Track rows and look for duplicates harder; this can be quite slow with big repositories 72 | .RE 73 | .PP 74 | \fB\-l, \-\-list\-file\-types\fR[=BOOL] 75 | .RS 4 76 | List all the file extensions available in the current branch of the repository 77 | .RE 78 | .PP 79 | \fB\-L, \-\-localize\-output\fR[=BOOL] 80 | .RS 4 81 | Localize the generated output to the selected system language if a translation is available 82 | .RE 83 | .PP 84 | \fB\-m, \-\-metrics\fR[=BOOL] 85 | .RS 4 86 | Include checks for certain metrics during the analysis of commits 87 | .RE 88 | .PP 89 | \fB\-r \-\-responsibilities\fR[=BOOL] 90 | .RS 4 91 | Show which files the different authors seem most responsible for 92 | .RE 93 | .PP 94 | \fB\-\-since\fR=DATE 95 | .RS 4 96 | Only show statistics for commits more recent than a specific date 97 | .RE 98 | .PP 99 | \fB\-T, \-\-timeline\fR[=BOOL] 100 | .RS 4 101 | Show commit timeline, including author names 102 | .RE 103 | .PP 104 | \fB\-\-until\fR=DATE 105 | .RS 4 106 | Only show statistics for commits older than a specific date 107 | .RE 108 | .PP 109 | \fB\-w, \-\-weeks\fR[=BOOL] 110 | .RS 4 111 | Show all statistical information in weeks instead of in months 112 | .RE 113 | .PP 114 | \fB\-x, \-\-exclude\fR=PATTERN 115 | .RS 4 116 | An exclusion pattern describing the file paths, revisions, author names or author emails that should be excluded from the statistics; can be specified multiple times (see 117 | \fBFILTERING\fR) 118 | .RE 119 | .PP 120 | \fB\-h, \-\-help\fR 121 | .RS 4 122 | Display help and exit 123 | .RE 124 | .PP 125 | \fB\-\-version\fR 126 | .RS 4 127 | Output version information and exit 128 | .RE 129 | .SH "OUTPUT FORMATS" 130 | .sp 131 | There is support for multiple output formats in gitinspector\&. They can be selected using the \fB\-F\fR/\fB\-\-format\fR flags when running the main gitinspector script\&. 132 | .PP 133 | \fBtext (plain text)\fR 134 | .RS 4 135 | Plain text with some very simple ANSI formatting, suitable for console output\&. This is the format chosen by default in gitinspector\&. 136 | .RE 137 | .PP 138 | \fBhtml\fR 139 | .RS 4 140 | HTML with external links\&. The generated HTML page links to some external resources; such as the JavaScript library JQuery\&. It requires an active internet connection to properly function\&. This output format will most likely also link to additional external resources in the future\&. 141 | .RE 142 | .PP 143 | \fBhtmlembedded\fR 144 | .RS 4 145 | HTML with no external links\&. Similar to the HTML output format, but requires no active internet connection\&. As a consequence; the generated pages are bigger (as certain scripts have to be embedded into the generated output)\&. 146 | .RE 147 | .PP 148 | \fBjson\fR 149 | .RS 4 150 | JSON suitable for machine consumption\&. If you want to parse the output generated by gitinspector in a script or application of your own; this format is suitable\&. 151 | .RE 152 | .PP 153 | \fBxml\fR 154 | .RS 4 155 | XML suitable for machine consumption\&. If you want to parse the output generated by gitinspector in a script or application of your own; this format is suitable\&. 156 | .RE 157 | .SH "FILTERING" 158 | .sp 159 | gitinspector offers several different ways of filtering out unwanted information from the generated statistics: 160 | .sp 161 | .RS 4 162 | .ie n \{\ 163 | \h'-04'\(bu\h'+03'\c 164 | .\} 165 | .el \{\ 166 | .sp -1 167 | .IP \(bu 2.3 168 | .\} 169 | \fBgitinspector \-x myfile\fR, filter out and exclude statistics from all files (or paths) with the string "myfile" 170 | .RE 171 | .sp 172 | .RS 4 173 | .ie n \{\ 174 | \h'-04'\(bu\h'+03'\c 175 | .\} 176 | .el \{\ 177 | .sp -1 178 | .IP \(bu 2.3 179 | .\} 180 | \fBgitinspector \-x file:myfile\fR, filter out and exclude statistics from all files (or paths) with the string "myfile" 181 | .RE 182 | .sp 183 | .RS 4 184 | .ie n \{\ 185 | \h'-04'\(bu\h'+03'\c 186 | .\} 187 | .el \{\ 188 | .sp -1 189 | .IP \(bu 2.3 190 | .\} 191 | \fBgitinspector \-x author:John\fR, filter out and exclude statistics from all authors containing the string "John" 192 | .RE 193 | .sp 194 | .RS 4 195 | .ie n \{\ 196 | \h'-04'\(bu\h'+03'\c 197 | .\} 198 | .el \{\ 199 | .sp -1 200 | .IP \(bu 2.3 201 | .\} 202 | \fBgitinspector \-x email:@gmail\&.com\fR, filter out and exclude statistics from all authors with a gmail account 203 | .RE 204 | .sp 205 | .RS 4 206 | .ie n \{\ 207 | \h'-04'\(bu\h'+03'\c 208 | .\} 209 | .el \{\ 210 | .sp -1 211 | .IP \(bu 2.3 212 | .\} 213 | \fBgitinspector \-x revision:8755fb33\fR, filter out and exclude statistics from all revisions containing the hash "8755fb33" 214 | .RE 215 | .sp 216 | .RS 4 217 | .ie n \{\ 218 | \h'-04'\(bu\h'+03'\c 219 | .\} 220 | .el \{\ 221 | .sp -1 222 | .IP \(bu 2.3 223 | .\} 224 | \fBgitinspector \-x message:BUGFIX\fR, filter out and exclude statistics from all revisions containing "BUGFIX" in the commit message\&. 225 | .RE 226 | .sp 227 | The gitinspector command also lets you add multiple filtering rules by simply specifying the \-x options several times or by separating each filtering rule with a comma; 228 | .sp 229 | .RS 4 230 | .ie n \{\ 231 | \h'-04'\(bu\h'+03'\c 232 | .\} 233 | .el \{\ 234 | .sp -1 235 | .IP \(bu 2.3 236 | .\} 237 | \fBgitinspector \-x author:John \-x email:@gmail\&.com\fR 238 | .RE 239 | .sp 240 | .RS 4 241 | .ie n \{\ 242 | \h'-04'\(bu\h'+03'\c 243 | .\} 244 | .el \{\ 245 | .sp -1 246 | .IP \(bu 2.3 247 | .\} 248 | \fBgitinspector \-x author:John,email:@gmail\&.com\fR 249 | .RE 250 | .sp 251 | Sometimes, sub\-string matching (as described above) is simply not enough\&. Therefore, gitinspector let\(cqs you specify regular expressions as filtering rules\&. This makes filtering much more flexible: 252 | .sp 253 | .RS 4 254 | .ie n \{\ 255 | \h'-04'\(bu\h'+03'\c 256 | .\} 257 | .el \{\ 258 | .sp -1 259 | .IP \(bu 2.3 260 | .\} 261 | \fBgitinspector \-x "author:\e^(?!(John Smith))"\fR, only show statistics from author "John Smith" 262 | .RE 263 | .sp 264 | .RS 4 265 | .ie n \{\ 266 | \h'-04'\(bu\h'+03'\c 267 | .\} 268 | .el \{\ 269 | .sp -1 270 | .IP \(bu 2.3 271 | .\} 272 | \fBgitinspector \-x "author:\e^(?!([A\-C]))"\fR, only show statistics from authors starting with the letters A/B/C 273 | .RE 274 | .sp 275 | .RS 4 276 | .ie n \{\ 277 | \h'-04'\(bu\h'+03'\c 278 | .\} 279 | .el \{\ 280 | .sp -1 281 | .IP \(bu 2.3 282 | .\} 283 | \fBgitinspector \-x "email:\&.com$"\fR, filter out statistics from all email addresses ending with "\&.com" 284 | .RE 285 | .SH "USING GIT TO CONFIGURE GITINSPECTOR" 286 | .sp 287 | Options in gitinspector can be set using \fBgit config\fR\&. Consequently, it is possible to configure gitinspector behavior globally (in all git repositories) or locally (in a specific git repository)\&. It also means that settings will be permanently stored\&. All the long options that can be given to gitinspector can also be configured via git config (and take the same arguments)\&. 288 | .sp 289 | To configure how gitinspector should behave in all git repositories, execute the following git command: 290 | .sp 291 | \fBgit config \-\-global inspector\&.option setting\fR 292 | .sp 293 | To configure how gitinspector should behave in a specific git repository, execute the following git command (with the current directory standing inside the repository in question): 294 | .sp 295 | \fBgit config inspector\&.option setting\fR 296 | .SH "AUTHOR" 297 | .sp 298 | Originally written by Adam Waldenberg\&. 299 | .SH "REPORTING BUGS" 300 | .sp 301 | Report gitinspector bugs to gitinspector@ejwa\&.se 302 | .sp 303 | The gitinspector project page: https://github\&.com/ejwa/gitinspector 304 | .sp 305 | If you encounter problems, be sure to read the FAQ first: https://github\&.com/ejwa/gitinspector/wiki/FAQ 306 | .sp 307 | There is also an issue tracker at: https://github\&.com/ejwa/gitinspector/issues 308 | .SH "COPYRIGHT" 309 | .sp 310 | Copyright \(co 2012\-2015 Ejwa Software\&. All rights reserved\&. License GPLv3+: GNU GPL version 3 or later http://gnu\&.org/licenses/gpl\&.html\&. This is free software: you are free to change and redistribute it\&. There is NO WARRANTY, to the extent permitted by law\&. 311 | .SH "SEE ALSO" 312 | .sp 313 | \fBgit\fR(1) 314 | -------------------------------------------------------------------------------- /docs/gitinspector.html: -------------------------------------------------------------------------------- 1 | 2 | gitinspector

Name

gitinspector — statistical analysis tool for git repositories

Synopsis

gitinspector [OPTION]… [REPOSITORY]

DESCRIPTION

Analyze and gather statistics about a git repository. The defaut analysis shows general statistics per author, which can be complemented with a timeline analysis that shows the workload and activity of each author. Under normal operation, gitinspector filters the results to only show statistics about a number of given extensions and by default only includes source files in the statistical analysis.

Several output formats are supported, including plain text, HTML, JSON and XML.

OPTIONS

List information about the repository in REPOSITORY. If no repository is specified, the current directory is used. If multiple repositories are given, information will be fetched from the last repository specified.

Mandatory arguments to long options are mandatory for short options too. Boolean arguments can only be given to long options.

3 | -f, --file-types=EXTENSIONS 4 |
5 | A comma separated list of file extensions to include when computing statistics. The default extensions used are: java,c,cc,cpp,h,hh,hpp,py,glsl,rb,js,sql. Specifying a single * asterisk character includes files with no extension. Specifying two consecutive ** asterisk characters includes all files regardless of extension. 6 |
7 | -F, --format=FORMAT 8 |
9 | Defines in which format output should be generated; the default format is text and the available formats are: html,htmlembedded,json,text,xml (see OUTPUT FORMATS) 10 |
11 | --grading[=BOOL] 12 |
13 | Show statistics and information in a way that is formatted for grading of student projects; this is the same as supplying the options -HlmrTw 14 |
15 | -H, --hard[=BOOL] 16 |
17 | Track rows and look for duplicates harder; this can be quite slow with big repositories 18 |
19 | -l, --list-file-types[=BOOL] 20 |
21 | List all the file extensions available in the current branch of the repository 22 |
23 | -L, --localize-output[=BOOL] 24 |
25 | Localize the generated output to the selected system language if a translation is available 26 |
27 | -m, --metrics[=BOOL] 28 |
29 | Include checks for certain metrics during the analysis of commits 30 |
31 | -r --responsibilities[=BOOL] 32 |
33 | Show which files the different authors seem most responsible for 34 |
35 | --since=DATE 36 |
37 | Only show statistics for commits more recent than a specific date 38 |
39 | -T, --timeline[=BOOL] 40 |
41 | Show commit timeline, including author names 42 |
43 | --until=DATE 44 |
45 | Only show statistics for commits older than a specific date 46 |
47 | -w, --weeks[=BOOL] 48 |
49 | Show all statistical information in weeks instead of in months 50 |
51 | -x, --exclude=PATTERN 52 |
53 | An exclusion pattern describing the file paths, revisions, author names or author emails that should be excluded from the statistics; can be specified multiple times (see FILTERING) 54 |
55 | -h, --help 56 |
57 | Display help and exit 58 |
59 | --version 60 |
61 | Output version information and exit 62 |

OUTPUT FORMATS

There is support for multiple output formats in gitinspector. They can be selected using the -F/--format flags when running the main gitinspector script.

63 | text (plain text) 64 |
65 | Plain text with some very simple ANSI formatting, suitable for console output. This is the format chosen by default in gitinspector. 66 |
67 | html 68 |
69 | HTML with external links. The generated HTML page links to some external resources; such as the JavaScript library JQuery. It requires an active internet connection to properly function. This output format will most likely also link to additional external resources in the future. 70 |
71 | htmlembedded 72 |
73 | HTML with no external links. Similar to the HTML output format, but requires no active internet connection. As a consequence; the generated pages are bigger (as certain scripts have to be embedded into the generated output). 74 |
75 | json 76 |
77 | JSON suitable for machine consumption. If you want to parse the output generated by gitinspector in a script or application of your own; this format is suitable. 78 |
79 | xml 80 |
81 | XML suitable for machine consumption. If you want to parse the output generated by gitinspector in a script or application of your own; this format is suitable. 82 |

FILTERING

gitinspector offers several different ways of filtering out unwanted information from the generated statistics:

  • 83 | gitinspector -x myfile, filter out and exclude statistics from all files (or paths) with the string "myfile" 84 |
  • 85 | gitinspector -x file:myfile, filter out and exclude statistics from all files (or paths) with the string "myfile" 86 |
  • 87 | gitinspector -x author:John, filter out and exclude statistics from all authors containing the string "John" 88 |
  • 89 | gitinspector -x email:@gmail.com, filter out and exclude statistics from all authors with a gmail account 90 |
  • 91 | gitinspector -x revision:8755fb33, filter out and exclude statistics from all revisions containing the hash "8755fb33" 92 |
  • 93 | gitinspector -x message:BUGFIX, filter out and exclude statistics from all revisions containing "BUGFIX" in the commit message. 94 |

The gitinspector command also lets you add multiple filtering rules by simply specifying the -x options several times or by separating each filtering rule with a comma;

  • 95 | gitinspector -x author:John -x email:@gmail.com 96 |
  • 97 | gitinspector -x author:John,email:@gmail.com 98 |

Sometimes, sub-string matching (as described above) is simply not enough. Therefore, gitinspector let’s you specify regular expressions as filtering rules. This makes filtering much more flexible:

  • 99 | gitinspector -x "author:\^(?!(John Smith))", only show statistics from author "John Smith" 100 |
  • 101 | gitinspector -x "author:\^(?!([A-C]))", only show statistics from authors starting with the letters A/B/C 102 |
  • 103 | gitinspector -x "email:.com$", filter out statistics from all email addresses ending with ".com" 104 |

USING GIT TO CONFIGURE GITINSPECTOR

Options in gitinspector can be set using git config. Consequently, it is possible to configure gitinspector behavior globally (in all git repositories) or locally (in a specific git repository). It also means that settings will be permanently stored. All the long options that can be given to gitinspector can also be configured via git config (and take the same arguments).

To configure how gitinspector should behave in all git repositories, execute the following git command:

git config --global inspector.option setting

To configure how gitinspector should behave in a specific git repository, execute the following git command (with the current directory standing inside the repository in question):

git config inspector.option setting

AUTHOR

Originally written by Adam Waldenberg.

REPORTING BUGS

Report gitinspector bugs to gitinspector@ejwa.se

The gitinspector project page: https://github.com/ejwa/gitinspector

If you encounter problems, be sure to read the FAQ first: https://github.com/ejwa/gitinspector/wiki/FAQ

There is also an issue tracker at: https://github.com/ejwa/gitinspector/issues

COPYRIGHT

Copyright © 2012-2015 Ejwa Software. All rights reserved. License GPLv3+: GNU GPL version 3 or later http://gnu.org/licenses/gpl.html. 105 | This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law.

SEE ALSO

git(1)

-------------------------------------------------------------------------------- /gitinspector/html/html.header: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {title} 6 | 8 | 9 | 10 | 11 | 207 | 362 | 363 | 364 |
368 | --------------------------------------------------------------------------------