Massive Linting and clean up

This commit is contained in:
JP White 2021-02-27 22:39:08 -05:00
parent 2dd72c1d96
commit c17c5795ad
31 changed files with 2233 additions and 1822 deletions

View File

@ -26,14 +26,11 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: | run: |
python -m pip install --upgrade pip python -m pip install --upgrade pip
make requirements
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
make test-coverage
- name: Lint with flake8 - name: Lint with flake8
run: | run: |
# stop the build if there are Python syntax errors or undefined names make lint
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest - name: Test with pytest
run: | run: |
pytest make test-coverage

View File

@ -35,9 +35,13 @@ clean-test: ## remove test and coverage artifacts
rm -fr .pytest_cache rm -fr .pytest_cache
lint: ## check style with flake8 lint: ## check style with flake8
black gitinspector --line-length 120 # stop the build if there are Python syntax errors or undefined names
find . -name '*.py' -exec autopep8 -i {} --max-line-length=120 \; flake8 gitinspector tests --count --select=E9,F63,F7,F82 --show-source --statistics --builtins="_"
flake8 gitinspector tests --max-line-length=120 # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 gitinspector tests --count --ignore=E722,W503,E401,C901 --exit-zero --max-complexity=10 --max-line-length=127 --statistics --builtins="_"
format: ## auto format all the code with black
black gitinspector --line-length 127
test: ## run tests quickly with the default Python test: ## run tests quickly with the default Python
pytest pytest

View File

