Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 48 additions & 41 deletions git-stats
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# Copyright (c) 2007-2010 Heikki Hokkanen <hoxu@users.sf.net> & others (see doc/author.txt)
# GPLv2 / GPLv3
# Ported to Python 3
import datetime
import getopt
import glob
Expand All @@ -17,7 +18,7 @@ import zlib
IMAGE_TYPE = 'svg' # everything that gnuplot accepts, I guess
GNUPLOT_IMAGE_SPECIFICATIONS = {'svg':'','png':'transparent'}

GNUPLOT_COMMON = 'set terminal {0} {1}\nset size 1.0,0.5\n'.format(IMAGE_TYPE, GNUPLOT_IMAGE_SPECIFICATIONS[IMAGE_TYPE] )
GNUPLOT_COMMON = 'set terminal {0} {1}\nset size 1.0,1.0\n'.format(IMAGE_TYPE, GNUPLOT_IMAGE_SPECIFICATIONS[IMAGE_TYPE] )
ON_LINUX = (platform.system() == 'Linux')

exectime_internal = 0.0
Expand All @@ -41,7 +42,7 @@ def getpipeoutput(cmds, quiet = False):
global exectime_external
start = time.time()
if not quiet and ON_LINUX and os.isatty(1):
print '>> ' + ' | '.join(cmds),
print('>> ' + ' | '.join(cmds), end=' ')
sys.stdout.flush()
p0 = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
p = p0
Expand All @@ -52,17 +53,17 @@ def getpipeoutput(cmds, quiet = False):
end = time.time()
if not quiet:
if ON_LINUX and os.isatty(1):
print '\r',
print '[%.5f] >> %s' % (end - start, ' | '.join(cmds))
print('\r', end=' ')
print('[%.5f] >> %s' % (end - start, ' | '.join(cmds)))
exectime_external += (end - start)
return output.rstrip('\n')
return output.decode('utf-8', errors='replace').rstrip('\n')

def getkeyssortedbyvalues(dict):
return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
return [el[1] for el in sorted((val, key) for key, val in dict.items())]

# dict['author'] = { 'commits': 512 } - ...key(dict, 'commits')
def getkeyssortedbyvaluekey(d, key):
return map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys())))
return [el[1] for el in sorted((d[k][key], k) for k in d.keys())]

VERSION = 0
def getversion():
Expand All @@ -88,8 +89,8 @@ class DataCollector:
def loadCache(self, cachefile):
if not os.path.exists(cachefile):
return
print 'Loading cache...'
f = open(cachefile)
print('Loading cache...')
f = open(cachefile, 'rb')
try:
self.cache = pickle.loads(zlib.decompress(f.read()))
except:
Expand Down Expand Up @@ -150,8 +151,8 @@ class DataCollector:
##
# Save cacheable data
def saveCache(self, cachefile):
print 'Saving cache...'
f = open(cachefile, 'w')
print('Saving cache...')
f = open(cachefile, 'wb')
#pickle.dump(self.cache, f)
data = zlib.compress(pickle.dumps(self.cache))
f.write(data)
Expand Down Expand Up @@ -219,7 +220,7 @@ class GitDataCollector(DataCollector):
self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), 'commits': 0, 'authors': {} }

# collect info on tags, starting from latest
tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), self.tags.items()))))
tags_sorted_by_date_desc = [el[1] for el in reversed(sorted((el[1]['date'], el[0]) for el in self.tags.items()))]
prev = None
for tag in reversed(tags_sorted_by_date_desc):
cmd = 'git shortlog -s "%s"' % tag
Expand All @@ -230,7 +231,7 @@ class GitDataCollector(DataCollector):
continue
prev = tag
for line in output.split('\n'):
parts = re.split('\s+', line, 2)
parts = re.split(r'\s+', line, 2)
commits = int(parts[1])
author = parts[2]
self.tags[tag]['commits'] += commits
Expand Down Expand Up @@ -358,7 +359,7 @@ class GitDataCollector(DataCollector):
try:
self.files_by_stamp[int(stamp)] = int(files)
except ValueError:
print 'Warning: failed to parse line "%s"' % line
print('Warning: failed to parse line "%s"' % line)

# extensions
self.extensions = {} # extension -> files, lines
Expand All @@ -367,7 +368,7 @@ class GitDataCollector(DataCollector):
for line in lines:
if len(line) == 0:
continue
parts = re.split('\s+', line, 4)
parts = re.split(r'\s+', line, 4)
sha1 = parts[2]
filename = parts[3]

Expand All @@ -385,7 +386,7 @@ class GitDataCollector(DataCollector):
try:
self.extensions[ext]['lines'] += self.getLinesInBlob(sha1)
except:
print 'Warning: Could not count lines for file "%s"' % line
print('Warning: Could not count lines for file "%s"' % line)

# line statistics
# outputs:
Expand All @@ -401,7 +402,7 @@ class GitDataCollector(DataCollector):
continue

