Browse Source

Convert gitstats to use python3

Python2 is no longer supported.

Convert gitstats using 2to3.
Adjust shebang.
Fix subprocess output decoding.
Update doc.
Line Aubechamps 3 years ago
parent
commit
ca415668ce
2 changed files with 48 additions and 48 deletions
  1. 1
    1
      doc/README
  2. 47
    47
      gitstats

+ 1
- 1
doc/README View File

5
 
5
 
6
 Requirements
6
 Requirements
7
 ============
7
 ============
8
-- Python (>= 2.6.0)
8
+- Python (>= 3.4.0)
9
 - Git (>= 1.5.2.4)
9
 - Git (>= 1.5.2.4)
10
 - Gnuplot (>= 4.0.0)
10
 - Gnuplot (>= 4.0.0)
11
 - a git repository (bare clone will work as well)
11
 - a git repository (bare clone will work as well)

+ 47
- 47
gitstats View File

1
-#!/usr/bin/env python2
1
+#!/usr/bin/env python3
2
 # Copyright (c) 2007-2014 Heikki Hokkanen <hoxu@users.sf.net> & others (see doc/AUTHOR)
2
 # Copyright (c) 2007-2014 Heikki Hokkanen <hoxu@users.sf.net> & others (see doc/AUTHOR)
3
 # GPLv2 / GPLv3
3
 # GPLv2 / GPLv3
4
 import datetime
4
 import datetime
14
 import time
14
 import time
15
 import zlib
15
 import zlib
16
 
16
 
17
-if sys.version_info < (2, 6):
18
-	print >> sys.stderr, "Python 2.6 or higher is required for gitstats"
17
+if sys.version_info < (3, 4):
18
+	print("Python 3.4 or higher is required for gitstats", file=sys.stderr)
19
 	sys.exit(1)
19
 	sys.exit(1)
20
 
20
 
21
 from multiprocessing import Pool
21
 from multiprocessing import Pool
54
 	global exectime_external
54
 	global exectime_external
55
 	start = time.time()
55
 	start = time.time()
56
 	if not quiet and ON_LINUX and os.isatty(1):
56
 	if not quiet and ON_LINUX and os.isatty(1):
57
-		print '>> ' + ' | '.join(cmds),
57
+		print('>> ' + ' | '.join(cmds), end=' ')
58
 		sys.stdout.flush()
58
 		sys.stdout.flush()
59
 	p = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
59
 	p = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
60
 	processes=[p]
60
 	processes=[p]
61
 	for x in cmds[1:]:
61
 	for x in cmds[1:]:
62
 		p = subprocess.Popen(x, stdin = p.stdout, stdout = subprocess.PIPE, shell = True)
62
 		p = subprocess.Popen(x, stdin = p.stdout, stdout = subprocess.PIPE, shell = True)
63
 		processes.append(p)
63
 		processes.append(p)
64
-	output = p.communicate()[0]
64
+	output = p.communicate()[0].decode('utf-8')
65
 	for p in processes:
65
 	for p in processes:
66
 		p.wait()
66
 		p.wait()
67
 	end = time.time()
67
 	end = time.time()
68
 	if not quiet:
68
 	if not quiet:
69
 		if ON_LINUX and os.isatty(1):
69
 		if ON_LINUX and os.isatty(1):
70
-			print '\r',
71
-		print '[%.5f] >> %s' % (end - start, ' | '.join(cmds))
70
+			print('\r', end=' ')
71
+		print('[%.5f] >> %s' % (end - start, ' | '.join(cmds)))
72
 	exectime_external += (end - start)
72
 	exectime_external += (end - start)
73
 	return output.rstrip('\n')
73
 	return output.rstrip('\n')
74
 
74
 
86
 	return defaultrange
86
 	return defaultrange
87
 
87
 
88
 def getkeyssortedbyvalues(dict):
88
 def getkeyssortedbyvalues(dict):
89
-	return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
89
+	return [el[1] for el in sorted([(el[1], el[0]) for el in list(dict.items())])]
90
 
90
 
91
 # dict['author'] = { 'commits': 512 } - ...key(dict, 'commits')
91
 # dict['author'] = { 'commits': 512 } - ...key(dict, 'commits')
92
 def getkeyssortedbyvaluekey(d, key):
92
 def getkeyssortedbyvaluekey(d, key):