@ -21,43 +21,45 @@ import os
import subprocess import subprocess
import sys import sys
def get_basedir(): def get_basedir():
if hasattr(sys, "frozen"): # exists when running via py2exe if hasattr(sys, "frozen"): # exists when running via py2exe
return sys.prefix return sys.prefix
else: else:
return os.path.dirname(os.path.realpath(__file__)) return os.path.dirname(os.path.realpath(__file__))
def get_basedir_git(path=None): def get_basedir_git(path=None):
previous_directory = None previous_directory = None
if path != None: if path is not None:
previous_directory = os.getcwd() previous_directory = os.getcwd()
os.chdir(path) os.chdir(path)
bare_command = subprocess.Popen(["git", "rev-parse", "--is-bare-repository"], bare_command = subprocess.Popen(
stdout=subprocess.PIPE, stderr=open(os.devnull, "w")) ["git", "rev-parse", "--is-bare-repository"], stdout=subprocess.PIPE, stderr=open(os.devnull, "w")
)
isbare = bare_command.stdout.readlines() isbare = bare_command.stdout.readlines()
bare_command.wait() bare_command.wait()
if bare_command.returncode != 0: if bare_command.returncode != 0:
sys.exit(_("Error processing git repository at \"%s\"." % os.getcwd())) sys.exit(_('Error processing git repository at "%s".' % os.getcwd()))
isbare = (isbare[0].decode("utf-8", "replace").strip() == "true") isbare = isbare[0].decode("utf-8", "replace").strip() == "true"
absolute_path = None absolute_path = None
if isbare: if isbare:
absolute_path = subprocess.Popen(["git", "rev-parse", "--git-dir"], stdout=subprocess.PIPE).stdout absolute_path = subprocess.Popen(["git", "rev-parse", "--git-dir"], stdout=subprocess.PIPE).stdout
else: else:
absolute_path = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], absolute_path = subprocess.Popen(["git", "rev-parse", "--show-toplevel"], stdout=subprocess.PIPE).stdout
stdout=subprocess.PIPE).stdout
absolute_path = absolute_path.readlines() absolute_path = absolute_path.readlines()
if len(absolute_path) == 0: if len(absolute_path) == 0:
sys.exit(_("Unable to determine absolute path of git repository.")) sys.exit(_("Unable to determine absolute path of git repository."))
if path != None: if path is not None:
os.chdir(previous_directory) os.chdir(previous_directory)
return absolute_path[0].decode("utf-8", "replace").strip() return absolute_path[0].decode("utf-8", "replace").strip()

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import datetime import datetime
import multiprocessing import multiprocessing
import re import re
@ -30,16 +29,19 @@ from . import comment, extensions, filtering, format, interval, terminal
NUM_THREADS = multiprocessing.cpu_count() NUM_THREADS = multiprocessing.cpu_count()
class BlameEntry(object): class BlameEntry(object):
rows = 0 rows = 0
skew = 0 # Used when calculating average code age. skew = 0 # Used when calculating average code age.
comments = 0 comments = 0
__thread_lock__ = threading.BoundedSemaphore(NUM_THREADS) __thread_lock__ = threading.BoundedSemaphore(NUM_THREADS)
__blame_lock__ = threading.Lock() __blame_lock__ = threading.Lock()
AVG_DAYS_PER_MONTH = 30.4167 AVG_DAYS_PER_MONTH = 30.4167
class BlameThread(threading.Thread): class BlameThread(threading.Thread):
def __init__(self, useweeks, changes, blame_command, extension, blames, filename): def __init__(self, useweeks, changes, blame_command, extension, blames, filename):
__thread_lock__.acquire() # Lock controlling the number of threads running __thread_lock__.acquire() # Lock controlling the number of threads running
@ -72,21 +74,24 @@ class BlameThread(threading.Thread):
except KeyError: except KeyError:
return return
if not filtering.set_filtered(author, "author") and not \ if (
filtering.set_filtered(self.blamechunk_email, "email") and not \ not filtering.set_filtered(author, "author")
filtering.set_filtered(self.blamechunk_revision, "revision"): and not filtering.set_filtered(self.blamechunk_email, "email")
and not filtering.set_filtered(self.blamechunk_revision, "revision")
):
__blame_lock__.acquire() # Global lock used to protect calls from here... __blame_lock__.acquire() # Global lock used to protect calls from here...
if self.blames.get((author, self.filename), None) == None: if self.blames.get((author, self.filename), None) is None:
self.blames[(author, self.filename)] = BlameEntry() self.blames[(author, self.filename)] = BlameEntry()
self.blames[(author, self.filename)].comments += comments self.blames[(author, self.filename)].comments += comments
self.blames[(author, self.filename)].rows += 1 self.blames[(author, self.filename)].rows += 1
if (self.blamechunk_time - self.changes.first_commit_date).days > 0: if (self.blamechunk_time - self.changes.first_commit_date).days > 0:
self.blames[(author, self.filename)].skew += ((self.changes.last_commit_date - self.blamechunk_time).days / self.blames[(author, self.filename)].skew += (self.changes.last_commit_date - self.blamechunk_time).days / (
(7.0 if self.useweeks else AVG_DAYS_PER_MONTH)) 7.0 if self.useweeks else AVG_DAYS_PER_MONTH
)
__blame_lock__.release() # ...to here. __blame_lock__.release() # ...to here.
@ -97,7 +102,7 @@ class BlameThread(threading.Thread):
self.__clear_blamechunk_info__() self.__clear_blamechunk_info__()
#pylint: disable=W0201 # pylint: disable=W0201
for j in range(0, len(rows)): for j in range(0, len(rows)):
row = rows[j].decode("utf-8", "replace").strip() row = rows[j].decode("utf-8", "replace").strip()
keyval = row.split(" ", 2) keyval = row.split(" ", 2)
@ -118,34 +123,45 @@ class BlameThread(threading.Thread):
__thread_lock__.release() # Lock controlling the number of threads running __thread_lock__.release() # Lock controlling the number of threads running
PROGRESS_TEXT = N_("Checking how many rows belong to each author (2 of 2): {0:.0f}%") PROGRESS_TEXT = N_("Checking how many rows belong to each author (2 of 2): {0:.0f}%")
class Blame(object): class Blame(object):
def __init__(self, repo, hard, useweeks, changes): def __init__(self, repo, hard, useweeks, changes):
self.blames = {} self.blames = {}
ls_tree_p = subprocess.Popen(["git", "ls-tree", "--name-only", "-r", interval.get_ref()], ls_tree_p = subprocess.Popen(
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) ["git", "ls-tree", "--name-only", "-r", interval.get_ref()], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
lines = ls_tree_p.communicate()[0].splitlines() lines = ls_tree_p.communicate()[0].splitlines()
ls_tree_p.stdout.close() ls_tree_p.stdout.close()
if ls_tree_p.returncode == 0: if ls_tree_p.returncode == 0:
progress_text = _(PROGRESS_TEXT) progress_text = _(PROGRESS_TEXT)
if repo != None: if repo is not None:
progress_text = "[%s] " % repo.name + progress_text progress_text = "[%s] " % repo.name + progress_text
for i, row in enumerate(lines): for i, row in enumerate(lines):
row = row.strip().decode("unicode_escape", "ignore") row = row.strip().decode("unicode_escape", "ignore")
row = row.encode("latin-1", "replace") row = row.encode("latin-1", "replace")
row = row.decode("utf-8", "replace").strip("\"").strip("'").strip() row = row.decode("utf-8", "replace").strip('"').strip("'").strip()
if FileDiff.get_extension(row) in extensions.get_located() and \ if (
FileDiff.is_valid_extension(row) and not filtering.set_filtered(FileDiff.get_filename(row)): FileDiff.get_extension(row) in extensions.get_located()
blame_command = [_f for _f in ["git", "blame", "--line-porcelain", "-w"] + \ and FileDiff.is_valid_extension(row)
(["-C", "-C", "-M"] if hard else []) + and not filtering.set_filtered(FileDiff.get_filename(row))
[interval.get_since(), interval.get_ref(), "--", row] if _f] ):
thread = BlameThread(useweeks, changes, blame_command, FileDiff.get_extension(row), blame_command = [
self.blames, row.strip()) _f
for _f in ["git", "blame", "--line-porcelain", "-w"]
+ (["-C", "-C", "-M"] if hard else [])
+ [interval.get_since(), interval.get_ref(), "--", row]
if _f
]
thread = BlameThread(
useweeks, changes, blame_command, FileDiff.get_extension(row), self.blames, row.strip()
)
thread.daemon = True thread.daemon = True
thread.start() thread.start()
@ -163,15 +179,15 @@ class Blame(object):
def __iadd__(self, other): def __iadd__(self, other):
try: try:
self.blames.update(other.blames) self.blames.update(other.blames)
return self; return self
except AttributeError: except AttributeError:
return other; return other
@staticmethod @staticmethod
def is_revision(string): def is_revision(string):
revision = re.search("([0-9a-f]{40})", string) revision = re.search("([0-9a-f]{40})", string)
if revision == None: if revision is None:
return False return False
return revision.group(1).strip() return revision.group(1).strip()
@ -191,7 +207,7 @@ class Blame(object):
def get_summed_blames(self): def get_summed_blames(self):
summed_blames = {} summed_blames = {}
for i in list(self.blames.items()): for i in list(self.blames.items()):
if summed_blames.get(i[0][0], None) == None: if summed_blames.get(i[0][0], None) is None:
summed_blames[i[0][0]] = BlameEntry() summed_blames[i[0][0]] = BlameEntry()
summed_blames[i[0][0]].rows += i[1].rows summed_blames[i[0][0]].rows += i[1].rows

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import bisect import bisect
import datetime import datetime
import multiprocessing import multiprocessing
@ -34,6 +33,7 @@ NUM_THREADS = multiprocessing.cpu_count()
__thread_lock__ = threading.BoundedSemaphore(NUM_THREADS) __thread_lock__ = threading.BoundedSemaphore(NUM_THREADS)
__changes_lock__ = threading.Lock() __changes_lock__ = threading.Lock()
class FileDiff(object): class FileDiff(object):
def __init__(self, string): def __init__(self, string):
commit_line = string.split("|") commit_line = string.split("|")
@ -46,26 +46,27 @@ class FileDiff(object):
@staticmethod @staticmethod
def is_filediff_line(string): def is_filediff_line(string):
string = string.split("|") string = string.split("|")
return string.__len__() == 2 and string[1].find("Bin") == -1 and ('+' in string[1] or '-' in string[1]) return string.__len__() == 2 and string[1].find("Bin") == -1 and ("+" in string[1] or "-" in string[1])
@staticmethod @staticmethod
def get_extension(string): def get_extension(string):
string = string.split("|")[0].strip().strip("{}").strip("\"").strip("'") string = string.split("|")[0].strip().strip("{}").strip('"').strip("'")
return os.path.splitext(string)[1][1:] return os.path.splitext(string)[1][1:]
@staticmethod @staticmethod
def get_filename(string): def get_filename(string):
return string.split("|")[0].strip().strip("{}").strip("\"").strip("'") return string.split("|")[0].strip().strip("{}").strip('"').strip("'")
@staticmethod @staticmethod
def is_valid_extension(string): def is_valid_extension(string):
extension = FileDiff.get_extension(string) extension = FileDiff.get_extension(string)
for i in extensions.get(): for i in extensions.get():
if (extension == "" and i == "*") or extension == i or i == '**': if (extension == "" and i == "*") or extension == i or i == "**":
return True return True
return False return False
class Commit(object): class Commit(object):
def __init__(self, string): def __init__(self, string):
self.filediffs = [] self.filediffs = []
@ -98,12 +99,14 @@ class Commit(object):
def is_commit_line(string): def is_commit_line(string):
return string.split("|").__len__() == 5 return string.split("|").__len__() == 5
class AuthorInfo(object): class AuthorInfo(object):
email = None email = None
insertions = 0 insertions = 0
deletions = 0 deletions = 0
commits = 0 commits = 0
class ChangesThread(threading.Thread): class ChangesThread(threading.Thread):
def __init__(self, hard, changes, first_hash, second_hash, offset): def __init__(self, hard, changes, first_hash, second_hash, offset):
__thread_lock__.acquire() # Lock controlling the number of threads running __thread_lock__.acquire() # Lock controlling the number of threads running
@ -122,10 +125,27 @@ class ChangesThread(threading.Thread):
thread.start() thread.start()
def run(self): def run(self):
git_log_r = subprocess.Popen([_f for _f in ["git", "log", "--reverse", "--pretty=%ct|%cd|%H|%aN|%aE", git_log_r = subprocess.Popen(
"--stat=100000,8192", "--no-merges", "-w", interval.get_since(), [
interval.get_until(), "--date=short"] + (["-C", "-C", "-M"] if self.hard else []) + _f
[self.first_hash + self.second_hash] if _f], stdout=subprocess.PIPE).stdout for _f in [
"git",
"log",
"--reverse",
"--pretty=%ct|%cd|%H|%aN|%aE",
"--stat=100000,8192",
"--no-merges",
"-w",
interval.get_since(),
interval.get_until(),
"--date=short",
]
+ (["-C", "-C", "-M"] if self.hard else [])
+ [self.first_hash + self.second_hash]
if _f
],
stdout=subprocess.PIPE,
).stdout
lines = git_log_r.readlines() lines = git_log_r.readlines()
git_log_r.close() git_log_r.close()
@ -154,15 +174,15 @@ class ChangesThread(threading.Thread):
is_filtered = False is_filtered = False
commit = Commit(j) commit = Commit(j)
if Commit.is_commit_line(j) and \ if Commit.is_commit_line(j) and (
(filtering.set_filtered(commit.author, "author") or \ filtering.set_filtered(commit.author, "author")
filtering.set_filtered(commit.email, "email") or \ or filtering.set_filtered(commit.email, "email")
filtering.set_filtered(commit.sha, "revision") or \ or filtering.set_filtered(commit.sha, "revision")
filtering.set_filtered(commit.sha, "message")): or filtering.set_filtered(commit.sha, "message")
):
is_filtered = True is_filtered = True
if FileDiff.is_filediff_line(j) and not \ if FileDiff.is_filediff_line(j) and not filtering.set_filtered(FileDiff.get_filename(j)) and not is_filtered:
filtering.set_filtered(FileDiff.get_filename(j)) and not is_filtered:
extensions.add_located(FileDiff.get_extension(j)) extensions.add_located(FileDiff.get_extension(j))
if FileDiff.is_valid_extension(j): if FileDiff.is_valid_extension(j):
@ -174,8 +194,10 @@ class ChangesThread(threading.Thread):
__changes_lock__.release() # ...to here. __changes_lock__.release() # ...to here.
__thread_lock__.release() # Lock controlling the number of threads running __thread_lock__.release() # Lock controlling the number of threads running
PROGRESS_TEXT = N_("Fetching and calculating primary statistics (1 of 2): {0:.0f}%") PROGRESS_TEXT = N_("Fetching and calculating primary statistics (1 of 2): {0:.0f}%")
class Changes(object): class Changes(object):
authors = {} authors = {}
authors_dateinfo = {} authors_dateinfo = {}
@ -184,16 +206,22 @@ class Changes(object):
def __init__(self, repo, hard): def __init__(self, repo, hard):
self.commits = [] self.commits = []
interval.set_ref("HEAD"); interval.set_ref("HEAD")
git_rev_list_p = subprocess.Popen([_f for _f in ["git", "rev-list", "--reverse", "--no-merges", git_rev_list_p = subprocess.Popen(
interval.get_since(), interval.get_until(), "HEAD"] if _f], [
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) _f
for _f in ["git", "rev-list", "--reverse", "--no-merges", interval.get_since(), interval.get_until(), "HEAD"]
if _f
],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
lines = git_rev_list_p.communicate()[0].splitlines() lines = git_rev_list_p.communicate()[0].splitlines()
git_rev_list_p.stdout.close() git_rev_list_p.stdout.close()
if git_rev_list_p.returncode == 0 and len(lines) > 0: if git_rev_list_p.returncode == 0 and len(lines) > 0:
progress_text = _(PROGRESS_TEXT) progress_text = _(PROGRESS_TEXT)
if repo != None: if repo is not None:
progress_text = "[%s] " % repo.name + progress_text progress_text = "[%s] " % repo.name + progress_text
chunks = len(lines) // CHANGES_PER_THREAD chunks = len(lines) // CHANGES_PER_THREAD
@ -229,10 +257,12 @@ class Changes(object):
if interval.has_interval(): if interval.has_interval():
interval.set_ref(self.commits[-1].sha) interval.set_ref(self.commits[-1].sha)
self.first_commit_date = datetime.date(int(self.commits[0].date[0:4]), int(self.commits[0].date[5:7]), self.first_commit_date = datetime.date(
int(self.commits[0].date[8:10])) int(self.commits[0].date[0:4]), int(self.commits[0].date[5:7]), int(self.commits[0].date[8:10])
self.last_commit_date = datetime.date(int(self.commits[-1].date[0:4]), int(self.commits[-1].date[5:7]), )
int(self.commits[-1].date[8:10])) self.last_commit_date = datetime.date(
int(self.commits[-1].date[0:4]), int(self.commits[-1].date[5:7]), int(self.commits[-1].date[8:10])
)
def __iadd__(self, other): def __iadd__(self, other):
try: try:
@ -255,7 +285,7 @@ class Changes(object):
@staticmethod @staticmethod
def modify_authorinfo(authors, key, commit): def modify_authorinfo(authors, key, commit):
if authors.get(key, None) == None: if authors.get(key, None) is None:
authors[key] = AuthorInfo() authors[key] = AuthorInfo()
if commit.get_filediffs(): if commit.get_filediffs():

View File

@ -31,6 +31,7 @@ except:
__cloned_paths__ = [] __cloned_paths__ = []
def create(url): def create(url):
class Repository(object): class Repository(object):
def __init__(self, name, location): def __init__(self, name, location):
@ -39,8 +40,13 @@ def create(url):
parsed_url = urlparse(url) parsed_url = urlparse(url)
if parsed_url.scheme == "file" or parsed_url.scheme == "git" or parsed_url.scheme == "http" or \ if (
parsed_url.scheme == "https" or parsed_url.scheme == "ssh": parsed_url.scheme == "file"
or parsed_url.scheme == "git"
or parsed_url.scheme == "http"
or parsed_url.scheme == "https"
or parsed_url.scheme == "ssh"
):
path = tempfile.mkdtemp(suffix=".gitinspector") path = tempfile.mkdtemp(suffix=".gitinspector")
git_clone = subprocess.Popen(["git", "clone", url, path], stdout=sys.stderr) git_clone = subprocess.Popen(["git", "clone", url, path], stdout=sys.stderr)
git_clone.wait() git_clone.wait()
@ -53,6 +59,7 @@ def create(url):
return Repository(None, os.path.abspath(url)) return Repository(None, os.path.abspath(url))
def delete(): def delete():
for path in __cloned_paths__: for path in __cloned_paths__:
shutil.rmtree(path, ignore_errors=True) shutil.rmtree(path, ignore_errors=True)

View File

@ -18,50 +18,128 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
__comment_begining__ = {
"java": "/*",
"c": "/*",
"cc": "/*",
"cpp": "/*",
"cs": "/*",
"h": "/*",
"hh": "/*",
"hpp": "/*",
"hs": "{-",
"html": "<!--",
"php": "/*",
"py": '"""',
"glsl": "/*",
"rb": "=begin",
"js": "/*",
"jspx": "<!--",
"scala": "/*",
"sql": "/*",
"tex": "\\begin{comment}",
"xhtml": "<!--",
"xml": "<!--",
"ml": "(*",
"mli": "(*",
"go": "/*",
"ly": "%{",
"ily": "%{",
}
__comment_begining__ = {"java": "/*", "c": "/*", "cc": "/*", "cpp": "/*", "cs": "/*", "h": "/*", "hh": "/*", "hpp": "/*", __comment_end__ = {
"hs": "{-", "html": "<!--", "php": "/*", "py": "\"\"\"", "glsl": "/*", "rb": "=begin", "js": "/*", "java": "*/",
"jspx": "<!--", "scala": "/*", "sql": "/*", "tex": "\\begin{comment}", "xhtml": "<!--", "c": "*/",
"xml": "<!--", "ml": "(*", "mli": "(*", "go": "/*", "ly": "%{", "ily": "%{"} "cc": "*/",
"cpp": "*/",
"cs": "*/",
"h": "*/",
"hh": "*/",
"hpp": "*/",
"hs": "-}",
"html": "-->",
"php": "*/",
"py": '"""',
"glsl": "*/",
"rb": "=end",
"js": "*/",
"jspx": "-->",
"scala": "*/",
"sql": "*/",
"tex": "\\end{comment}",
"xhtml": "-->",
"xml": "-->",
"ml": "*)",
"mli": "*)",
"go": "*/",
"ly": "%}",
"ily": "%}",
}
__comment_end__ = {"java": "*/", "c": "*/", "cc": "*/", "cpp": "*/", "cs": "*/", "h": "*/", "hh": "*/", "hpp": "*/", "hs": "-}", __comment__ = {
"html": "-->", "php": "*/", "py": "\"\"\"", "glsl": "*/", "rb": "=end", "js": "*/", "jspx": "-->", "java": "//",
"scala": "*/", "sql": "*/", "tex": "\\end{comment}", "xhtml": "-->", "xml": "-->", "ml": "*)", "mli": "*)", "c": "//",
"go": "*/", "ly": "%}", "ily": "%}"} "cc": "//",
"cpp": "//",
__comment__ = {"java": "//", "c": "//", "cc": "//", "cpp": "//", "cs": "//", "h": "//", "hh": "//", "hpp": "//", "hs": "--", "cs": "//",
"pl": "#", "php": "//", "py": "#", "glsl": "//", "rb": "#", "robot": "#", "rs": "//", "rlib": "//", "js": "//", "h": "//",
"scala": "//", "sql": "--", "tex": "%", "ada": "--", "ads": "--", "adb": "--", "pot": "#", "po": "#", "go": "//", "hh": "//",
"ly": "%", "ily": "%"} "hpp": "//",
"hs": "--",
"pl": "#",
"php": "//",
"py": "#",
"glsl": "//",
"rb": "#",
"robot": "#",
"rs": "//",
"rlib": "//",
"js": "//",
"scala": "//",
"sql": "--",
"tex": "%",
"ada": "--",
"ads": "--",
"adb": "--",
"pot": "#",
"po": "#",
"go": "//",
"ly": "%",
"ily": "%",
}
__comment_markers_must_be_at_begining__ = {"tex": True} __comment_markers_must_be_at_begining__ = {"tex": True}
def __has_comment_begining__(extension, string): def __has_comment_begining__(extension, string):
if __comment_markers_must_be_at_begining__.get(extension, None) == True: if __comment_markers_must_be_at_begining__.get(extension, None):
return string.find(__comment_begining__[extension]) == 0 return string.find(__comment_begining__[extension]) == 0
elif __comment_begining__.get(extension, None) != None and string.find(__comment_end__[extension], 2) == -1: elif __comment_begining__.get(extension, None) is not None and string.find(__comment_end__[extension], 2) == -1:
return string.find(__comment_begining__[extension]) != -1 return string.find(__comment_begining__[extension]) != -1
return False return False
def __has_comment_end__(extension, string): def __has_comment_end__(extension, string):
if __comment_markers_must_be_at_begining__.get(extension, None) == True: if __comment_markers_must_be_at_begining__.get(extension, None):
return string.find(__comment_end__[extension]) == 0 return string.find(__comment_end__[extension]) == 0
elif __comment_end__.get(extension, None) != None: elif __comment_end__.get(extension, None) is not None:
return string.find(__comment_end__[extension]) != -1 return string.find(__comment_end__[extension]) != -1
return False return False
def is_comment(extension, string): def is_comment(extension, string):
if __comment_begining__.get(extension, None) != None and string.strip().startswith(__comment_begining__[extension]): if __comment_begining__.get(extension, None) is not None and string.strip().startswith(__comment_begining__[extension]):
return True return True
if __comment_end__.get(extension, None) != None and string.strip().endswith(__comment_end__[extension]): if __comment_end__.get(extension, None) is not None and string.strip().endswith(__comment_end__[extension]):
return True return True
if __comment__.get(extension, None) != None and string.strip().startswith(__comment__[extension]): if __comment__.get(extension, None) is not None and string.strip().startswith(__comment__[extension]):
return True return True
return False return False
def handle_comment_block(is_inside_comment, extension, content): def handle_comment_block(is_inside_comment, extension, content):
comments = 0 comments = 0

View File

@ -22,6 +22,7 @@ import os
import subprocess import subprocess
from . import extensions, filtering, format, interval, optval from . import extensions, filtering, format, interval, optval
class GitConfig(object): class GitConfig(object):
def __init__(self, run, repo, global_only=False): def __init__(self, run, repo, global_only=False):
self.run = run self.run = run
@ -31,8 +32,10 @@ class GitConfig(object):
def __read_git_config__(self, variable): def __read_git_config__(self, variable):
previous_directory = os.getcwd() previous_directory = os.getcwd()
os.chdir(self.repo) os.chdir(self.repo)
setting = subprocess.Popen([_f for _f in ["git", "config", "--global" if self.global_only else "", setting = subprocess.Popen(
"inspector." + variable] if _f], stdout=subprocess.PIPE).stdout [_f for _f in ["git", "config", "--global" if self.global_only else "", "inspector." + variable] if _f],
stdout=subprocess.PIPE,
).stdout
os.chdir(previous_directory) os.chdir(previous_directory)
try: try:

View File

@ -18,24 +18,27 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
DEFAULT_EXTENSIONS = ["java", "c", "cc", "cpp", "h", "hh", "hpp", "py", "glsl", "rb", "js", "sql"] DEFAULT_EXTENSIONS = ["java", "c", "cc", "cpp", "h", "hh", "hpp", "py", "glsl", "rb", "js", "sql"]
__extensions__ = DEFAULT_EXTENSIONS __extensions__ = DEFAULT_EXTENSIONS
__located_extensions__ = set() __located_extensions__ = set()
def get(): def get():
return __extensions__ return __extensions__
def define(string): def define(string):
global __extensions__ global __extensions__
__extensions__ = string.split(",") __extensions__ = string.split(",")
def add_located(string): def add_located(string):
if len(string) == 0: if len(string) == 0:
__located_extensions__.add("*") __located_extensions__.add("*")
else: else:
__located_extensions__.add(string) __located_extensions__.add(string)
def get_located(): def get_located():
return __located_extensions__ return __located_extensions__

View File

@ -21,17 +21,25 @@
import re import re
import subprocess import subprocess
__filters__ = {"file": [set(), set()], "author": [set(), set()], "email": [set(), set()], "revision": [set(), set()], __filters__ = {
"message" : [set(), None]} "file": [set(), set()],
"author": [set(), set()],
"email": [set(), set()],
"revision": [set(), set()],
"message": [set(), None],
}
class InvalidRegExpError(ValueError): class InvalidRegExpError(ValueError):
def __init__(self, msg): def __init__(self, msg):
super(InvalidRegExpError, self).__init__(msg) super(InvalidRegExpError, self).__init__(msg)
self.msg = msg self.msg = msg
def get(): def get():
return __filters__ return __filters__
def __add_one__(string): def __add_one__(string):
for i in __filters__: for i in __filters__:
if (i + ":").lower() == string[0:len(i) + 1].lower(): if (i + ":").lower() == string[0:len(i) + 1].lower():
@ -39,27 +47,33 @@ def __add_one__(string):
return return
__filters__["file"][0].add(string) __filters__["file"][0].add(string)
def add(string): def add(string):
rules = string.split(",") rules = string.split(",")
for rule in rules: for rule in rules:
__add_one__(rule) __add_one__(rule)
def clear(): def clear():
for i in __filters__: for i in __filters__:
__filters__[i][0] = set() __filters__[i][0] = set()
def get_filered(filter_type="file"): def get_filered(filter_type="file"):
return __filters__[filter_type][1] return __filters__[filter_type][1]
def has_filtered(): def has_filtered():
for i in __filters__: for i in __filters__:
if __filters__[i][1]: if __filters__[i][1]:
return True return True
return False return False
def __find_commit_message__(sha): def __find_commit_message__(sha):
git_show_r = subprocess.Popen([_f for _f in ["git", "show", "-s", "--pretty=%B", "-w", sha] if _f], git_show_r = subprocess.Popen(
stdout=subprocess.PIPE).stdout [_f for _f in ["git", "show", "-s", "--pretty=%B", "-w", sha] if _f], stdout=subprocess.PIPE
).stdout
commit_message = git_show_r.read() commit_message = git_show_r.read()
git_show_r.close() git_show_r.close()
@ -68,6 +82,7 @@ def __find_commit_message__(sha):
commit_message = commit_message.encode("latin-1", "replace") commit_message = commit_message.encode("latin-1", "replace")
return commit_message.decode("utf-8", "replace") return commit_message.decode("utf-8", "replace")
def set_filtered(string, filter_type="file"): def set_filtered(string, filter_type="file"):
string = string.strip() string = string.strip()
@ -78,7 +93,7 @@ def set_filtered(string, filter_type="file"):
if filter_type == "message": if filter_type == "message":
search_for = __find_commit_message__(string) search_for = __find_commit_message__(string)
try: try:
if re.search(i, search_for) != None: if re.search(i, search_for) is not None:
if filter_type == "message": if filter_type == "message":
__add_one__("revision:" + string) __add_one__("revision:" + string)
else: else:

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import base64 import base64
import os import os
import textwrap import textwrap
@ -33,23 +32,28 @@ DEFAULT_FORMAT = __available_formats__[3]
__selected_format__ = DEFAULT_FORMAT __selected_format__ = DEFAULT_FORMAT
class InvalidFormatError(Exception): class InvalidFormatError(Exception):
def __init__(self, msg): def __init__(self, msg):
super(InvalidFormatError, self).__init__(msg) super(InvalidFormatError, self).__init__(msg)
self.msg = msg self.msg = msg
def select(format): def select(format):
global __selected_format__ global __selected_format__
__selected_format__ = format __selected_format__ = format
return format in __available_formats__ return format in __available_formats__
def get_selected(): def get_selected():
return __selected_format__ return __selected_format__
def is_interactive_format(): def is_interactive_format():
return __selected_format__ == "text" return __selected_format__ == "text"
def __output_html_template__(name): def __output_html_template__(name):
template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), name) template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), name)
file_r = open(template_path, "rb") file_r = open(template_path, "rb")
@ -58,6 +62,7 @@ def __output_html_template__(name):
file_r.close() file_r.close()
return template return template
def __get_zip_file_content__(name, file_name="/html/flot.zip"): def __get_zip_file_content__(name, file_name="/html/flot.zip"):
zip_file = zipfile.ZipFile(basedir.get_basedir() + file_name, "r") zip_file = zipfile.ZipFile(basedir.get_basedir() + file_name, "r")
content = zip_file.read(name) content = zip_file.read(name)
@ -65,17 +70,20 @@ def __get_zip_file_content__(name, file_name="/html/flot.zip"):
zip_file.close() zip_file.close()
return content.decode("utf-8", "replace") return content.decode("utf-8", "replace")
INFO_ONE_REPOSITORY = N_("Statistical information for the repository '{0}' was gathered on {1}.") INFO_ONE_REPOSITORY = N_("Statistical information for the repository '{0}' was gathered on {1}.")
INFO_MANY_REPOSITORIES = N_("Statistical information for the repositories '{0}' was gathered on {1}.") INFO_MANY_REPOSITORIES = N_("Statistical information for the repositories '{0}' was gathered on {1}.")
def output_header(repos): def output_header(repos):
repos_string = ", ".join([repo.name for repo in repos]) repos_string = ", ".join([repo.name for repo in repos])
if __selected_format__ == "html" or __selected_format__ == "htmlembedded": if __selected_format__ == "html" or __selected_format__ == "htmlembedded":
base = basedir.get_basedir() base = basedir.get_basedir()
html_header = __output_html_template__(base + "/html/html.header") html_header = __output_html_template__(base + "/html/html.header")
tablesorter_js = __get_zip_file_content__("jquery.tablesorter.min.js", tablesorter_js = __get_zip_file_content__("jquery.tablesorter.min.js", "/html/jquery.tablesorter.min.js.zip").encode(
"/html/jquery.tablesorter.min.js.zip").encode("latin-1", "replace") "latin-1", "replace"
)
tablesorter_js = tablesorter_js.decode("utf-8", "ignore") tablesorter_js = tablesorter_js.decode("utf-8", "ignore")
flot_js = __get_zip_file_content__("jquery.flot.js") flot_js = __get_zip_file_content__("jquery.flot.js")
pie_js = __get_zip_file_content__("jquery.flot.pie.js") pie_js = __get_zip_file_content__("jquery.flot.pie.js")
@ -89,40 +97,44 @@ def output_header(repos):
if __selected_format__ == "htmlembedded": if __selected_format__ == "htmlembedded":
jquery_js = ">" + __get_zip_file_content__("jquery.js") jquery_js = ">" + __get_zip_file_content__("jquery.js")
else: else:
jquery_js = " src=\"https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js\">" jquery_js = ' src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js">'
print(html_header.format(title=_("Repository statistics for '{0}'").format(repos_string), print(
html_header.format(
title=_("Repository statistics for '{0}'").format(repos_string),
jquery=jquery_js, jquery=jquery_js,
jquery_tablesorter=tablesorter_js, jquery_tablesorter=tablesorter_js,
jquery_flot=flot_js, jquery_flot=flot_js,
jquery_flot_pie=pie_js, jquery_flot_pie=pie_js,
jquery_flot_resize=resize_js, jquery_flot_resize=resize_js,
logo=logo.decode("utf-8", "replace"), logo=logo.decode("utf-8", "replace"),
logo_text=_("The output has been generated by {0} {1}. The statistical analysis tool" logo_text=_(
" for git repositories.").format( "The output has been generated by {0} {1}. The statistical analysis tool" " for git repositories."
"<a href=\"https://github.com/ejwa/gitinspector\">gitinspector</a>", ).format('<a href="https://github.com/ejwa/gitinspector">gitinspector</a>', version.__version__),
version.__version__),
repo_text=_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format( repo_text=_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format(
repos_string, localization.get_date()), repos_string, localization.get_date()
),
show_minor_authors=_("Show minor authors"), show_minor_authors=_("Show minor authors"),
hide_minor_authors=_("Hide minor authors"), hide_minor_authors=_("Hide minor authors"),
show_minor_rows=_("Show rows with minor work"), show_minor_rows=_("Show rows with minor work"),
hide_minor_rows=_("Hide rows with minor work"))) hide_minor_rows=_("Hide rows with minor work"),
)
)
elif __selected_format__ == "json": elif __selected_format__ == "json":
print("{\n\t\"gitinspector\": {") print('{\n\t"gitinspector": {')
print("\t\t\"version\": \"" + version.__version__ + "\",") print('\t\t"version": "' + version.__version__ + '",')
if len(repos) <= 1: if len(repos) <= 1:
print("\t\t\"repository\": \"" + repos_string + "\",") print('\t\t"repository": "' + repos_string + '",')
else: else:
repos_json = "\t\t\"repositories\": [ " repos_json = '\t\t"repositories": [ '
for repo in repos: for repo in repos:
repos_json += "\"" + repo.name + "\", " repos_json += '"' + repo.name + '", '
print(repos_json[:-2] + " ],") print(repos_json[:-2] + " ],")
print("\t\t\"report_date\": \"" + time.strftime("%Y/%m/%d") + "\",") print('\t\t"report_date": "' + time.strftime("%Y/%m/%d") + '",')
elif __selected_format__ == "xml": elif __selected_format__ == "xml":
print("<gitinspector>") print("<gitinspector>")
@ -140,8 +152,15 @@ def output_header(repos):
print("\t<report-date>" + time.strftime("%Y/%m/%d") + "</report-date>") print("\t<report-date>" + time.strftime("%Y/%m/%d") + "</report-date>")
else: else:
print(textwrap.fill(_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format( print(
repos_string, localization.get_date()), width=terminal.get_size()[0])) textwrap.fill(
_(INFO_ONE_REPOSITORY if len(repos) <= 1 else INFO_MANY_REPOSITORIES).format(
repos_string, localization.get_date()
),
width=terminal.get_size()[0],
)
)
def output_footer(): def output_footer():
if __selected_format__ == "html" or __selected_format__ == "htmlembedded": if __selected_format__ == "html" or __selected_format__ == "htmlembedded":

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import atexit import atexit
import getopt import getopt
import os import os
@ -27,8 +26,7 @@ from .blame import Blame
from .changes import Changes from .changes import Changes
from .config import GitConfig from .config import GitConfig
from .metrics import MetricsLogic from .metrics import MetricsLogic
from . import (basedir, clone, extensions, filtering, format, help, interval, from . import basedir, clone, extensions, filtering, format, help, interval, localization, optval, terminal, version
localization, optval, terminal, version)
from .output import outputable from .output import outputable
from .output.blameoutput import BlameOutput from .output.blameoutput import BlameOutput
from .output.changesoutput import ChangesOutput from .output.changesoutput import ChangesOutput
@ -40,6 +38,7 @@ from .output.timelineoutput import TimelineOutput
localization.init() localization.init()
class Runner(object): class Runner(object):
def __init__(self): def __init__(self):
self.hard = False self.hard = False
@ -102,22 +101,24 @@ class Runner(object):
format.output_footer() format.output_footer()
os.chdir(previous_directory) os.chdir(previous_directory)
def __check_python_version__(): def __check_python_version__():
if sys.version_info < (2, 6): if sys.version_info < (2, 6):
python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1]) python_version = str(sys.version_info[0]) + "." + str(sys.version_info[1])
sys.exit(_("gitinspector requires at least Python 2.6 to run (version {0} was found).").format(python_version)) sys.exit(_("gitinspector requires at least Python 2.6 to run (version {0} was found).").format(python_version))
def __get_validated_git_repos__(repos_relative): def __get_validated_git_repos__(repos_relative):
if not repos_relative: if not repos_relative:
repos_relative = "." repos_relative = "."
repos = [] repos = []
#Try to clone the repos or return the same directory and bail out. # Try to clone the repos or return the same directory and bail out.
for repo in repos_relative: for repo in repos_relative:
cloned_repo = clone.create(repo) cloned_repo = clone.create(repo)
if cloned_repo.name == None: if cloned_repo.name is None:
cloned_repo.location = basedir.get_basedir_git(cloned_repo.location) cloned_repo.location = basedir.get_basedir_git(cloned_repo.location)
cloned_repo.name = os.path.basename(cloned_repo.location) cloned_repo.name = os.path.basename(cloned_repo.location)
@ -125,6 +126,7 @@ def __get_validated_git_repos__(repos_relative):
return repos return repos
def main(): def main():
terminal.check_terminal_encoding() terminal.check_terminal_encoding()
terminal.set_stdin_encoding() terminal.set_stdin_encoding()
@ -133,23 +135,40 @@ def main():
repos = [] repos = []
try: try:
opts, args = optval.gnu_getopt(argv[1:], "f:F:hHlLmrTwx:", ["exclude=", "file-types=", "format=", opts, args = optval.gnu_getopt(
"hard:true", "help", "list-file-types:true", "localize-output:true", argv[1:],
"metrics:true", "responsibilities:true", "since=", "grading:true", "f:F:hHlLmrTwx:",
"timeline:true", "until=", "version", "weeks:true"]) [
"exclude=",
"file-types=",
"format=",
"hard:true",
"help",
"list-file-types:true",
"localize-output:true",
"metrics:true",
"responsibilities:true",
"since=",
"grading:true",
"timeline:true",
"until=",
"version",
"weeks:true",
],
)
repos = __get_validated_git_repos__(set(args)) repos = __get_validated_git_repos__(set(args))
#We need the repos above to be set before we read the git config. # We need the repos above to be set before we read the git config.
GitConfig(run, repos[-1].location).read() GitConfig(run, repos[-1].location).read()
clear_x_on_next_pass = True clear_x_on_next_pass = True
for o, a in opts: for o, a in opts:
if o in("-h", "--help"): if o in ("-h", "--help"):
help.output() help.output()
sys.exit(0) sys.exit(0)
elif o in("-f", "--file-types"): elif o in ("-f", "--file-types"):
extensions.define(a) extensions.define(a)
elif o in("-F", "--format"): elif o in ("-F", "--format"):
if not format.select(a): if not format.select(a):
raise format.InvalidFormatError(_("specified output format not supported.")) raise format.InvalidFormatError(_("specified output format not supported."))
elif o == "-H": elif o == "-H":
@ -196,7 +215,7 @@ def main():
run.useweeks = True run.useweeks = True
elif o == "--weeks": elif o == "--weeks":
run.useweeks = optval.get_boolean_argument(a) run.useweeks = optval.get_boolean_argument(a)
elif o in("-x", "--exclude"): elif o in ("-x", "--exclude"):
if clear_x_on_next_pass: if clear_x_on_next_pass:
clear_x_on_next_pass = False clear_x_on_next_pass = False
filtering.clear() filtering.clear()
@ -210,9 +229,11 @@ def main():
print(_("Try `{0} --help' for more information.").format(sys.argv[0]), file=sys.stderr) print(_("Try `{0} --help' for more information.").format(sys.argv[0]), file=sys.stderr)
sys.exit(2) sys.exit(2)
@atexit.register @atexit.register
def cleanup(): def cleanup():
clone.delete() clone.delete()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -27,6 +27,7 @@ except:
from . import format from . import format
def get_url(email, size=20): def get_url(email, size=20):
md5hash = hashlib.md5(email.encode("utf-8").lower().strip()).hexdigest() md5hash = hashlib.md5(email.encode("utf-8").lower().strip()).hexdigest()
base_url = "https://www.gravatar.com/avatar/" + md5hash base_url = "https://www.gravatar.com/avatar/" + md5hash