# <stamp> <author>
if line.find('files changed,') == -1:
if re.search(r'\d+ files? changed', line) is None:
pos = line.find(' ')
if pos != -1:
try:
Expand All @@ -412,19 +413,25 @@ class GitDataCollector(DataCollector):
self.authors[author]['lines_added'] = self.authors[author].get('lines_added', 0) + inserted
self.authors[author]['lines_removed'] = self.authors[author].get('lines_removed', 0) + deleted
except ValueError:
print 'Warning: unexpected line "%s"' % line
print('Warning: unexpected line "%s"' % line)
else:
print 'Warning: unexpected line "%s"' % line
print('Warning: unexpected line "%s"' % line)
else:
numbers = re.findall('\d+', line)
if len(numbers) == 3:
(files, inserted, deleted) = map(lambda el : int(el), numbers)
# git omits the insertions/deletions clause entirely when its
# count is zero, so don't assume there are always 3 numbers
files_m = re.search(r'(\d+) files? changed', line)
ins_m = re.search(r'(\d+) insertions?\(\+\)', line)
del_m = re.search(r'(\d+) deletions?\(-\)', line)
if files_m:
files = int(files_m.group(1))
inserted = int(ins_m.group(1)) if ins_m else 0
deleted = int(del_m.group(1)) if del_m else 0
total_lines += inserted
total_lines -= deleted
self.total_lines_added += inserted
self.total_lines_removed += deleted
else:
print 'Warning: failed to handle line "%s"' % line
print('Warning: failed to handle line "%s"' % line)
(files, inserted, deleted) = (0, 0, 0)
#self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
self.total_lines = total_lines
Expand Down Expand Up @@ -465,13 +472,13 @@ class GitDataCollector(DataCollector):
return res[:limit]

def getCommitDeltaDays(self):
return (self.last_commit_stamp - self.first_commit_stamp) / 86400 + 1
return (self.last_commit_stamp - self.first_commit_stamp) // 86400 + 1

def getDomainInfo(self, domain):
return self.domains[domain]

def getDomains(self):
return self.domains.keys()
return list(self.domains.keys())

def getFilesInCommit(self, rev):
try:
Expand Down Expand Up @@ -555,7 +562,7 @@ class HTMLReportCreator(ReportCreator):
shutil.copyfile(src, path + '/' + file)
break
else:
print 'Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs)
print('Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs))

f = open(path + "/index.html", 'w')
format = '%Y-%m-%d %H:%M:%S'
Expand Down Expand Up @@ -870,7 +877,7 @@ class HTMLReportCreator(ReportCreator):
for ext in sorted(data.extensions.keys()):
files = data.extensions[ext]['files']
lines = data.extensions[ext]['lines']
f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d (%.2f%%)</td><td>%d</td></tr>' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, (100.0 * lines) / data.getTotalLOC(), lines / files))
f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d (%.2f%%)</td><td>%d</td></tr>' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, (100.0 * lines) / data.getTotalLOC(), lines // files))
f.write('</table>')

f.write('</body></html>')
Expand Down Expand Up @@ -914,7 +921,7 @@ class HTMLReportCreator(ReportCreator):
f.write('<table class="tags">')
f.write('<tr><th>Name</th><th>Date</th><th>Commits</th><th>Authors</th></tr>')
# sort the tags by date desc
tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
tags_sorted_by_date_desc = [el[1] for el in reversed(sorted((el[1]['date'], el[0]) for el in data.tags.items()))]
for tag in tags_sorted_by_date_desc:
authorinfo = []
authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors'])
Expand All @@ -929,7 +936,7 @@ class HTMLReportCreator(ReportCreator):
self.createGraphs(path)

def createGraphs(self, path):
print 'Generating graphs...'
print('Generating graphs...')

# hour of day
f = open(path + '/hour_of_day.plot', 'w')
Expand Down Expand Up @@ -1065,7 +1072,7 @@ plot 'lines_of_code.dat' using 1:2 w lines
for f in files:
out = getpipeoutput([gnuplot_cmd + ' "%s"' % f])
if len(out) > 0:
print out
print(out)

def printHeader(self, f, title = ''):
f.write(
Expand Down Expand Up @@ -1103,22 +1110,22 @@ class GitStats:
if o == '-c':
key, value = v.split('=', 1)
if key not in conf:
raise 'Error: no such key "%s" in config' % key
raise Exception('Error: no such key "%s" in config' % key)
if isinstance(conf[key], int):
conf[key] = int(value)
else:
conf[key] = value

if len(args) < 2:
print """
print("""
Usage: gitstats [options] <gitpath> <outputpath>

Options:
-c key=value Override configuration value

Default config values:
%s
""" % conf
""" % conf)
sys.exit(0)

gitpath = args[0]
Expand All @@ -1130,33 +1137,33 @@ Default config values:
except OSError:
pass
if not os.path.isdir(outputpath):
print 'FATAL: Output path is not a directory or does not exist'
print('FATAL: Output path is not a directory or does not exist')
sys.exit(1)

print 'Git path: %s' % gitpath
print 'Output path: %s' % outputpath
print('Git path: %s' % gitpath)
print('Output path: %s' % outputpath)

os.chdir(gitpath)

cachefile = os.path.join(outputpath, 'gitstats.cache')

print 'Collecting data...'
print('Collecting data...')
data = GitDataCollector()
data.loadCache(cachefile)
data.collect(gitpath)
print 'Refining data...'
print('Refining data...')
data.saveCache(cachefile)
data.refine()

os.chdir(rundir)

print 'Generating report...'
print('Generating report...')
report = HTMLReportCreator()
report.create(data, outputpath)

time_end = time.time()
exectime_internal = time_end - time_start
print 'Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal)
print('Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal))

g = GitStats()
g.run(sys.argv[1:])
Expand Down