93
-	return map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys())))
93
+	return [el[1] for el in sorted([(d[el][key], el) for el in list(d.keys())])]
94
 
94
 
95
 def getstatsummarycounts(line):
95
 def getstatsummarycounts(line):
96
 	numbers = re.findall('\d+', line)
96
 	numbers = re.findall('\d+', line)
207
 	def loadCache(self, cachefile):
207
 	def loadCache(self, cachefile):
208
 		if not os.path.exists(cachefile):
208
 		if not os.path.exists(cachefile):
209
 			return
209
 			return
210
-		print 'Loading cache...'
210
+		print('Loading cache...')
211
 		f = open(cachefile, 'rb')
211
 		f = open(cachefile, 'rb')
212
 		try:
212
 		try:
213
 			self.cache = pickle.loads(zlib.decompress(f.read()))
213
 			self.cache = pickle.loads(zlib.decompress(f.read()))
269
 	##
269
 	##
270
 	# Save cacheable data
270
 	# Save cacheable data
271
 	def saveCache(self, cachefile):
271
 	def saveCache(self, cachefile):
272
-		print 'Saving cache...'
272
+		print('Saving cache...')
273
 		tempfile = cachefile + '.tmp'
273
 		tempfile = cachefile + '.tmp'
274
 		f = open(tempfile, 'wb')
274
 		f = open(tempfile, 'wb')
275
 		#pickle.dump(self.cache, f)
275
 		#pickle.dump(self.cache, f)
308
 				self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), 'commits': 0, 'authors': {} }
308
 				self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), 'commits': 0, 'authors': {} }
309
 
309
 
310
 		# collect info on tags, starting from latest
310
 		# collect info on tags, starting from latest
311
-		tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), self.tags.items()))))
311
+		tags_sorted_by_date_desc = [el[1] for el in reversed(sorted([(el[1]['date'], el[0]) for el in list(self.tags.items())]))]
312
 		prev = None
312
 		prev = None
313
 		for tag in reversed(tags_sorted_by_date_desc):
313
 		for tag in reversed(tags_sorted_by_date_desc):
314
 			cmd = 'git shortlog -s "%s"' % tag
314
 			cmd = 'git shortlog -s "%s"' % tag
444
 			time, rev = revline.split(' ')
444
 			time, rev = revline.split(' ')
445
 			#if cache empty then add time and rev to list of new rev's
445
 			#if cache empty then add time and rev to list of new rev's
446
 			#otherwise try to read needed info from cache
446
 			#otherwise try to read needed info from cache
447
-			if 'files_in_tree' not in self.cache.keys():
447
+			if 'files_in_tree' not in list(self.cache.keys()):
448
 				revs_to_read.append((time,rev))
448
 				revs_to_read.append((time,rev))
449
 				continue
449
 				continue
450
-			if rev in self.cache['files_in_tree'].keys():
450
+			if rev in list(self.cache['files_in_tree'].keys()):
451
 				lines.append('%d %d' % (int(time), self.cache['files_in_tree'][rev]))
451
 				lines.append('%d %d' % (int(time), self.cache['files_in_tree'][rev]))
452
 			else:
452
 			else:
453
 				revs_to_read.append((time,rev))
453
 				revs_to_read.append((time,rev))
474
 			try:
474
 			try:
475
 				self.files_by_stamp[int(stamp)] = int(files)
475
 				self.files_by_stamp[int(stamp)] = int(files)
476
 			except ValueError:
476
 			except ValueError:
477
-				print 'Warning: failed to parse line "%s"' % line
477
+				print('Warning: failed to parse line "%s"' % line)
478
 
478
 
479
 		# extensions and size of files
479
 		# extensions and size of files
480
 		lines = getpipeoutput(['git ls-tree -r -l -z %s' % getcommitrange('HEAD', end_only = True)]).split('\000')
480
 		lines = getpipeoutput(['git ls-tree -r -l -z %s' % getcommitrange('HEAD', end_only = True)]).split('\000')
505
 			self.extensions[ext]['files'] += 1
505
 			self.extensions[ext]['files'] += 1
506
 			#if cache empty then add ext and blob id to list of new blob's
506
 			#if cache empty then add ext and blob id to list of new blob's
507
 			#otherwise try to read needed info from cache
507
 			#otherwise try to read needed info from cache