View File

@ -18,13 +18,13 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import sys import sys
from .extensions import DEFAULT_EXTENSIONS from .extensions import DEFAULT_EXTENSIONS
from .format import __available_formats__ from .format import __available_formats__
__doc__ = _("""Usage: {0} [OPTION]... [REPOSITORY]... __doc__ = _(
"""Usage: {0} [OPTION]... [REPOSITORY]...
List information about the repository in REPOSITORY. If no repository is List information about the repository in REPOSITORY. If no repository is
specified, the current directory is used. If multiple repositories are specified, the current directory is used. If multiple repositories are
given, information will be merged into a unified statistical report. given, information will be merged into a unified statistical report.
@ -76,7 +76,9 @@ add or remove one of the specified extensions, see -f or --file-types for
more information. more information.
gitinspector requires that the git executable is available in your PATH. gitinspector requires that the git executable is available in your PATH.
Report gitinspector bugs to gitinspector@ejwa.se.""") Report gitinspector bugs to gitinspector@ejwa.se."""
)
def output(): def output():
print(__doc__.format(sys.argv[0], ",".join(DEFAULT_EXTENSIONS), ",".join(__available_formats__))) print(__doc__.format(sys.argv[0], ",".join(DEFAULT_EXTENSIONS), ",".join(__available_formats__)))

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
try: try:
from shlex import quote from shlex import quote
except ImportError: except ImportError:
@ -30,26 +29,33 @@ __until__ = ""
__ref__ = "HEAD" __ref__ = "HEAD"
def has_interval(): def has_interval():
return __since__ + __until__ != "" return __since__ + __until__ != ""
def get_since(): def get_since():
return __since__ return __since__
def set_since(since): def set_since(since):
global __since__ global __since__
__since__ = "--since=" + quote(since) __since__ = "--since=" + quote(since)
def get_until(): def get_until():
return __until__ return __until__
def set_until(until): def set_until(until):
global __until__ global __until__
__until__ = "--until=" + quote(until) __until__ = "--until=" + quote(until)
def get_ref(): def get_ref():
return __ref__ return __ref__
def set_ref(ref): def set_ref(ref):
global __ref__ global __ref__
__ref__ = ref __ref__ = ref

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import gettext import gettext
import locale import locale
import os import os
@ -31,10 +30,12 @@ __enabled__ = False
__installed__ = False __installed__ = False
__translation__ = None __translation__ = None
#Dummy function used to handle string constants
# Dummy function used to handle string constants
def N_(message): def N_(message):
return message return message
def init(): def init():
global __enabled__ global __enabled__
global __installed__ global __installed__
@ -48,12 +49,12 @@ def init():
else: else:
lang = locale.getlocale() lang = locale.getlocale()
#Fix for non-POSIX-compliant systems (Windows et al.). # Fix for non-POSIX-compliant systems (Windows et al.).
if os.getenv('LANG') is None: if os.getenv("LANG") is None:
lang = locale.getdefaultlocale() lang = locale.getdefaultlocale()
if lang[0]: if lang[0]:
os.environ['LANG'] = lang[0] os.environ["LANG"] = lang[0]
if lang[0] is not None: if lang[0] is not None:
filename = basedir.get_basedir() + "/translations/messages_%s.mo" % lang[0][0:2] filename = basedir.get_basedir() + "/translations/messages_%s.mo" % lang[0][0:2]
@ -70,27 +71,32 @@ def init():
__installed__ = True __installed__ = True
__translation__.install() __translation__.install()
def check_compatibility(version): def check_compatibility(version):
if isinstance(__translation__, gettext.GNUTranslations): if isinstance(__translation__, gettext.GNUTranslations):
header_pattern = re.compile("^([^:\n]+): *(.*?) *$", re.MULTILINE) header_pattern = re.compile("^([^:\n]+): *(.*?) *$", re.MULTILINE)
header_entries = dict(header_pattern.findall(_(""))) header_entries = dict(header_pattern.findall(_("")))
if header_entries["Project-Id-Version"] != "gitinspector {0}".format(version): if header_entries["Project-Id-Version"] != "gitinspector {0}".format(version):
print("WARNING: The translation for your system locale is not up to date with the current gitinspector " print(
"WARNING: The translation for your system locale is not up to date with the current gitinspector "
"version. The current maintainer of this locale is {0}.".format(header_entries["Last-Translator"]), "version. The current maintainer of this locale is {0}.".format(header_entries["Last-Translator"]),
file=sys.stderr) file=sys.stderr,
)
def get_date(): def get_date():
if __enabled__ and isinstance(__translation__, gettext.GNUTranslations): if __enabled__ and isinstance(__translation__, gettext.GNUTranslations):
date = time.strftime("%x") date = time.strftime("%x")
if hasattr(date, 'decode'): if hasattr(date, "decode"):
date = date.decode("utf-8", "replace") date = date.decode("utf-8", "replace")
return date return date
else: else:
return time.strftime("%Y/%m/%d") return time.strftime("%Y/%m/%d")
def enable(): def enable():
if isinstance(__translation__, gettext.GNUTranslations): if isinstance(__translation__, gettext.GNUTranslations):
__translation__.install(True) __translation__.install(True)
@ -98,6 +104,7 @@ def enable():
global __enabled__ global __enabled__
__enabled__ = True __enabled__ = True
def disable(): def disable():
global __enabled__ global __enabled__
__enabled__ = False __enabled__ = False

View File

@ -23,29 +23,62 @@ import subprocess
from .changes import FileDiff from .changes import FileDiff
from . import comment, filtering, interval from . import comment, filtering, interval
__metric_eloc__ = {"java": 500, "c": 500, "cpp": 500, "cs": 500, "h": 300, "hpp": 300, "php": 500, "py": 500, "glsl": 1000, __metric_eloc__ = {
"rb": 500, "js": 500, "sql": 1000, "xml": 1000} "java": 500,
"c": 500,
"cpp": 500,
"cs": 500,
"h": 300,
"hpp": 300,
"php": 500,
"py": 500,
"glsl": 1000,
"rb": 500,
"js": 500,
"sql": 1000,
"xml": 1000,
}
__metric_cc_tokens__ = [[["java", "js", "c", "cc", "cpp"], ["else", r"for\s+\(.*\)", r"if\s+\(.*\)", r"case\s+\w+:", __metric_cc_tokens__ = [
"default:", r"while\s+\(.*\)"], [
["assert", "break", "continue", "return"]], ["java", "js", "c", "cc", "cpp"],
[["cs"], ["else", r"for\s+\(.*\)", r"foreach\s+\(.*\)", r"goto\s+\w+:", r"if\s+\(.*\)", r"case\s+\w+:", ["else", r"for\s+\(.*\)", r"if\s+\(.*\)", r"case\s+\w+:", "default:", r"while\s+\(.*\)"],
"default:", r"while\s+\(.*\)"], ["assert", "break", "continue", "return"],
["assert", "break", "continue", "return"]], ],
[["py"], [r"^\s+elif .*:$", r"^\s+else:$", r"^\s+for .*:", r"^\s+if .*:$", r"^\s+while .*:$"], [
[r"^\s+assert", "break", "continue", "return"]]] ["cs"],
[
"else",
r"for\s+\(.*\)",
r"foreach\s+\(.*\)",
r"goto\s+\w+:",
r"if\s+\(.*\)",
r"case\s+\w+:",
"default:",
r"while\s+\(.*\)",
],
["assert", "break", "continue", "return"],
],
[
["py"],
[r"^\s+elif .*:$", r"^\s+else:$", r"^\s+for .*:", r"^\s+if .*:$", r"^\s+while .*:$"],
[r"^\s+assert", "break", "continue", "return"],
],
]
METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD = 50 METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD = 50
METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD = 0.75 METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD = 0.75
class MetricsLogic(object): class MetricsLogic(object):
def __init__(self): def __init__(self):
self.eloc = {} self.eloc = {}
self.cyclomatic_complexity = {} self.cyclomatic_complexity = {}
self.cyclomatic_complexity_density = {} self.cyclomatic_complexity_density = {}
ls_tree_p = subprocess.Popen(["git", "ls-tree", "--name-only", "-r", interval.get_ref()], ls_tree_p = subprocess.Popen(
stdout=subprocess.PIPE, stderr=subprocess.STDOUT) ["git", "ls-tree", "--name-only", "-r", interval.get_ref()], stdout=subprocess.PIPE, stderr=subprocess.STDOUT
)
lines = ls_tree_p.communicate()[0].splitlines() lines = ls_tree_p.communicate()[0].splitlines()
ls_tree_p.stdout.close() ls_tree_p.stdout.close()
@ -53,17 +86,18 @@ class MetricsLogic(object):
for i in lines: for i in lines:
i = i.strip().decode("unicode_escape", "ignore") i = i.strip().decode("unicode_escape", "ignore")
i = i.encode("latin-1", "replace") i = i.encode("latin-1", "replace")
i = i.decode("utf-8", "replace").strip("\"").strip("'").strip() i = i.decode("utf-8", "replace").strip('"').strip("'").strip()
if FileDiff.is_valid_extension(i) and not filtering.set_filtered(FileDiff.get_filename(i)): if FileDiff.is_valid_extension(i) and not filtering.set_filtered(FileDiff.get_filename(i)):
file_r = subprocess.Popen(["git", "show", interval.get_ref() + ":{0}".format(i.strip())], file_r = subprocess.Popen(
stdout=subprocess.PIPE).stdout.readlines() ["git", "show", interval.get_ref() + ":{0}".format(i.strip())], stdout=subprocess.PIPE
).stdout.readlines()
extension = FileDiff.get_extension(i) extension = FileDiff.get_extension(i)
lines = MetricsLogic.get_eloc(file_r, extension) lines = MetricsLogic.get_eloc(file_r, extension)
cycc = MetricsLogic.get_cyclomatic_complexity(file_r, extension) cycc = MetricsLogic.get_cyclomatic_complexity(file_r, extension)
if __metric_eloc__.get(extension, None) != None and __metric_eloc__[extension] < lines: if __metric_eloc__.get(extension, None) is not None and __metric_eloc__[extension] < lines:
self.eloc[i.strip()] = lines self.eloc[i.strip()] = lines
if METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD < cycc: if METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD < cycc:
@ -79,7 +113,7 @@ class MetricsLogic(object):
self.cyclomatic_complexity_density.update(other.cyclomatic_complexity_density) self.cyclomatic_complexity_density.update(other.cyclomatic_complexity_density)
return self return self
except AttributeError: except AttributeError:
return other; return other
@staticmethod @staticmethod
def get_cyclomatic_complexity(file_r, extension): def get_cyclomatic_complexity(file_r, extension):

View File