508
-			if 'lines_in_blob' not in self.cache.keys():
508
+			if 'lines_in_blob' not in list(self.cache.keys()):
509
 				blobs_to_read.append((ext,blob_id))
509
 				blobs_to_read.append((ext,blob_id))
510
 				continue
510
 				continue
511
-			if blob_id in self.cache['lines_in_blob'].keys():
511
+			if blob_id in list(self.cache['lines_in_blob'].keys()):
512
 				self.extensions[ext]['lines'] += self.cache['lines_in_blob'][blob_id]
512
 				self.extensions[ext]['lines'] += self.cache['lines_in_blob'][blob_id]
513
 			else:
513
 			else:
514
 				blobs_to_read.append((ext,blob_id))
514
 				blobs_to_read.append((ext,blob_id))
563
 
563
 
564
 						files, inserted, deleted = 0, 0, 0
564
 						files, inserted, deleted = 0, 0, 0
565
 					except ValueError:
565
 					except ValueError:
566
-						print 'Warning: unexpected line "%s"' % line
566
+						print('Warning: unexpected line "%s"' % line)
567
 				else:
567
 				else:
568
-					print 'Warning: unexpected line "%s"' % line
568
+					print('Warning: unexpected line "%s"' % line)
569
 			else:
569
 			else:
570
 				numbers = getstatsummarycounts(line)
570
 				numbers = getstatsummarycounts(line)
571
 
571
 
572
 				if len(numbers) == 3:
572
 				if len(numbers) == 3:
573
-					(files, inserted, deleted) = map(lambda el : int(el), numbers)
573
+					(files, inserted, deleted) = [int(el) for el in numbers]
574
 					total_lines += inserted
574
 					total_lines += inserted
575
 					total_lines -= deleted
575
 					total_lines -= deleted
576
 					self.total_lines_added += inserted
576
 					self.total_lines_added += inserted
577
 					self.total_lines_removed += deleted
577
 					self.total_lines_removed += deleted
578
 
578
 
579
 				else:
579
 				else:
580
-					print 'Warning: failed to handle line "%s"' % line
580
+					print('Warning: failed to handle line "%s"' % line)
581
 					(files, inserted, deleted) = (0, 0, 0)
581
 					(files, inserted, deleted) = (0, 0, 0)
582
 				#self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
582
 				#self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
583
 		self.total_lines += total_lines
583
 		self.total_lines += total_lines
622
 						self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits']
622
 						self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits']
623
 						files, inserted, deleted = 0, 0, 0
623
 						files, inserted, deleted = 0, 0, 0
624
 					except ValueError:
624
 					except ValueError:
625
-						print 'Warning: unexpected line "%s"' % line
625
+						print('Warning: unexpected line "%s"' % line)
626
 				else:
626
 				else:
627
-					print 'Warning: unexpected line "%s"' % line
627
+					print('Warning: unexpected line "%s"' % line)
628
 			else:
628
 			else:
629
 				numbers = getstatsummarycounts(line);
629
 				numbers = getstatsummarycounts(line);
630
 
630
 
631
 				if len(numbers) == 3:
631
 				if len(numbers) == 3:
632
-					(files, inserted, deleted) = map(lambda el : int(el), numbers)
632
+					(files, inserted, deleted) = [int(el) for el in numbers]
633
 				else:
633
 				else:
634
-					print 'Warning: failed to handle line "%s"' % line
634
+					print('Warning: failed to handle line "%s"' % line)
635
 					(files, inserted, deleted) = (0, 0, 0)
635
 					(files, inserted, deleted) = (0, 0, 0)
636
 	
636
 	
637
 	def refine(self):
637
 	def refine(self):
642
 		for i, name in enumerate(self.authors_by_commits):
642
 		for i, name in enumerate(self.authors_by_commits):
643
 			self.authors[name]['place_by_commits'] = i + 1
643
 			self.authors[name]['place_by_commits'] = i + 1
644
 
644
 
645
-		for name in self.authors.keys():
645
+		for name in list(self.authors.keys()):
646
 			a = self.authors[name]
646
 			a = self.authors[name]
647
 			a['commits_frac'] = (100 * float(a['commits'])) / self.getTotalCommits()
647
 			a['commits_frac'] = (100 * float(a['commits'])) / self.getTotalCommits()
648
 			date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp'])