@ -20,11 +20,13 @@
import getopt import getopt
class InvalidOptionArgument(Exception): class InvalidOptionArgument(Exception):
def __init__(self, msg): def __init__(self, msg):
super(InvalidOptionArgument, self).__init__(msg) super(InvalidOptionArgument, self).__init__(msg)
self.msg = msg self.msg = msg
def __find_arg_in_options__(arg, options): def __find_arg_in_options__(arg, options):
for opt in options: for opt in options:
if opt[0].find(arg) == 0: if opt[0].find(arg) == 0:
@ -32,6 +34,7 @@ def __find_arg_in_options__(arg, options):
return None return None
def __find_options_to_extend__(long_options): def __find_options_to_extend__(long_options):
options_to_extend = [] options_to_extend = []
@ -43,8 +46,10 @@ def __find_options_to_extend__(long_options):
return options_to_extend return options_to_extend
# This is a duplicate of gnu_getopt, but with support for optional arguments in long options, in the form; "arg:default_value". # This is a duplicate of gnu_getopt, but with support for optional arguments in long options, in the form; "arg:default_value".
def gnu_getopt(args, options, long_options): def gnu_getopt(args, options, long_options):
options_to_extend = __find_options_to_extend__(long_options) options_to_extend = __find_options_to_extend__(long_options)
@ -55,10 +60,11 @@ def gnu_getopt(args, options, long_options):
return getopt.gnu_getopt(args, options, long_options) return getopt.gnu_getopt(args, options, long_options)
def get_boolean_argument(arg): def get_boolean_argument(arg):
if isinstance(arg, bool): if isinstance(arg, bool):
return arg return arg
elif arg == None or arg.lower() == "false" or arg.lower() == "f" or arg == "0": elif arg is None or arg.lower() == "false" or arg.lower() == "f" or arg == "0":
return False return False
elif arg.lower() == "true" or arg.lower() == "t" or arg == "1": elif arg.lower() == "true" or arg.lower() == "t" or arg == "1":
return True return True

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import json import json
import sys import sys
import textwrap import textwrap
@ -27,8 +26,10 @@ from .. import format, gravatar, terminal
from ..blame import Blame from ..blame import Blame
from .outputable import Outputable from .outputable import Outputable
BLAME_INFO_TEXT = N_("Below are the number of rows from each author that have survived and are still " BLAME_INFO_TEXT = N_(
"intact in the current revision") "Below are the number of rows from each author that have survived and are still " "intact in the current revision"
)
class BlameOutput(Outputable): class BlameOutput(Outputable):
def __init__(self, changes, blame): def __init__(self, changes, blame):
@ -40,10 +41,11 @@ class BlameOutput(Outputable):
Outputable.__init__(self) Outputable.__init__(self)
def output_html(self): def output_html(self):
blame_xml = "<div><div class=\"box\">" blame_xml = '<div><div class="box">'
blame_xml += "<p>" + _(BLAME_INFO_TEXT) + ".</p><div><table id=\"blame\" class=\"git\">" blame_xml += "<p>" + _(BLAME_INFO_TEXT) + '.</p><div><table id="blame" class="git">'
blame_xml += "<thead><tr> <th>{0}</th> <th>{1}</th> <th>{2}</th> <th>{3}</th> <th>{4}</th> </tr></thead>".format( blame_xml += "<thead><tr> <th>{0}</th> <th>{1}</th> <th>{2}</th> <th>{3}</th> <th>{4}</th> </tr></thead>".format(
_("Author"), _("Rows"), _("Stability"), _("Age"), _("% in comments")) _("Author"), _("Rows"), _("Stability"), _("Age"), _("% in comments")
)
blame_xml += "<tbody>" blame_xml += "<tbody>"
chart_data = "" chart_data = ""
blames = sorted(self.blame.get_summed_blames().items()) blames = sorted(self.blame.get_summed_blames().items())
@ -54,11 +56,11 @@ class BlameOutput(Outputable):
for i, entry in enumerate(blames): for i, entry in enumerate(blames):
work_percentage = str("{0:.2f}".format(100.0 * entry[1].rows / total_blames)) work_percentage = str("{0:.2f}".format(100.0 * entry[1].rows / total_blames))
blame_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">") blame_xml += "<tr " + ('class="odd">' if i % 2 == 1 else ">")
if format.get_selected() == "html": if format.get_selected() == "html":
author_email = self.changes.get_latest_email_by_author(entry[0]) author_email = self.changes.get_latest_email_by_author(entry[0])
blame_xml += "<td><img src=\"{0}\"/>{1}</td>".format(gravatar.get_url(author_email), entry[0]) blame_xml += '<td><img src="{0}"/>{1}</td>'.format(gravatar.get_url(author_email), entry[0])
else: else:
blame_xml += "<td>" + entry[0] + "</td>" blame_xml += "<td>" + entry[0] + "</td>"
@ -66,24 +68,24 @@ class BlameOutput(Outputable):
blame_xml += "<td>" + ("{0:.1f}".format(Blame.get_stability(entry[0], entry[1].rows, self.changes)) + "</td>") blame_xml += "<td>" + ("{0:.1f}".format(Blame.get_stability(entry[0], entry[1].rows, self.changes)) + "</td>")
blame_xml += "<td>" + "{0:.1f}".format(float(entry[1].skew) / entry[1].rows) + "</td>" blame_xml += "<td>" + "{0:.1f}".format(float(entry[1].skew) / entry[1].rows) + "</td>"
blame_xml += "<td>" + "{0:.2f}".format(100.0 * entry[1].comments / entry[1].rows) + "</td>" blame_xml += "<td>" + "{0:.2f}".format(100.0 * entry[1].comments / entry[1].rows) + "</td>"
blame_xml += "<td style=\"display: none\">" + work_percentage + "</td>" blame_xml += '<td style="display: none">' + work_percentage + "</td>"
blame_xml += "</tr>" blame_xml += "</tr>"
chart_data += "{{label: {0}, data: {1}}}".format(json.dumps(entry[0]), work_percentage) chart_data += "{{label: {0}, data: {1}}}".format(json.dumps(entry[0]), work_percentage)
if blames[-1] != entry: if blames[-1] != entry:
chart_data += ", " chart_data += ", "
blame_xml += "<tfoot><tr> <td colspan=\"5\">&nbsp;</td> </tr></tfoot></tbody></table>" blame_xml += '<tfoot><tr> <td colspan="5">&nbsp;</td> </tr></tfoot></tbody></table>'
blame_xml += "<div class=\"chart\" id=\"blame_chart\"></div></div>" blame_xml += '<div class="chart" id="blame_chart"></div></div>'
blame_xml += "<script type=\"text/javascript\">" blame_xml += '<script type="text/javascript">'
blame_xml += " blame_plot = $.plot($(\"#blame_chart\"), [{0}], {{".format(chart_data) blame_xml += ' blame_plot = $.plot($("#blame_chart"), [{0}], {{'.format(chart_data)
blame_xml += " series: {" blame_xml += " series: {"
blame_xml += " pie: {" blame_xml += " pie: {"
blame_xml += " innerRadius: 0.4," blame_xml += " innerRadius: 0.4,"
blame_xml += " show: true," blame_xml += " show: true,"
blame_xml += " combine: {" blame_xml += " combine: {"
blame_xml += " threshold: 0.01," blame_xml += " threshold: 0.01,"
blame_xml += " label: \"" + _("Minor Authors") + "\"" blame_xml += ' label: "' + _("Minor Authors") + '"'
blame_xml += " }" blame_xml += " }"
blame_xml += " }" blame_xml += " }"
blame_xml += " }, grid: {" blame_xml += " }, grid: {"
@ -95,35 +97,51 @@ class BlameOutput(Outputable):
print(blame_xml) print(blame_xml)
def output_json(self): def output_json(self):
message_json = "\t\t\t\"message\": \"" + _(BLAME_INFO_TEXT) + "\",\n" message_json = '\t\t\t"message": "' + _(BLAME_INFO_TEXT) + '",\n'
blame_json = "" blame_json = ""
for i in sorted(self.blame.get_summed_blames().items()): for i in sorted(self.blame.get_summed_blames().items()):
author_email = self.changes.get_latest_email_by_author(i[0]) author_email = self.changes.get_latest_email_by_author(i[0])
name_json = "\t\t\t\t\"name\": \"" + i[0] + "\",\n" name_json = '\t\t\t\t"name": "' + i[0] + '",\n'
email_json = "\t\t\t\t\"email\": \"" + author_email + "\",\n" email_json = '\t\t\t\t"email": "' + author_email + '",\n'
gravatar_json = "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n" gravatar_json = '\t\t\t\t"gravatar": "' + gravatar.get_url(author_email) + '",\n'
rows_json = "\t\t\t\t\"rows\": " + str(i[1].rows) + ",\n" rows_json = '\t\t\t\t"rows": ' + str(i[1].rows) + ",\n"
stability_json = ("\t\t\t\t\"stability\": " + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, stability_json = (
self.changes)) + ",\n") '\t\t\t\t"stability": ' + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, self.changes)) + ",\n"
age_json = ("\t\t\t\t\"age\": " + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + ",\n") )
percentage_in_comments_json = ("\t\t\t\t\"percentage_in_comments\": " + age_json = '\t\t\t\t"age": ' + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + ",\n"
"{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + "\n") percentage_in_comments_json = (
blame_json += ("{\n" + name_json + email_json + gravatar_json + rows_json + stability_json + age_json + '\t\t\t\t"percentage_in_comments": ' + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + "\n"
percentage_in_comments_json + "\t\t\t},") )
blame_json += (
"{\n"
+ name_json
+ email_json
+ gravatar_json
+ rows_json
+ stability_json
+ age_json
+ percentage_in_comments_json
+ "\t\t\t},"
)
else: else:
blame_json = blame_json[:-1] blame_json = blame_json[:-1]
print(",\n\t\t\"blame\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + blame_json + "]\n\t\t}", end="") print(',\n\t\t"blame": {\n' + message_json + '\t\t\t"authors": [\n\t\t\t' + blame_json + "]\n\t\t}", end="")
def output_text(self): def output_text(self):
if sys.stdout.isatty() and format.is_interactive_format(): if sys.stdout.isatty() and format.is_interactive_format():
terminal.clear_row() terminal.clear_row()
print(textwrap.fill(_(BLAME_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n") print(textwrap.fill(_(BLAME_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n")
terminal.printb(terminal.ljust(_("Author"), 21) + terminal.rjust(_("Rows"), 10) + terminal.rjust(_("Stability"), 15) + terminal.printb(
terminal.rjust(_("Age"), 13) + terminal.rjust(_("% in comments"), 20)) terminal.ljust(_("Author"), 21)
+ terminal.rjust(_("Rows"), 10)
+ terminal.rjust(_("Stability"), 15)
+ terminal.rjust(_("Age"), 13)
+ terminal.rjust(_("% in comments"), 20)
)
for i in sorted(self.blame.get_summed_blames().items()): for i in sorted(self.blame.get_summed_blames().items()):
print(terminal.ljust(i[0], 20)[0:20 - terminal.get_excess_column_count(i[0])], end=" ") print(terminal.ljust(i[0], 20)[0:20 - terminal.get_excess_column_count(i[0])], end=" ")
@ -143,12 +161,25 @@ class BlameOutput(Outputable):
email_xml = "\t\t\t\t<email>" + author_email + "</email>\n" email_xml = "\t\t\t\t<email>" + author_email + "</email>\n"
gravatar_xml = "\t\t\t\t<gravatar>" + gravatar.get_url(author_email) + "</gravatar>\n" gravatar_xml = "\t\t\t\t<gravatar>" + gravatar.get_url(author_email) + "</gravatar>\n"
rows_xml = "\t\t\t\t<rows>" + str(i[1].rows) + "</rows>\n" rows_xml = "\t\t\t\t<rows>" + str(i[1].rows) + "</rows>\n"
stability_xml = ("\t\t\t\t<stability>" + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, stability_xml = (
self.changes)) + "</stability>\n") "\t\t\t\t<stability>" + "{0:.1f}".format(Blame.get_stability(i[0], i[1].rows, self.changes)) + "</stability>\n"
age_xml = ("\t\t\t\t<age>" + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + "</age>\n") )
percentage_in_comments_xml = ("\t\t\t\t<percentage-in-comments>" + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows) + age_xml = "\t\t\t\t<age>" + "{0:.1f}".format(float(i[1].skew) / i[1].rows) + "</age>\n"
"</percentage-in-comments>\n") percentage_in_comments_xml = (
blame_xml += ("\t\t\t<author>\n" + name_xml + email_xml + gravatar_xml + rows_xml + stability_xml + "\t\t\t\t<percentage-in-comments>"
age_xml + percentage_in_comments_xml + "\t\t\t</author>\n") + "{0:.2f}".format(100.0 * i[1].comments / i[1].rows)
+ "</percentage-in-comments>\n"
)
blame_xml += (
"\t\t\t<author>\n"
+ name_xml
+ email_xml
+ gravatar_xml
+ rows_xml
+ stability_xml
+ age_xml
+ percentage_in_comments_xml
+ "\t\t\t</author>\n"
)
print("\t<blame>\n" + message_xml + "\t\t<authors>\n" + blame_xml + "\t\t</authors>\n\t</blame>") print("\t<blame>\n" + message_xml + "\t\t<authors>\n" + blame_xml + "\t\t</authors>\n\t</blame>")

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import json import json
import textwrap import textwrap
from ..localization import N_ from ..localization import N_
@ -28,6 +27,7 @@ from .outputable import Outputable
HISTORICAL_INFO_TEXT = N_("The following historical commit information, by author, was found") HISTORICAL_INFO_TEXT = N_("The following historical commit information, by author, was found")
NO_COMMITED_FILES_TEXT = N_("No commited files with the specified extensions were found") NO_COMMITED_FILES_TEXT = N_("No commited files with the specified extensions were found")
class ChangesOutput(Outputable): class ChangesOutput(Outputable):
def __init__(self, changes): def __init__(self, changes):
self.changes = changes self.changes = changes
@ -36,7 +36,7 @@ class ChangesOutput(Outputable):
def output_html(self): def output_html(self):
authorinfo_list = self.changes.get_authorinfo_list() authorinfo_list = self.changes.get_authorinfo_list()
total_changes = 0.0 total_changes = 0.0
changes_xml = "<div><div class=\"box\">" changes_xml = '<div><div class="box">'
chart_data = "" chart_data = ""
for i in authorinfo_list: for i in authorinfo_list:
@ -44,20 +44,22 @@ class ChangesOutput(Outputable):
total_changes += authorinfo_list.get(i).deletions total_changes += authorinfo_list.get(i).deletions
if authorinfo_list: if authorinfo_list:
changes_xml += "<p>" + _(HISTORICAL_INFO_TEXT) + ".</p><div><table id=\"changes\" class=\"git\">" changes_xml += "<p>" + _(HISTORICAL_INFO_TEXT) + '.</p><div><table id="changes" class="git">'
changes_xml += "<thead><tr> <th>{0}</th> <th>{1}</th> <th>{2}</th> <th>{3}</th> <th>{4}</th>".format( changes_xml += "<thead><tr> <th>{0}</th> <th>{1}</th> <th>{2}</th> <th>{3}</th> <th>{4}</th>".format(
_("Author"), _("Commits"), _("Insertions"), _("Deletions"), _("% of changes")) _("Author"), _("Commits"), _("Insertions"), _("Deletions"), _("% of changes")
)
changes_xml += "</tr></thead><tbody>" changes_xml += "</tr></thead><tbody>"
for i, entry in enumerate(sorted(authorinfo_list)): for i, entry in enumerate(sorted(authorinfo_list)):
authorinfo = authorinfo_list.get(entry) authorinfo = authorinfo_list.get(entry)
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
changes_xml += "<tr " + ("class=\"odd\">" if i % 2 == 1 else ">") changes_xml += "<tr " + ('class="odd">' if i % 2 == 1 else ">")
if format.get_selected() == "html": if format.get_selected() == "html":
changes_xml += "<td><img src=\"{0}\"/>{1}</td>".format( changes_xml += '<td><img src="{0}"/>{1}</td>'.format(
gravatar.get_url(self.changes.get_latest_email_by_author(entry)), entry) gravatar.get_url(self.changes.get_latest_email_by_author(entry)), entry
)
else: else:
changes_xml += "<td>" + entry + "</td>" changes_xml += "<td>" + entry + "</td>"
@ -71,17 +73,17 @@ class ChangesOutput(Outputable):
if sorted(authorinfo_list)[-1] != entry: if sorted(authorinfo_list)[-1] != entry:
chart_data += ", " chart_data += ", "
changes_xml += ("<tfoot><tr> <td colspan=\"5\">&nbsp;</td> </tr></tfoot></tbody></table>") changes_xml += '<tfoot><tr> <td colspan="5">&nbsp;</td> </tr></tfoot></tbody></table>'
changes_xml += "<div class=\"chart\" id=\"changes_chart\"></div></div>" changes_xml += '<div class="chart" id="changes_chart"></div></div>'
changes_xml += "<script type=\"text/javascript\">" changes_xml += '<script type="text/javascript">'
changes_xml += " changes_plot = $.plot($(\"#changes_chart\"), [{0}], {{".format(chart_data) changes_xml += ' changes_plot = $.plot($("#changes_chart"), [{0}], {{'.format(chart_data)
changes_xml += " series: {" changes_xml += " series: {"
changes_xml += " pie: {" changes_xml += " pie: {"
changes_xml += " innerRadius: 0.4," changes_xml += " innerRadius: 0.4,"
changes_xml += " show: true," changes_xml += " show: true,"
changes_xml += " combine: {" changes_xml += " combine: {"
changes_xml += " threshold: 0.01," changes_xml += " threshold: 0.01,"
changes_xml += " label: \"" + _("Minor Authors") + "\"" changes_xml += ' label: "' + _("Minor Authors") + '"'
changes_xml += " }" changes_xml += " }"
changes_xml += " }" changes_xml += " }"
changes_xml += " }, grid: {" changes_xml += " }, grid: {"
@ -104,7 +106,7 @@ class ChangesOutput(Outputable):
total_changes += authorinfo_list.get(i).deletions total_changes += authorinfo_list.get(i).deletions
if authorinfo_list: if authorinfo_list:
message_json = "\t\t\t\"message\": \"" + _(HISTORICAL_INFO_TEXT) + "\",\n" message_json = '\t\t\t"message": "' + _(HISTORICAL_INFO_TEXT) + '",\n'
changes_json = "" changes_json = ""
for i in sorted(authorinfo_list): for i in sorted(authorinfo_list):
@ -112,23 +114,32 @@ class ChangesOutput(Outputable):
authorinfo = authorinfo_list.get(i) authorinfo = authorinfo_list.get(i)
percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100 percentage = 0 if total_changes == 0 else (authorinfo.insertions + authorinfo.deletions) / total_changes * 100
name_json = "\t\t\t\t\"name\": \"" + i + "\",\n" name_json = '\t\t\t\t"name": "' + i + '",\n'
email_json = "\t\t\t\t\"email\": \"" + author_email + "\",\n" email_json = '\t\t\t\t"email": "' + author_email + '",\n'
gravatar_json = "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n" gravatar_json = '\t\t\t\t"gravatar": "' + gravatar.get_url(author_email) + '",\n'
commits_json = "\t\t\t\t\"commits\": " + str(authorinfo.commits) + ",\n" commits_json = '\t\t\t\t"commits": ' + str(authorinfo.commits) + ",\n"
insertions_json = "\t\t\t\t\"insertions\": " + str(authorinfo.insertions) + ",\n" insertions_json = '\t\t\t\t"insertions": ' + str(authorinfo.insertions) + ",\n"
deletions_json = "\t\t\t\t\"deletions\": " + str(authorinfo.deletions) + ",\n" deletions_json = '\t\t\t\t"deletions": ' + str(authorinfo.deletions) + ",\n"
percentage_json = "\t\t\t\t\"percentage_of_changes\": " + "{0:.2f}".format(percentage) + "\n" percentage_json = '\t\t\t\t"percentage_of_changes": ' + "{0:.2f}".format(percentage) + "\n"
changes_json += ("{\n" + name_json + email_json + gravatar_json + commits_json + changes_json += (
insertions_json + deletions_json + percentage_json + "\t\t\t}") "{\n"
+ name_json
+ email_json
+ gravatar_json
+ commits_json
+ insertions_json
+ deletions_json
+ percentage_json
+ "\t\t\t}"
)
changes_json += "," changes_json += ","
else: else:
changes_json = changes_json[:-1] changes_json = changes_json[:-1]
print("\t\t\"changes\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + changes_json + "]\n\t\t}", end="") print('\t\t"changes": {\n' + message_json + '\t\t\t"authors": [\n\t\t\t' + changes_json + "]\n\t\t}", end="")
else: else:
print("\t\t\"exception\": \"" + _(NO_COMMITED_FILES_TEXT) + "\"") print('\t\t"exception": "' + _(NO_COMMITED_FILES_TEXT) + '"')
def output_text(self): def output_text(self):
authorinfo_list = self.changes.get_authorinfo_list() authorinfo_list = self.changes.get_authorinfo_list()
@ -140,9 +151,13 @@ class ChangesOutput(Outputable):
if authorinfo_list: if authorinfo_list:
print(textwrap.fill(_(HISTORICAL_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n") print(textwrap.fill(_(HISTORICAL_INFO_TEXT) + ":", width=terminal.get_size()[0]) + "\n")
terminal.printb(terminal.ljust(_("Author"), 21) + terminal.rjust(_("Commits"), 13) + terminal.printb(
terminal.rjust(_("Insertions"), 14) + terminal.rjust(_("Deletions"), 15) + terminal.ljust(_("Author"), 21)
terminal.rjust(_("% of changes"), 16)) + terminal.rjust(_("Commits"), 13)
+ terminal.rjust(_("Insertions"), 14)
+ terminal.rjust(_("Deletions"), 15)
+ terminal.rjust(_("% of changes"), 16)
)
for i in sorted(authorinfo_list): for i in sorted(authorinfo_list):
authorinfo = authorinfo_list.get(i) authorinfo = authorinfo_list.get(i)
@ -179,10 +194,21 @@ class ChangesOutput(Outputable):
commits_xml = "\t\t\t\t<commits>" + str(authorinfo.commits) + "</commits>\n" commits_xml = "\t\t\t\t<commits>" + str(authorinfo.commits) + "</commits>\n"
insertions_xml = "\t\t\t\t<insertions>" + str(authorinfo.insertions) + "</insertions>\n" insertions_xml = "\t\t\t\t<insertions>" + str(authorinfo.insertions) + "</insertions>\n"
deletions_xml = "\t\t\t\t<deletions>" + str(authorinfo.deletions) + "</deletions>\n" deletions_xml = "\t\t\t\t<deletions>" + str(authorinfo.deletions) + "</deletions>\n"
percentage_xml = "\t\t\t\t<percentage-of-changes>" + "{0:.2f}".format(percentage) + "</percentage-of-changes>\n" percentage_xml = (
"\t\t\t\t<percentage-of-changes>" + "{0:.2f}".format(percentage) + "</percentage-of-changes>\n"
)
changes_xml += ("\t\t\t<author>\n" + name_xml + email_xml + gravatar_xml + commits_xml + changes_xml += (
insertions_xml + deletions_xml + percentage_xml + "\t\t\t</author>\n") "\t\t\t<author>\n"
+ name_xml
+ email_xml
+ gravatar_xml
+ commits_xml
+ insertions_xml
+ deletions_xml
+ percentage_xml
+ "\t\t\t</author>\n"
)
print("\t<changes>\n" + message_xml + "\t\t<authors>\n" + changes_xml + "\t\t</authors>\n\t</changes>") print("\t<changes>\n" + message_xml + "\t\t<authors>\n" + changes_xml + "\t\t</authors>\n\t</changes>")
else: else:

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import textwrap import textwrap
from ..localization import N_ from ..localization import N_
from .. import extensions, terminal from .. import extensions, terminal
@ -28,6 +27,7 @@ from .outputable import Outputable
EXTENSIONS_INFO_TEXT = N_("The extensions below were found in the repository history") EXTENSIONS_INFO_TEXT = N_("The extensions below were found in the repository history")
EXTENSIONS_MARKED_TEXT = N_("(extensions used during statistical analysis are marked)") EXTENSIONS_MARKED_TEXT = N_("(extensions used during statistical analysis are marked)")
class ExtensionsOutput(Outputable): class ExtensionsOutput(Outputable):
@staticmethod @staticmethod
def is_marked(extension): def is_marked(extension):
@ -38,7 +38,7 @@ class ExtensionsOutput(Outputable):
def output_html(self): def output_html(self):
if extensions.__located_extensions__: if extensions.__located_extensions__:
extensions_xml = "<div><div class=\"box\">" extensions_xml = '<div><div class="box">'
extensions_xml += "<p>{0} {1}.</p><p>".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT)) extensions_xml += "<p>{0} {1}.</p><p>".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT))
for i in sorted(extensions.__located_extensions__): for i in sorted(extensions.__located_extensions__):
@ -53,32 +53,45 @@ class ExtensionsOutput(Outputable):
def output_json(self): def output_json(self):
if extensions.__located_extensions__: if extensions.__located_extensions__:
message_json = "\t\t\t\"message\": \"" + _(EXTENSIONS_INFO_TEXT) + "\",\n" message_json = '\t\t\t"message": "' + _(EXTENSIONS_INFO_TEXT) + '",\n'
used_extensions_json = "" used_extensions_json = ""
unused_extensions_json = "" unused_extensions_json = ""
for i in sorted(extensions.__located_extensions__): for i in sorted(extensions.__located_extensions__):
if ExtensionsOutput.is_marked(i): if ExtensionsOutput.is_marked(i):
used_extensions_json += "\"" + i + "\", " used_extensions_json += '"' + i + '", '
else: else:
unused_extensions_json += "\"" + i + "\", " unused_extensions_json += '"' + i + '", '
used_extensions_json = used_extensions_json[:-2] used_extensions_json = used_extensions_json[:-2]
unused_extensions_json = unused_extensions_json[:-2] unused_extensions_json = unused_extensions_json[:-2]
print(",\n\t\t\"extensions\": {\n" + message_json + "\t\t\t\"used\": [ " + used_extensions_json + print(
" ],\n\t\t\t\"unused\": [ " + unused_extensions_json + " ]\n" + "\t\t}", end="") ',\n\t\t"extensions": {\n'
+ message_json
+ '\t\t\t"used": [ '
+ used_extensions_json
+ ' ],\n\t\t\t"unused": [ '
+ unused_extensions_json
+ " ]\n"
+ "\t\t}",
end="",
)
def output_text(self): def output_text(self):
if extensions.__located_extensions__: if extensions.__located_extensions__:
print("\n" + textwrap.fill("{0} {1}:".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT)), print(
width=terminal.get_size()[0])) "\n"
+ textwrap.fill(
"{0} {1}:".format(_(EXTENSIONS_INFO_TEXT), _(EXTENSIONS_MARKED_TEXT)), width=terminal.get_size()[0]
)
)
for i in sorted(extensions.__located_extensions__): for i in sorted(extensions.__located_extensions__):
if ExtensionsOutput.is_marked(i): if ExtensionsOutput.is_marked(i):
print("[" + terminal.__bold__ + i + terminal.__normal__ + "]", end=" ") print("[" + terminal.__bold__ + i + terminal.__normal__ + "]", end=" ")
else: else:
print (i, end=" ") print(i, end=" ")
print("") print("")
def output_xml(self): def output_xml(self):
@ -93,5 +106,14 @@ class ExtensionsOutput(Outputable):
else: else:
unused_extensions_xml += "\t\t\t<extension>" + i + "</extension>\n" unused_extensions_xml += "\t\t\t<extension>" + i + "</extension>\n"
print("\t<extensions>\n" + message_xml + "\t\t<used>\n" + used_extensions_xml + "\t\t</used>\n" + print(
"\t\t<unused>\n" + unused_extensions_xml + "\t\t</unused>\n" + "\t</extensions>") "\t<extensions>\n"
+ message_xml
+ "\t\t<used>\n"
+ used_extensions_xml
+ "\t\t</used>\n"
+ "\t\t<unused>\n"
+ unused_extensions_xml
+ "\t\t</unused>\n"
+ "\t</extensions>"
)

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import textwrap import textwrap
from ..localization import N_ from ..localization import N_
from ..filtering import __filters__, has_filtered from ..filtering import __filters__, has_filtered
@ -26,11 +25,16 @@ from .. import terminal
from .outputable import Outputable from .outputable import Outputable
FILTERING_INFO_TEXT = N_("The following files were excluded from the statistics due to the specified exclusion patterns") FILTERING_INFO_TEXT = N_("The following files were excluded from the statistics due to the specified exclusion patterns")
FILTERING_AUTHOR_INFO_TEXT = N_("The following authors were excluded from the statistics due to the specified exclusion patterns") FILTERING_AUTHOR_INFO_TEXT = N_(
FILTERING_EMAIL_INFO_TEXT = N_("The authors with the following emails were excluded from the statistics due to the specified " \ "The following authors were excluded from the statistics due to the specified exclusion patterns"
"exclusion patterns") )
FILTERING_COMMIT_INFO_TEXT = N_("The following commit revisions were excluded from the statistics due to the specified " \ FILTERING_EMAIL_INFO_TEXT = N_(
"exclusion patterns") "The authors with the following emails were excluded from the statistics due to the specified " "exclusion patterns"
)
FILTERING_COMMIT_INFO_TEXT = N_(
"The following commit revisions were excluded from the statistics due to the specified " "exclusion patterns"
)
class FilteringOutput(Outputable): class FilteringOutput(Outputable):
@staticmethod @staticmethod
@ -38,7 +42,7 @@ class FilteringOutput(Outputable):
filtering_xml = "" filtering_xml = ""
if filtered: if filtered:
filtering_xml += "<p>" + info_string + "."+ "</p>" filtering_xml += "<p>" + info_string + "." + "</p>"
for i in filtered: for i in filtered:
filtering_xml += "<p>" + i + "</p>" filtering_xml += "<p>" + i + "</p>"
@ -47,7 +51,7 @@ class FilteringOutput(Outputable):
def output_html(self): def output_html(self):
if has_filtered(): if has_filtered():
filtering_xml = "<div><div class=\"box\">" filtering_xml = '<div><div class="box">'
FilteringOutput.__output_html_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1]) FilteringOutput.__output_html_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1])
FilteringOutput.__output_html_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1]) FilteringOutput.__output_html_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1])
FilteringOutput.__output_html_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1]) FilteringOutput.__output_html_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1])
@ -59,26 +63,35 @@ class FilteringOutput(Outputable):
@staticmethod @staticmethod
def __output_json_section__(info_string, filtered, container_tagname): def __output_json_section__(info_string, filtered, container_tagname):
if filtered: if filtered:
message_json = "\t\t\t\t\"message\": \"" + info_string + "\",\n" message_json = '\t\t\t\t"message": "' + info_string + '",\n'
filtering_json = "" filtering_json = ""
for i in filtered: for i in filtered:
filtering_json += "\t\t\t\t\t\"" + i + "\",\n" filtering_json += '\t\t\t\t\t"' + i + '",\n'
else: else:
filtering_json = filtering_json[:-3] filtering_json = filtering_json[:-3]
return "\n\t\t\t\"{0}\": {{\n".format(container_tagname) + message_json + \ return (
"\t\t\t\t\"entries\": [\n" + filtering_json + "\"\n\t\t\t\t]\n\t\t\t}," '\n\t\t\t"{0}": {{\n'.format(container_tagname)
+ message_json
+ '\t\t\t\t"entries": [\n'
+ filtering_json
+ '"\n\t\t\t\t]\n\t\t\t},'
)
return "" return ""
def output_json(self): def output_json(self):
if has_filtered(): if has_filtered():
output = ",\n\t\t\"filtering\": {" output = ',\n\t\t"filtering": {'
output += FilteringOutput.__output_json_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1], "files") output += FilteringOutput.__output_json_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1], "files")
output += FilteringOutput.__output_json_section__(_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1], "authors") output += FilteringOutput.__output_json_section__(
_(FILTERING_AUTHOR_INFO_TEXT), __filters__["author"][1], "authors"
)
output += FilteringOutput.__output_json_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1], "emails") output += FilteringOutput.__output_json_section__(_(FILTERING_EMAIL_INFO_TEXT), __filters__["email"][1], "emails")
output += FilteringOutput.__output_json_section__(_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1], "revision") output += FilteringOutput.__output_json_section__(
_(FILTERING_COMMIT_INFO_TEXT), __filters__["revision"][1], "revision"
)
output = output[:-1] output = output[:-1]
output += "\n\t\t}" output += "\n\t\t}"
print(output, end="") print(output, end="")
@ -90,7 +103,7 @@ class FilteringOutput(Outputable):
for i in filtered: for i in filtered:
(width, _unused) = terminal.get_size() (width, _unused) = terminal.get_size()
print("...%s" % i[-width+3:] if len(i) > width else i) print("...%s" % i[-width + 3:] if len(i) > width else i)
def output_text(self): def output_text(self):
FilteringOutput.__output_text_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1]) FilteringOutput.__output_text_section__(_(FILTERING_INFO_TEXT), __filters__["file"][1])

View File

@ -18,25 +18,27 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
from ..changes import FileDiff from ..changes import FileDiff
from ..localization import N_ from ..localization import N_
from ..metrics import (__metric_eloc__, METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD) from ..metrics import __metric_eloc__, METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD
from .outputable import Outputable from .outputable import Outputable
ELOC_INFO_TEXT = N_("The following files are suspiciously big (in order of severity)") ELOC_INFO_TEXT = N_("The following files are suspiciously big (in order of severity)")
CYCLOMATIC_COMPLEXITY_TEXT = N_("The following files have an elevated cyclomatic complexity (in order of severity)") CYCLOMATIC_COMPLEXITY_TEXT = N_("The following files have an elevated cyclomatic complexity (in order of severity)")
CYCLOMATIC_COMPLEXITY_DENSITY_TEXT = N_("The following files have an elevated cyclomatic complexity density " \ CYCLOMATIC_COMPLEXITY_DENSITY_TEXT = N_(
"(in order of severity)") "The following files have an elevated cyclomatic complexity density " "(in order of severity)"
)
METRICS_MISSING_INFO_TEXT = N_("No metrics violations were found in the repository") METRICS_MISSING_INFO_TEXT = N_("No metrics violations were found in the repository")
METRICS_VIOLATION_SCORES = [[1.0, "minimal"], [1.25, "minor"], [1.5, "medium"], [2.0, "bad"], [3.0, "severe"]] METRICS_VIOLATION_SCORES = [[1.0, "minimal"], [1.25, "minor"], [1.5, "medium"], [2.0, "bad"], [3.0, "severe"]]
def __get_metrics_score__(ceiling, value): def __get_metrics_score__(ceiling, value):
for i in reversed(METRICS_VIOLATION_SCORES): for i in reversed(METRICS_VIOLATION_SCORES):
if value > ceiling * i[0]: if value > ceiling * i[0]:
return i[1] return i[1]
class MetricsOutput(Outputable): class MetricsOutput(Outputable):
def __init__(self, metrics): def __init__(self, metrics):
self.metrics = metrics self.metrics = metrics
@ -58,11 +60,13 @@ class MetricsOutput(Outputable):
if self.metrics.cyclomatic_complexity_density: if self.metrics.cyclomatic_complexity_density:
print("\n" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + ":") print("\n" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + ":")
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True): for i in sorted(
set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True
):
print(_("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0])) print(_("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0]))
def output_html(self): def output_html(self):
metrics_xml = "<div><div class=\"box\" id=\"metrics\">" metrics_xml = '<div><div class="box" id="metrics">'
if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density: if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density:
metrics_xml += "<p>" + _(METRICS_MISSING_INFO_TEXT) + ".</p>" metrics_xml += "<p>" + _(METRICS_MISSING_INFO_TEXT) + ".</p>"
@ -70,25 +74,41 @@ class MetricsOutput(Outputable):
if self.metrics.eloc: if self.metrics.eloc:
metrics_xml += "<div><h4>" + _(ELOC_INFO_TEXT) + ".</h4>" metrics_xml += "<div><h4>" + _(ELOC_INFO_TEXT) + ".</h4>"
for num, i in enumerate(sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True)): for num, i in enumerate(sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True)):
metrics_xml += "<div class=\"" + __get_metrics_score__(__metric_eloc__[FileDiff.get_extension(i[1])], i[0]) + \ metrics_xml += (
(" odd\">" if num % 2 == 1 else "\">") + \ '<div class="'
_("{0} ({1} estimated lines of code)").format(i[1], str(i[0])) + "</div>" + __get_metrics_score__(__metric_eloc__[FileDiff.get_extension(i[1])], i[0])
+ (' odd">' if num % 2 == 1 else '">')
+ _("{0} ({1} estimated lines of code)").format(i[1], str(i[0]))
+ "</div>"
)
metrics_xml += "</div>" metrics_xml += "</div>"
if self.metrics.cyclomatic_complexity: if self.metrics.cyclomatic_complexity:
metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_TEXT) + "</h4>" metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_TEXT) + "</h4>"
for num, i in enumerate(sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True)): for num, i in enumerate(
metrics_xml += "<div class=\"" + __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, i[0]) + \ sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True)
(" odd\">" if num % 2 == 1 else "\">") + \ ):
_("{0} ({1} in cyclomatic complexity)").format(i[1], str(i[0])) + "</div>" metrics_xml += (
'<div class="'
+ __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_THRESHOLD, i[0])
+ (' odd">' if num % 2 == 1 else '">')
+ _("{0} ({1} in cyclomatic complexity)").format(i[1], str(i[0]))
+ "</div>"
)
metrics_xml += "</div>" metrics_xml += "</div>"
if self.metrics.cyclomatic_complexity_density: if self.metrics.cyclomatic_complexity_density:
metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + "</h4>" metrics_xml += "<div><h4>" + _(CYCLOMATIC_COMPLEXITY_DENSITY_TEXT) + "</h4>"
for num, i in enumerate(sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True)): for num, i in enumerate(
metrics_xml += "<div class=\"" + __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD, i[0]) + \ sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True)
(" odd\">" if num % 2 == 1 else "\">") + \ ):
_("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0]) + "</div>" metrics_xml += (
'<div class="'
+ __get_metrics_score__(METRIC_CYCLOMATIC_COMPLEXITY_DENSITY_THRESHOLD, i[0])
+ (' odd">' if num % 2 == 1 else '">')
+ _("{0} ({1:.3f} in cyclomatic complexity density)").format(i[1], i[0])
+ "</div>"
)
metrics_xml += "</div>" metrics_xml += "</div>"
metrics_xml += "</div></div>" metrics_xml += "</div></div>"
@ -96,15 +116,15 @@ class MetricsOutput(Outputable):
def output_json(self): def output_json(self):
if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density: if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density:
print(",\n\t\t\"metrics\": {\n\t\t\t\"message\": \"" + _(METRICS_MISSING_INFO_TEXT) + "\"\n\t\t}", end="") print(',\n\t\t"metrics": {\n\t\t\t"message": "' + _(METRICS_MISSING_INFO_TEXT) + '"\n\t\t}', end="")
else: else:
eloc_json = "" eloc_json = ""
if self.metrics.eloc: if self.metrics.eloc:
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True): for i in sorted(set([(j, i) for (i, j) in list(self.metrics.eloc.items())]), reverse=True):
eloc_json += "{\n\t\t\t\t\"type\": \"estimated-lines-of-code\",\n" eloc_json += '{\n\t\t\t\t"type": "estimated-lines-of-code",\n'
eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n" eloc_json += '\t\t\t\t"file_name": "' + i[1] + '",\n'
eloc_json += "\t\t\t\t\"value\": " + str(i[0]) + "\n" eloc_json += '\t\t\t\t"value": ' + str(i[0]) + "\n"
eloc_json += "\t\t\t}," eloc_json += "\t\t\t},"
else: else:
if not self.metrics.cyclomatic_complexity: if not self.metrics.cyclomatic_complexity:
@ -112,24 +132,27 @@ class MetricsOutput(Outputable):
if self.metrics.cyclomatic_complexity: if self.metrics.cyclomatic_complexity:
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True): for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity.items())]), reverse=True):
eloc_json += "{\n\t\t\t\t\"type\": \"cyclomatic-complexity\",\n" eloc_json += '{\n\t\t\t\t"type": "cyclomatic-complexity",\n'
eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n" eloc_json += '\t\t\t\t"file_name": "' + i[1] + '",\n'
eloc_json += "\t\t\t\t\"value\": " + str(i[0]) + "\n" eloc_json += '\t\t\t\t"value": ' + str(i[0]) + "\n"
eloc_json += "\t\t\t}," eloc_json += "\t\t\t},"
else: else:
if not self.metrics.cyclomatic_complexity_density: if not self.metrics.cyclomatic_complexity_density:
eloc_json = eloc_json[:-1] eloc_json = eloc_json[:-1]
if self.metrics.cyclomatic_complexity_density: if self.metrics.cyclomatic_complexity_density:
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True): for i in sorted(
eloc_json += "{\n\t\t\t\t\"type\": \"cyclomatic-complexity-density\",\n" set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True
eloc_json += "\t\t\t\t\"file_name\": \"" + i[1] + "\",\n" ):
eloc_json += "\t\t\t\t\"value\": {0:.3f}\n".format(i[0]) eloc_json += '{\n\t\t\t\t"type": "cyclomatic-complexity-density",\n'
eloc_json += '\t\t\t\t"file_name": "' + i[1] + '",\n'
eloc_json += '\t\t\t\t"value": {0:.3f}\n'.format(i[0])
eloc_json += "\t\t\t}," eloc_json += "\t\t\t},"
else: else:
eloc_json = eloc_json[:-1] eloc_json = eloc_json[:-1]
print(",\n\t\t\"metrics\": {\n\t\t\t\"violations\": [\n\t\t\t" + eloc_json + "]\n\t\t}", end="") print(',\n\t\t"metrics": {\n\t\t\t"violations": [\n\t\t\t' + eloc_json + "]\n\t\t}", end="")
def output_xml(self): def output_xml(self):
if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density: if not self.metrics.eloc and not self.metrics.cyclomatic_complexity and not self.metrics.cyclomatic_complexity_density:
print("\t<metrics>\n\t\t<message>" + _(METRICS_MISSING_INFO_TEXT) + "</message>\n\t</metrics>") print("\t<metrics>\n\t\t<message>" + _(METRICS_MISSING_INFO_TEXT) + "</message>\n\t</metrics>")
@ -151,7 +174,9 @@ class MetricsOutput(Outputable):
eloc_xml += "\t\t\t</cyclomatic-complexity>\n" eloc_xml += "\t\t\t</cyclomatic-complexity>\n"
if self.metrics.cyclomatic_complexity_density: if self.metrics.cyclomatic_complexity_density:
for i in sorted(set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True): for i in sorted(
set([(j, i) for (i, j) in list(self.metrics.cyclomatic_complexity_density.items())]), reverse=True
):
eloc_xml += "\t\t\t<cyclomatic-complexity-density>\n" eloc_xml += "\t\t\t<cyclomatic-complexity-density>\n"
eloc_xml += "\t\t\t\t<file-name>" + i[1] + "</file-name>\n" eloc_xml += "\t\t\t\t<file-name>" + i[1] + "</file-name>\n"
eloc_xml += "\t\t\t\t<value>{0:.3f}</value>\n".format(i[0]) eloc_xml += "\t\t\t\t<value>{0:.3f}</value>\n".format(i[0])