648
 			date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp'])
678
 		return self.domains[domain]
678
 		return self.domains[domain]
679
 
679
 
680
 	def getDomains(self):
680
 	def getDomains(self):
681
-		return self.domains.keys()
681
+		return list(self.domains.keys())
682
 	
682
 	
683
 	def getFirstCommitDate(self):
683
 	def getFirstCommitDate(self):
684
 		return datetime.datetime.fromtimestamp(self.first_commit_stamp)
684
 		return datetime.datetime.fromtimestamp(self.first_commit_stamp)
744
 					shutil.copyfile(src, path + '/' + file)
744
 					shutil.copyfile(src, path + '/' + file)
745
 					break
745
 					break
746
 			else:
746
 			else:
747
-				print 'Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs)
747
+				print('Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs))
748
 
748
 
749
 		f = open(path + "/index.html", 'w')
749
 		f = open(path + "/index.html", 'w')
750
 		format = '%Y-%m-%d %H:%M:%S'
750
 		format = '%Y-%m-%d %H:%M:%S'
942
 		f.write('<th>Timezone</th><th>Commits</th>')
942
 		f.write('<th>Timezone</th><th>Commits</th>')
943
 		f.write('</tr>')
943
 		f.write('</tr>')
944
 		max_commits_on_tz = max(data.commits_by_timezone.values())
944
 		max_commits_on_tz = max(data.commits_by_timezone.values())
945
-		for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)):
945
+		for i in sorted(list(data.commits_by_timezone.keys()), key = lambda n : int(n)):
946
 			commits = data.commits_by_timezone[i]
946
 			commits = data.commits_by_timezone[i]
947
 			r = 127 + int((float(commits) / max_commits_on_tz) * 128)
947
 			r = 127 + int((float(commits) / max_commits_on_tz) * 128)