View File

@ -18,21 +18,22 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
from .. import format from .. import format
class Outputable(object): class Outputable(object):
def output_html(self): def output_html(self):
raise NotImplementedError(_("HTML output not yet supported in") + " \"" + self.__class__.__name__ + "\".") raise NotImplementedError(_("HTML output not yet supported in") + ' "' + self.__class__.__name__ + '".')
def output_json(self): def output_json(self):
raise NotImplementedError(_("JSON output not yet supported in") + " \"" + self.__class__.__name__ + "\".") raise NotImplementedError(_("JSON output not yet supported in") + ' "' + self.__class__.__name__ + '".')
def output_text(self): def output_text(self):
raise NotImplementedError(_("Text output not yet supported in") + " \"" + self.__class__.__name__ + "\".") raise NotImplementedError(_("Text output not yet supported in") + ' "' + self.__class__.__name__ + '".')
def output_xml(self): def output_xml(self):
raise NotImplementedError(_("XML output not yet supported in") + " \"" + self.__class__.__name__ + "\".") raise NotImplementedError(_("XML output not yet supported in") + ' "' + self.__class__.__name__ + '".')
def output(outputable): def output(outputable):
if format.get_selected() == "html" or format.get_selected() == "htmlembedded": if format.get_selected() == "html" or format.get_selected() == "htmlembedded":

View File

@ -18,18 +18,20 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import textwrap import textwrap
from ..localization import N_ from ..localization import N_
from .. import format, gravatar, terminal from .. import format, gravatar, terminal
from .. import responsibilities as resp from .. import responsibilities as resp
from .outputable import Outputable from .outputable import Outputable
RESPONSIBILITIES_INFO_TEXT = N_("The following responsibilities, by author, were found in the current " RESPONSIBILITIES_INFO_TEXT = N_(
"The following responsibilities, by author, were found in the current "
"revision of the repository (comments are excluded from the line count, " "revision of the repository (comments are excluded from the line count, "
"if possible)") "if possible)"
)
MOSTLY_RESPONSIBLE_FOR_TEXT = N_("is mostly responsible for") MOSTLY_RESPONSIBLE_FOR_TEXT = N_("is mostly responsible for")
class ResponsibilitiesOutput(Outputable): class ResponsibilitiesOutput(Outputable):
def __init__(self, changes, blame): def __init__(self, changes, blame):
self.changes = changes self.changes = changes
@ -50,13 +52,13 @@ class ResponsibilitiesOutput(Outputable):
width -= 7 width -= 7
print(str(entry[0]).rjust(6), end=" ") print(str(entry[0]).rjust(6), end=" ")
print("...%s" % entry[1][-width+3:] if len(entry[1]) > width else entry[1]) print("...%s" % entry[1][-width + 3:] if len(entry[1]) > width else entry[1])
if j >= 9: if j >= 9:
break break
def output_html(self): def output_html(self):
resp_xml = "<div><div class=\"box\" id=\"responsibilities\">" resp_xml = '<div><div class="box" id="responsibilities">'
resp_xml += "<p>" + _(RESPONSIBILITIES_INFO_TEXT) + ".</p>" resp_xml += "<p>" + _(RESPONSIBILITIES_INFO_TEXT) + ".</p>"
for i in sorted(set(i[0] for i in self.blame.blames)): for i in sorted(set(i[0] for i in self.blame.blames)):
@ -67,14 +69,16 @@ class ResponsibilitiesOutput(Outputable):
if format.get_selected() == "html": if format.get_selected() == "html":
author_email = self.changes.get_latest_email_by_author(i) author_email = self.changes.get_latest_email_by_author(i)
resp_xml += "<h3><img src=\"{0}\"/>{1} {2}</h3>".format(gravatar.get_url(author_email, size=32), resp_xml += '<h3><img src="{0}"/>{1} {2}</h3>'.format(
i, _(MOSTLY_RESPONSIBLE_FOR_TEXT)) gravatar.get_url(author_email, size=32), i, _(MOSTLY_RESPONSIBLE_FOR_TEXT)
)
else: else:
resp_xml += "<h3>{0} {1}</h3>".format(i, _(MOSTLY_RESPONSIBLE_FOR_TEXT)) resp_xml += "<h3>{0} {1}</h3>".format(i, _(MOSTLY_RESPONSIBLE_FOR_TEXT))
for j, entry in enumerate(responsibilities): for j, entry in enumerate(responsibilities):
resp_xml += "<div" + (" class=\"odd\">" if j % 2 == 1 else ">") + entry[1] + \ resp_xml += (
" (" + str(entry[0]) + " eloc)</div>" "<div" + (' class="odd">' if j % 2 == 1 else ">") + entry[1] + " (" + str(entry[0]) + " eloc)</div>"
)
if j >= 9: if j >= 9:
break break
@ -83,7 +87,7 @@ class ResponsibilitiesOutput(Outputable):
print(resp_xml) print(resp_xml)
def output_json(self): def output_json(self):
message_json = "\t\t\t\"message\": \"" + _(RESPONSIBILITIES_INFO_TEXT) + "\",\n" message_json = '\t\t\t"message": "' + _(RESPONSIBILITIES_INFO_TEXT) + '",\n'
resp_json = "" resp_json = ""
for i in sorted(set(i[0] for i in self.blame.blames)): for i in sorted(set(i[0] for i in self.blame.blames)):
@ -93,15 +97,15 @@ class ResponsibilitiesOutput(Outputable):
author_email = self.changes.get_latest_email_by_author(i) author_email = self.changes.get_latest_email_by_author(i)
resp_json += "{\n" resp_json += "{\n"
resp_json += "\t\t\t\t\"name\": \"" + i + "\",\n" resp_json += '\t\t\t\t"name": "' + i + '",\n'
resp_json += "\t\t\t\t\"email\": \"" + author_email + "\",\n" resp_json += '\t\t\t\t"email": "' + author_email + '",\n'
resp_json += "\t\t\t\t\"gravatar\": \"" + gravatar.get_url(author_email) + "\",\n" resp_json += '\t\t\t\t"gravatar": "' + gravatar.get_url(author_email) + '",\n'
resp_json += "\t\t\t\t\"files\": [\n\t\t\t\t" resp_json += '\t\t\t\t"files": [\n\t\t\t\t'
for j, entry in enumerate(responsibilities): for j, entry in enumerate(responsibilities):
resp_json += "{\n" resp_json += "{\n"
resp_json += "\t\t\t\t\t\"name\": \"" + entry[1] + "\",\n" resp_json += '\t\t\t\t\t"name": "' + entry[1] + '",\n'
resp_json += "\t\t\t\t\t\"rows\": " + str(entry[0]) + "\n" resp_json += '\t\t\t\t\t"rows": ' + str(entry[0]) + "\n"
resp_json += "\t\t\t\t}," resp_json += "\t\t\t\t},"
if j >= 9: if j >= 9:
@ -111,7 +115,7 @@ class ResponsibilitiesOutput(Outputable):
resp_json += "]\n\t\t\t}," resp_json += "]\n\t\t\t},"
resp_json = resp_json[:-1] resp_json = resp_json[:-1]
print(",\n\t\t\"responsibilities\": {\n" + message_json + "\t\t\t\"authors\": [\n\t\t\t" + resp_json + "]\n\t\t}", end="") print(',\n\t\t"responsibilities": {\n' + message_json + '\t\t\t"authors": [\n\t\t\t' + resp_json + "]\n\t\t}", end="")
def output_xml(self): def output_xml(self):
message_xml = "\t\t<message>" + _(RESPONSIBILITIES_INFO_TEXT) + "</message>\n" message_xml = "\t\t<message>" + _(RESPONSIBILITIES_INFO_TEXT) + "</message>\n"

View File

@ -18,7 +18,6 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
import textwrap import textwrap
from ..localization import N_ from ..localization import N_
from .. import format, gravatar, terminal, timeline from .. import format, gravatar, terminal, timeline
@ -27,6 +26,7 @@ from .outputable import Outputable
TIMELINE_INFO_TEXT = N_("The following history timeline has been gathered from the repository") TIMELINE_INFO_TEXT = N_("The following history timeline has been gathered from the repository")
MODIFIED_ROWS_TEXT = N_("Modified Rows:") MODIFIED_ROWS_TEXT = N_("Modified Rows:")
def __output_row__text__(timeline_data, periods, names): def __output_row__text__(timeline_data, periods, names):
print("\n" + terminal.__bold__ + terminal.ljust(_("Author"), 20), end=" ") print("\n" + terminal.__bold__ + terminal.ljust(_("Author"), 20), end=" ")
@ -42,9 +42,13 @@ def __output_row__text__(timeline_data, periods, names):
for period in periods: for period in periods:
multiplier = timeline_data.get_multiplier(period, 9) multiplier = timeline_data.get_multiplier(period, 9)
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
signs_str = (signs[1] * "-" + signs[0] * "+") signs_str = signs[1] * "-" + signs[0] * "+"
print (("." if timeline_data.is_author_in_period(period, name[0]) and print(
len(signs_str) == 0 else signs_str).rjust(10), end=" ") ("." if timeline_data.is_author_in_period(period, name[0]) and len(signs_str) == 0 else signs_str).rjust(
10
),
end=" ",
)
print("") print("")
print(terminal.__bold__ + terminal.ljust(_(MODIFIED_ROWS_TEXT), 20) + terminal.__normal__, end=" ") print(terminal.__bold__ + terminal.ljust(_(MODIFIED_ROWS_TEXT), 20) + terminal.__normal__, end=" ")
@ -52,15 +56,16 @@ def __output_row__text__(timeline_data, periods, names):
for period in periods: for period in periods:
total_changes = str(timeline_data.get_total_changes_in_period(period)[2]) total_changes = str(timeline_data.get_total_changes_in_period(period)[2])
if hasattr(total_changes, 'decode'): if hasattr(total_changes, "decode"):
total_changes = total_changes.decode("utf-8", "replace") total_changes = total_changes.decode("utf-8", "replace")
print(terminal.rjust(total_changes, 10), end=" ") print(terminal.rjust(total_changes, 10), end=" ")
print("") print("")
def __output_row__html__(timeline_data, periods, names): def __output_row__html__(timeline_data, periods, names):
timeline_xml = "<table class=\"git full\"><thead><tr><th>" + _("Author") + "</th>" timeline_xml = '<table class="git full"><thead><tr><th>' + _("Author") + "</th>"
for period in periods: for period in periods:
timeline_xml += "<th>" + str(period) + "</th>" timeline_xml += "<th>" + str(period) + "</th>"
@ -70,19 +75,21 @@ def __output_row__html__(timeline_data, periods, names):
for name in names: for name in names:
if timeline_data.is_author_in_periods(periods, name[0]): if timeline_data.is_author_in_periods(periods, name[0]):
timeline_xml += "<tr" + (" class=\"odd\">" if i % 2 == 1 else ">") timeline_xml += "<tr" + (' class="odd">' if i % 2 == 1 else ">")
if format.get_selected() == "html": if format.get_selected() == "html":
timeline_xml += "<td><img src=\"{0}\"/>{1}</td>".format(gravatar.get_url(name[1]), name[0]) timeline_xml += '<td><img src="{0}"/>{1}</td>'.format(gravatar.get_url(name[1]), name[0])
else: else:
timeline_xml += "<td>" + name[0] + "</td>" timeline_xml += "<td>" + name[0] + "</td>"
for period in periods: for period in periods:
multiplier = timeline_data.get_multiplier(period, 18) multiplier = timeline_data.get_multiplier(period, 18)
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
signs_str = (signs[1] * "<div class=\"remove\">&nbsp;</div>" + signs[0] * "<div class=\"insert\">&nbsp;</div>") signs_str = signs[1] * '<div class="remove">&nbsp;</div>' + signs[0] * '<div class="insert">&nbsp;</div>'
timeline_xml += "<td>" + ("." if timeline_data.is_author_in_period(period, name[0]) and len(signs_str) == 0 else signs_str) timeline_xml += "<td>" + (
"." if timeline_data.is_author_in_period(period, name[0]) and len(signs_str) == 0 else signs_str
)
timeline_xml += "</td>" timeline_xml += "</td>"
timeline_xml += "</tr>" timeline_xml += "</tr>"
i = i + 1 i = i + 1
@ -96,6 +103,7 @@ def __output_row__html__(timeline_data, periods, names):
timeline_xml += "</tr></tfoot></tbody></table>" timeline_xml += "</tr></tfoot></tbody></table>"
print(timeline_xml) print(timeline_xml)
class TimelineOutput(Outputable): class TimelineOutput(Outputable):
def __init__(self, changes, useweeks): def __init__(self, changes, useweeks):
self.changes = changes self.changes = changes
@ -113,7 +121,7 @@ class TimelineOutput(Outputable):
max_periods_per_row = int((width - 21) / 11) max_periods_per_row = int((width - 21) / 11)
for i in range(0, len(periods), max_periods_per_row): for i in range(0, len(periods), max_periods_per_row):
__output_row__text__(timeline_data, periods[i:i+max_periods_per_row], names) __output_row__text__(timeline_data, periods[i:i + max_periods_per_row], names)
def output_html(self): def output_html(self):
if self.changes.get_commits(): if self.changes.get_commits():
@ -122,61 +130,62 @@ class TimelineOutput(Outputable):
names = timeline_data.get_authors() names = timeline_data.get_authors()
max_periods_per_row = 8 max_periods_per_row = 8
timeline_xml = "<div><div id=\"timeline\" class=\"box\">" timeline_xml = '<div><div id="timeline" class="box">'
timeline_xml += "<p>" + _(TIMELINE_INFO_TEXT) + ".</p>" timeline_xml += "<p>" + _(TIMELINE_INFO_TEXT) + ".</p>"
print(timeline_xml) print(timeline_xml)
for i in range(0, len(periods), max_periods_per_row): for i in range(0, len(periods), max_periods_per_row):
__output_row__html__(timeline_data, periods[i:i+max_periods_per_row], names) __output_row__html__(timeline_data, periods[i:i + max_periods_per_row], names)
timeline_xml = "</div></div>" timeline_xml = "</div></div>"
print(timeline_xml) print(timeline_xml)
def output_json(self): def output_json(self):
if self.changes.get_commits(): if self.changes.get_commits():
message_json = "\t\t\t\"message\": \"" + _(TIMELINE_INFO_TEXT) + "\",\n" message_json = '\t\t\t"message": "' + _(TIMELINE_INFO_TEXT) + '",\n'
timeline_json = "" timeline_json = ""
periods_json = "\t\t\t\"period_length\": \"{0}\",\n".format("week" if self.useweeks else "month") periods_json = '\t\t\t"period_length": "{0}",\n'.format("week" if self.useweeks else "month")
periods_json += "\t\t\t\"periods\": [\n\t\t\t" periods_json += '\t\t\t"periods": [\n\t\t\t'
timeline_data = timeline.TimelineData(self.changes, self.useweeks) timeline_data = timeline.TimelineData(self.changes, self.useweeks)
periods = timeline_data.get_periods() periods = timeline_data.get_periods()
names = timeline_data.get_authors() names = timeline_data.get_authors()
for period in periods: for period in periods:
name_json = "\t\t\t\t\"name\": \"" + str(period) + "\",\n" name_json = '\t\t\t\t"name": "' + str(period) + '",\n'
authors_json = "\t\t\t\t\"authors\": [\n\t\t\t\t" authors_json = '\t\t\t\t"authors": [\n\t\t\t\t'
for name in names: for name in names:
if timeline_data.is_author_in_period(period, name[0]): if timeline_data.is_author_in_period(period, name[0]):
multiplier = timeline_data.get_multiplier(period, 24) multiplier = timeline_data.get_multiplier(period, 24)
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
signs_str = (signs[1] * "-" + signs[0] * "+") signs_str = signs[1] * "-" + signs[0] * "+"
if len(signs_str) == 0: if len(signs_str) == 0:
signs_str = "." signs_str = "."
authors_json += "{\n\t\t\t\t\t\"name\": \"" + name[0] + "\",\n" authors_json += '{\n\t\t\t\t\t"name": "' + name[0] + '",\n'
authors_json += "\t\t\t\t\t\"email\": \"" + name[1] + "\",\n" authors_json += '\t\t\t\t\t"email": "' + name[1] + '",\n'
authors_json += "\t\t\t\t\t\"gravatar\": \"" + gravatar.get_url(name[1]) + "\",\n" authors_json += '\t\t\t\t\t"gravatar": "' + gravatar.get_url(name[1]) + '",\n'
authors_json += "\t\t\t\t\t\"work\": \"" + signs_str + "\"\n\t\t\t\t}," authors_json += '\t\t\t\t\t"work": "' + signs_str + '"\n\t\t\t\t},'
else: else:
authors_json = authors_json[:-1] authors_json = authors_json[:-1]
authors_json += "],\n" authors_json += "],\n"
modified_rows_json = "\t\t\t\t\"modified_rows\": " + \ modified_rows_json = (
str(timeline_data.get_total_changes_in_period(period)[2]) + "\n" '\t\t\t\t"modified_rows": ' + str(timeline_data.get_total_changes_in_period(period)[2]) + "\n"
)
timeline_json += "{\n" + name_json + authors_json + modified_rows_json + "\t\t\t}," timeline_json += "{\n" + name_json + authors_json + modified_rows_json + "\t\t\t},"
else: else:
timeline_json = timeline_json[:-1] timeline_json = timeline_json[:-1]
print(",\n\t\t\"timeline\": {\n" + message_json + periods_json + timeline_json + "]\n\t\t}", end="") print(',\n\t\t"timeline": {\n' + message_json + periods_json + timeline_json + "]\n\t\t}", end="")
def output_xml(self): def output_xml(self):
if self.changes.get_commits(): if self.changes.get_commits():
message_xml = "\t\t<message>" + _(TIMELINE_INFO_TEXT) + "</message>\n" message_xml = "\t\t<message>" + _(TIMELINE_INFO_TEXT) + "</message>\n"
timeline_xml = "" timeline_xml = ""
periods_xml = "\t\t<periods length=\"{0}\">\n".format("week" if self.useweeks else "month") periods_xml = '\t\t<periods length="{0}">\n'.format("week" if self.useweeks else "month")
timeline_data = timeline.TimelineData(self.changes, self.useweeks) timeline_data = timeline.TimelineData(self.changes, self.useweeks)
periods = timeline_data.get_periods() periods = timeline_data.get_periods()
@ -190,7 +199,7 @@ class TimelineOutput(Outputable):
if timeline_data.is_author_in_period(period, name[0]): if timeline_data.is_author_in_period(period, name[0]):
multiplier = timeline_data.get_multiplier(period, 24) multiplier = timeline_data.get_multiplier(period, 24)
signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier) signs = timeline_data.get_author_signs_in_period(name[0], period, multiplier)
signs_str = (signs[1] * "-" + signs[0] * "+") signs_str = signs[1] * "-" + signs[0] * "+"
if len(signs_str) == 0: if len(signs_str) == 0:
signs_str = "." signs_str = "."
@ -201,8 +210,11 @@ class TimelineOutput(Outputable):
authors_xml += "\t\t\t\t\t\t<work>" + signs_str + "</work>\n\t\t\t\t\t</author>\n" authors_xml += "\t\t\t\t\t\t<work>" + signs_str + "</work>\n\t\t\t\t\t</author>\n"
authors_xml += "\t\t\t\t</authors>\n" authors_xml += "\t\t\t\t</authors>\n"
modified_rows_xml = "\t\t\t\t<modified_rows>" + \ modified_rows_xml = (
str(timeline_data.get_total_changes_in_period(period)[2]) + "</modified_rows>\n" "\t\t\t\t<modified_rows>"
+ str(timeline_data.get_total_changes_in_period(period)[2])
+ "</modified_rows>\n"
)
timeline_xml += "\t\t\t<period>\n" + name_xml + authors_xml + modified_rows_xml + "\t\t\t</period>\n" timeline_xml += "\t\t\t<period>\n" + name_xml + authors_xml + modified_rows_xml + "\t\t\t</period>\n"
print("\t<timeline>\n" + message_xml + periods_xml + timeline_xml + "\t\t</periods>\n\t</timeline>") print("\t<timeline>\n" + message_xml + periods_xml + timeline_xml + "\t\t</periods>\n\t</timeline>")

View File

@ -18,11 +18,10 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
class ResponsibiltyEntry(object): class ResponsibiltyEntry(object):
blames = {} blames = {}
class Responsibilities(object): class Responsibilities(object):
@staticmethod @staticmethod
def get(blame, author_name): def get(blame, author_name):

View File

@ -29,6 +29,7 @@ __normal__ = "\033[0;0m"
DEFAULT_TERMINAL_SIZE = (80, 25) DEFAULT_TERMINAL_SIZE = (80, 25)
def __get_size_windows__(): def __get_size_windows__():
res = None res = None
try: try:
@ -42,6 +43,7 @@ def __get_size_windows__():
if res: if res:
import struct import struct
(_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack("hhhhHhhhhhh", csbi.raw) (_, _, _, _, _, left, top, right, bottom, _, _) = struct.unpack("hhhhHhhhhhh", csbi.raw)
sizex = right - left + 1 sizex = right - left + 1
sizey = bottom - top + 1 sizey = bottom - top + 1
@ -49,11 +51,13 @@ def __get_size_windows__():
else: else:
return DEFAULT_TERMINAL_SIZE return DEFAULT_TERMINAL_SIZE
def __get_size_linux__(): def __get_size_linux__():
def ioctl_get_window_size(file_descriptor): def ioctl_get_window_size(file_descriptor):
try: try:
import fcntl, termios, struct import fcntl, termios, struct
size = struct.unpack('hh', fcntl.ioctl(file_descriptor, termios.TIOCGWINSZ, "1234"))
size = struct.unpack("hh", fcntl.ioctl(file_descriptor, termios.TIOCGWINSZ, "1234"))
except: except:
return DEFAULT_TERMINAL_SIZE return DEFAULT_TERMINAL_SIZE
@ -76,9 +80,11 @@ def __get_size_linux__():
return int(size[1]), int(size[0]) return int(size[1]), int(size[0])
def clear_row(): def clear_row():
print("\r", end="") print("\r", end="")
def skip_escapes(skip): def skip_escapes(skip):
if skip: if skip:
global __bold__ global __bold__
@ -86,9 +92,11 @@ def skip_escapes(skip):
__bold__ = "" __bold__ = ""
__normal__ = "" __normal__ = ""
def printb(string): def printb(string):
print(__bold__ + string + __normal__) print(__bold__ + string + __normal__)
def get_size(): def get_size():
width = 0 width = 0
height = 0 height = 0
@ -106,14 +114,17 @@ def get_size():
return DEFAULT_TERMINAL_SIZE return DEFAULT_TERMINAL_SIZE
def set_stdout_encoding(): def set_stdout_encoding():
if not sys.stdout.isatty() and sys.version_info < (3,): if not sys.stdout.isatty() and sys.version_info < (3,):
sys.stdout = codecs.getwriter("utf-8")(sys.stdout) sys.stdout = codecs.getwriter("utf-8")(sys.stdout)
def set_stdin_encoding(): def set_stdin_encoding():
if not sys.stdin.isatty() and sys.version_info < (3,): if not sys.stdin.isatty() and sys.version_info < (3,):
sys.stdin = codecs.getreader("utf-8")(sys.stdin) sys.stdin = codecs.getreader("utf-8")(sys.stdin)
def convert_command_line_to_utf8(): def convert_command_line_to_utf8():
try: try:
argv = [] argv = []
@ -125,13 +136,20 @@ def convert_command_line_to_utf8():
except AttributeError: except AttributeError:
return sys.argv return sys.argv
def check_terminal_encoding(): def check_terminal_encoding():
if sys.stdout.isatty() and (sys.stdout.encoding == None or sys.stdin.encoding == None): if sys.stdout.isatty() and (sys.stdout.encoding is None or sys.stdin.encoding is None):
print(_("WARNING: The terminal encoding is not correctly configured. gitinspector might malfunction. " print(
"The encoding can be configured with the environment variable 'PYTHONIOENCODING'."), file=sys.stderr) _(
"WARNING: The terminal encoding is not correctly configured. gitinspector might malfunction. "
"The encoding can be configured with the environment variable 'PYTHONIOENCODING'."
),
file=sys.stderr,
)
def get_excess_column_count(string): def get_excess_column_count(string):
width_mapping = {'F': 2, 'H': 1, 'W': 2, 'Na': 1, 'N': 1, 'A': 1} width_mapping = {"F": 2, "H": 1, "W": 2, "Na": 1, "N": 1, "A": 1}
result = 0 result = 0
for i in string: for i in string:
@ -140,19 +158,22 @@ def get_excess_column_count(string):
return result - len(string) return result - len(string)
def ljust(string, pad): def ljust(string, pad):
return string.ljust(pad - get_excess_column_count(string)) return string.ljust(pad - get_excess_column_count(string))
def rjust(string, pad): def rjust(string, pad):
return string.rjust(pad - get_excess_column_count(string)) return string.rjust(pad - get_excess_column_count(string))
def output_progress(text, pos, length): def output_progress(text, pos, length):
if sys.stdout.isatty(): if sys.stdout.isatty():
(width, _unused) = get_size() (width, _unused) = get_size()
progress_text = text.format(100 * pos / length) progress_text = text.format(100 * pos / length)
if len(progress_text) > width: if len(progress_text) > width:
progress_text = "...%s" % progress_text[-width+3:] progress_text = "...%s" % progress_text[-width + 3:]
print("\r{0}\r{1}".format(" " * width, progress_text), end="") print("\r{0}\r{1}".format(" " * width, progress_text), end="")
sys.stdout.flush() sys.stdout.flush()

View File

@ -20,6 +20,7 @@
import datetime import datetime
class TimelineData(object): class TimelineData(object):
def __init__(self, changes, useweeks): def __init__(self, changes, useweeks):
authordateinfo_list = sorted(changes.get_authordateinfo_list().items()) authordateinfo_list = sorted(changes.get_authordateinfo_list().items())
@ -37,7 +38,7 @@ class TimelineData(object):
else: else:
key = (i[0][1], i[0][0][0:7]) key = (i[0][1], i[0][0][0:7])
if self.entries.get(key, None) == None: if self.entries.get(key, None) is None:
self.entries[key] = i[1] self.entries[key] = i[1]
else: else:
self.entries[key].insertions += i[1].insertions self.entries[key].insertions += i[1].insertions
@ -49,12 +50,11 @@ class TimelineData(object):
for author in self.get_authors(): for author in self.get_authors():
entry = self.entries.get((author[0], period), None) entry = self.entries.get((author[0], period), None)
if entry != None: if entry is not None:
total_insertions += entry.insertions total_insertions += entry.insertions
total_deletions += entry.deletions total_deletions += entry.deletions
self.total_changes_by_period[period] = (total_insertions, total_deletions, self.total_changes_by_period[period] = (total_insertions, total_deletions, total_insertions + total_deletions)
total_insertions + total_deletions)
def get_periods(self): def get_periods(self):
return sorted(set([i[1] for i in self.entries])) return sorted(set([i[1] for i in self.entries]))
@ -91,7 +91,7 @@ class TimelineData(object):
multiplier += 0.25 multiplier += 0.25
def is_author_in_period(self, period, author): def is_author_in_period(self, period, author):
return self.entries.get((author, period), None) != None return self.entries.get((author, period), None) is not None
def is_author_in_periods(self, periods, author): def is_author_in_periods(self, periods, author):
for period in periods: for period in periods:

View File

@ -18,17 +18,21 @@
# along with gitinspector. If not, see <http://www.gnu.org/licenses/>. # along with gitinspector. If not, see <http://www.gnu.org/licenses/>.
from . import localization from . import localization
localization.init() localization.init()
__version__ = "0.5.0dev" __version__ = "0.5.0dev"
__doc__ = _("""Copyright © 2012-2015 Ejwa Software. All rights reserved. __doc__ = _(
"""Copyright © 2012-2015 Ejwa Software. All rights reserved.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>. 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. This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. There is NO WARRANTY, to the extent permitted by law.
Written by Adam Waldenberg.""") Written by Adam Waldenberg."""
)
def output(): def output():
print("gitinspector {0}\n".format(__version__) + __doc__) print("gitinspector {0}\n".format(__version__) + __doc__)

View File

@ -19,10 +19,10 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import os import os
import sys
import unittest import unittest
import gitinspector.comment import gitinspector.comment
def __test_extension__(commented_file, extension): def __test_extension__(commented_file, extension):
base = os.path.dirname(os.path.realpath(__file__)) base = os.path.dirname(os.path.realpath(__file__))
tex_file = open(base + commented_file, "r") tex_file = open(base + commented_file, "r")
@ -38,11 +38,13 @@ def __test_extension__(commented_file, extension):
return comment_counter return comment_counter
class TexFileTest(unittest.TestCase): class TexFileTest(unittest.TestCase):
def test(self): def test(self):
comment_counter = __test_extension__("/resources/commented_file.tex", "tex") comment_counter = __test_extension__("/resources/commented_file.tex", "tex")
self.assertEqual(comment_counter, 30) self.assertEqual(comment_counter, 30)
class CppFileTest(unittest.TestCase): class CppFileTest(unittest.TestCase):
def test(self): def test(self):
comment_counter = __test_extension__("/resources/commented_file.cpp", "cpp") comment_counter = __test_extension__("/resources/commented_file.cpp", "cpp")