948
 			f.write('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
948
 			f.write('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
1006
 			fgl.write('%d' % stamp)
1006
 			fgl.write('%d' % stamp)
1007
 			fgc.write('%d' % stamp)
1007
 			fgc.write('%d' % stamp)
1008
 			for author in self.authors_to_plot:
1008
 			for author in self.authors_to_plot:
1009
-				if author in data.changes_by_date_by_author[stamp].keys():
1009
+				if author in list(data.changes_by_date_by_author[stamp].keys()):
1010
 					lines_by_authors[author] = data.changes_by_date_by_author[stamp][author]['lines_added']
1010
 					lines_by_authors[author] = data.changes_by_date_by_author[stamp][author]['lines_added']
1011
 					commits_by_authors[author] = data.changes_by_date_by_author[stamp][author]['commits']
1011
 					commits_by_authors[author] = data.changes_by_date_by_author[stamp][author]['commits']
1012
 				fgl.write(' %d' % lines_by_authors[author])
1012
 				fgl.write(' %d' % lines_by_authors[author])
1153
 		f.write('<table class="tags">')
1153
 		f.write('<table class="tags">')
1154
 		f.write('<tr><th>Name</th><th>Date</th><th>Commits</th><th>Authors</th></tr>')
1154
 		f.write('<tr><th>Name</th><th>Date</th><th>Commits</th><th>Authors</th></tr>')
1155
 		# sort the tags by date desc
1155
 		# sort the tags by date desc
1156
-		tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
1156
+		tags_sorted_by_date_desc = [el[1] for el in reversed(sorted([(el[1]['date'], el[0]) for el in list(data.tags.items())]))]
1157
 		for tag in tags_sorted_by_date_desc:
1157
 		for tag in tags_sorted_by_date_desc:
1158
 			authorinfo = []
1158
 			authorinfo = []
1159
 			self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors'])
1159
 			self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors'])
1168
 		self.createGraphs(path)
1168
 		self.createGraphs(path)
1169
 	
1169
 	
1170
 	def createGraphs(self, path):
1170
 	def createGraphs(self, path):
1171
-		print 'Generating graphs...'
1171
+		print('Generating graphs...')
1172
 
1172
 
1173
 		# hour of day
1173
 		# hour of day
1174
 		f = open(path + '/hour_of_day.plot', 'w')
1174
 		f = open(path + '/hour_of_day.plot', 'w')
1370
 		for f in files:
1370
 		for f in files:
1371
 			out = getpipeoutput([gnuplot_cmd + ' "%s"' % f])
1371
 			out = getpipeoutput([gnuplot_cmd + ' "%s"' % f])
1372
 			if len(out) > 0:
1372
 			if len(out) > 0:
1373
-				print out
1373
+				print(out)
1374
 
1374
 
1375
 	def printHeader(self, f, title = ''):
1375
 	def printHeader(self, f, title = ''):
1376
 		f.write(
1376
 		f.write(
1401
 """)
1401
 """)
1402
 		
1402
 		
1403
 def usage():
1403
 def usage():
1404
-	print """
1404
+	print("""
1405
 Usage: gitstats [options] <gitpath..> <outputpath>
1405
 Usage: gitstats [options] <gitpath..> <outputpath>
1406
 
1406
 
1407
 Options:
1407
 Options:
1411
 %s
1411
 %s
1412
 
1412
 
1413
 Please see the manual page for more details.
1413
 Please see the manual page for more details.
1414
-""" % conf
1414
+""" % conf)
1415
 
1415
 
1416
 
1416
 
1417
 class GitStats:
1417
 class GitStats:
1442
 		except OSError:
1442
 		except OSError:
1443
 			pass
1443
 			pass
1444
 		if not os.path.isdir(outputpath):
1444
 		if not os.path.isdir(outputpath):
1445
-			print 'FATAL: Output path is not a directory or does not exist'
1445
+			print('FATAL: Output path is not a directory or does not exist')
1446
 			sys.exit(1)
1446
 			sys.exit(1)
1447
 
1447
 
1448
 		if not getgnuplotversion():
1448
 		if not getgnuplotversion():
1449
-			print 'gnuplot not found'
1449
+			print('gnuplot not found')
1450
 			sys.exit(1)
1450
 			sys.exit(1)
1451
 
1451
 
1452
-		print 'Output path: %s' % outputpath
1452
+		print('Output path: %s' % outputpath)
1453
 		cachefile = os.path.join(outputpath, 'gitstats.cache')
1453
 		cachefile = os.path.join(outputpath, 'gitstats.cache')
1454
 
1454
 
1455
 		data = GitDataCollector()
1455
 		data = GitDataCollector()
1456
 		data.loadCache(cachefile)
1456
 		data.loadCache(cachefile)
1457
 
1457
 
1458
 		for gitpath in args[0:-1]:
1458
 		for gitpath in args[0:-1]:
1459
-			print 'Git path: %s' % gitpath
1459
+			print('Git path: %s' % gitpath)
1460
 
1460
 
1461
 			prevdir = os.getcwd()
1461
 			prevdir = os.getcwd()
1462
 			os.chdir(gitpath)
1462
 			os.chdir(gitpath)
1463
 
1463
 
1464
-			print 'Collecting data...'
1464
+			print('Collecting data...')
1465
 			data.collect(gitpath)
1465
 			data.collect(gitpath)
1466
 
1466
 
1467
 			os.chdir(prevdir)
1467
 			os.chdir(prevdir)
1468
 
1468
 
1469
-		print 'Refining data...'
1469
+		print('Refining data...')
1470
 		data.saveCache(cachefile)
1470
 		data.saveCache(cachefile)
1471
 		data.refine()
1471
 		data.refine()
1472
 
1472
 
1473
 		os.chdir(rundir)
1473
 		os.chdir(rundir)
1474
 
1474
 
1475
-		print 'Generating report...'
1475
+		print('Generating report...')
1476
 		report = HTMLReportCreator()
1476
 		report = HTMLReportCreator()
1477
 		report.create(data, outputpath)
1477
 		report.create(data, outputpath)
1478
 
1478
 
1479
 		time_end = time.time()
1479
 		time_end = time.time()
1480
 		exectime_internal = time_end - time_start
1480
 		exectime_internal = time_end - time_start
1481
-		print 'Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal)
1481
+		print('Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal))
1482
 		if sys.stdin.isatty():
1482
 		if sys.stdin.isatty():
1483
-			print 'You may now run:'
1484
-			print
1485
-			print '   sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''")
1486
-			print
1483
+			print('You may now run:')
1484
+			print()
1485
+			print('   sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''"))
1486
+			print()
1487
 
1487
 
1488
 if __name__=='__main__':
1488
 if __name__=='__main__':
1489
 	g = GitStats()
1489
 	g = GitStats()