From 065640ef4b0c716193227148a411f7088b827c8f Mon Sep 17 00:00:00 2001 From: MFTECH Date: Wed, 28 Feb 2024 15:50:10 -0500 Subject: [PATCH 1/9] update to python3 --- gitstats | 73 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 37 insertions(+), 36 deletions(-) diff --git a/gitstats b/gitstats index c71b0e44..b57a9529 100755 --- a/gitstats +++ b/gitstats @@ -1,4 +1,4 @@ -#!/usr/bin/env python2 +#!/usr/bin/env python3 # Copyright (c) 2007-2014 Heikki Hokkanen & others (see doc/AUTHOR) # GPLv2 / GPLv3 import datetime @@ -54,21 +54,21 @@ 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)) sys.stdout.flush() p = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True) processes=[p] for x in cmds[1:]: p = subprocess.Popen(x, stdin = p.stdout, stdout = subprocess.PIPE, shell = True) processes.append(p) - output = p.communicate()[0] + output = (p.communicate()[0]).decode("utf-8") for p in processes: p.wait() end = time.time() if not quiet: if ON_LINUX and os.isatty(1): - print '\r', - print '[%.5f] >> %s' % (end - start, ' | '.join(cmds)) + print('\r') + print('[%.5f] >> %s' % (end - start, ' | '.join(cmds))) exectime_external += (end - start) return output.rstrip('\n') @@ -86,11 +86,11 @@ def getcommitrange(defaultrange = 'HEAD', end_only = False): return defaultrange def getkeyssortedbyvalues(dict): - return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items()))) + return list(map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), 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 list(map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys())))) def getstatsummarycounts(line): numbers = re.findall('\d+', line) @@ -207,7 +207,7 @@ class DataCollector: def loadCache(self, cachefile): if not os.path.exists(cachefile): return - print 'Loading cache...' + print( 'Loading cache...') f = open(cachefile, 'rb') try: self.cache = pickle.loads(zlib.decompress(f.read())) @@ -269,7 +269,7 @@ class DataCollector: ## # Save cacheable data def saveCache(self, cachefile): - print 'Saving cache...' + print( 'Saving cache...') tempfile = cachefile + '.tmp' f = open(tempfile, 'wb') #pickle.dump(self.cache, f) @@ -308,7 +308,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 = list(map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), self.tags.items()))))) prev = None for tag in reversed(tags_sorted_by_date_desc): cmd = 'git shortlog -s "%s"' % tag @@ -474,7 +474,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 and size of files lines = getpipeoutput(['git ls-tree -r -l -z %s' % getcommitrange('HEAD', end_only = True)]).split('\000') @@ -563,21 +563,21 @@ class GitDataCollector(DataCollector): files, inserted, deleted = 0, 0, 0 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 = getstatsummarycounts(line) if len(numbers) == 3: - (files, inserted, deleted) = map(lambda el : int(el), numbers) + (files, inserted, deleted) = list(map(lambda el : int(el), numbers)) 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 @@ -622,16 +622,16 @@ class GitDataCollector(DataCollector): self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits'] files, inserted, deleted = 0, 0, 0 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 = getstatsummarycounts(line); if len(numbers) == 3: - (files, inserted, deleted) = map(lambda el : int(el), numbers) + (files, inserted, deleted) = list(map(lambda el : int(el), numbers)) else: - print 'Warning: failed to handle line "%s"' % line + print( 'Warning: failed to handle line "%s"' % line) (files, inserted, deleted) = (0, 0, 0) def refine(self): @@ -744,7 +744,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' @@ -1153,7 +1153,7 @@ class HTMLReportCreator(ReportCreator): f.write('') f.write('') # 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 = list(map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))) for tag in tags_sorted_by_date_desc: authorinfo = [] self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors']) @@ -1168,7 +1168,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') @@ -1370,7 +1370,7 @@ plot """ 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( @@ -1401,7 +1401,7 @@ plot """ """) def usage(): - print """ + text = """ Usage: gitstats [options] Options: @@ -1412,6 +1412,7 @@ Default config values: Please see the manual page for more details. """ % conf + print(text) class GitStats: @@ -1442,48 +1443,48 @@ class GitStats: 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) if not getgnuplotversion(): - print 'gnuplot not found' + print( 'gnuplot not found') sys.exit(1) - print 'Output path: %s' % outputpath + print( 'Output path: %s' % outputpath) cachefile = os.path.join(outputpath, 'gitstats.cache') data = GitDataCollector() data.loadCache(cachefile) for gitpath in args[0:-1]: - print 'Git path: %s' % gitpath + print( 'Git path: %s' % gitpath) prevdir = os.getcwd() os.chdir(gitpath) - print 'Collecting data...' + print( 'Collecting data...') data.collect(gitpath) os.chdir(prevdir) - 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)) if sys.stdin.isatty(): - print 'You may now run:' - print - print ' sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''") - print + print( 'You may now run:') + print() + print( ' sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''")) + print() if __name__=='__main__': g = GitStats() From 011810bdefd32523c43c33c11336dc0fe0c90638 Mon Sep 17 00:00:00 2001 From: MFTECH Date: Wed, 28 Feb 2024 17:41:48 -0500 Subject: [PATCH 2/9] add code lines stats, json report --- .gitignore | 2 + README.md | 15 ++ gitstats | 527 +++++++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 470 insertions(+), 74 deletions(-) create mode 100644 .gitignore create mode 100644 README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..45382360 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..aaf92d21 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# Gitstats - meaningful data for your git repository. + +## How to run +1. Clone this repo and `cd` into it +2. Create a output directory for your output +3. `./gitstats [path_to_git_repo] [path_to_output_folder]` +4. Open the `index.html` within your `ouptut_folder` + +## Dependencies + +* Gnuplot + * MacOS: `brew install gnuplot` + * Ubuntu: `sudo apt-get install gnuplot` +* Git +* Python >= 3.0 \ No newline at end of file diff --git a/gitstats b/gitstats index b57a9529..e8502092 100755 --- a/gitstats +++ b/gitstats @@ -4,6 +4,7 @@ import datetime import getopt import glob +import json import os import pickle import platform @@ -13,6 +14,7 @@ import subprocess import sys import time import zlib +import multiprocessing if sys.version_info < (2, 6): print >> sys.stderr, "Python 2.6 or higher is required for gitstats" @@ -25,6 +27,7 @@ os.environ['LC_ALL'] = 'C' GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n' ON_LINUX = (platform.system() == 'Linux') WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') +JSONFILE = 'gitstats.json' exectime_internal = 0.0 exectime_external = 0.0 @@ -46,8 +49,11 @@ conf = { 'commit_end': 'HEAD', 'linear_linestats': 1, 'project_name': '', - 'processes': 8, - 'start_date': '' + 'processes': multiprocessing.cpu_count(), + 'start_date': '', + 'end_date': '', + 'excluded_authors': [], + 'excluded_prefixes': [] } def getpipeoutput(cmds, quiet = False): @@ -74,8 +80,15 @@ def getpipeoutput(cmds, quiet = False): def getlogrange(defaultrange = 'HEAD', end_only = True): commit_range = getcommitrange(defaultrange, end_only) + datesel = '' if len(conf['start_date']) > 0: - return '--since="%s" "%s"' % (conf['start_date'], commit_range) + datesel = '--since="%s" %s' % (conf['start_date'], datesel) + if len(conf['end_date']) > 0: + datesel = '--until="%s" %s' % (conf['end_date'], datesel) + + if (len(datesel) > 0): + commit_range = '%s "%s"' % (datesel, commit_range) + return commit_range def getcommitrange(defaultrange = 'HEAD', end_only = False): @@ -147,6 +160,14 @@ class DataCollector: self.activity_by_hour_of_week_busiest = 0 self.activity_by_year_week = {} # yy_wNN -> commits self.activity_by_year_week_peak = 0 + self.lineactivity_by_hour_of_day = {} # hour -> commits + self.lineactivity_by_day_of_week = {} # day -> commits + self.lineactivity_by_month_of_year = {} # month [1-12] -> commits + self.lineactivity_by_hour_of_week = {} # weekday -> hour -> commits + self.lineactivity_by_hour_of_day_busiest = 0 + self.lineactivity_by_hour_of_week_busiest = 0 + self.lineactivity_by_year_week = {} # yy_wNN -> commits + self.lineactivity_by_year_week_peak = 0 self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp, last_active_day, active_days, lines_added, lines_removed} @@ -207,7 +228,7 @@ class DataCollector: def loadCache(self, cachefile): if not os.path.exists(cachefile): return - print( 'Loading cache...') + print('Loading cache...') f = open(cachefile, 'rb') try: self.cache = pickle.loads(zlib.decompress(f.read())) @@ -232,6 +253,12 @@ class DataCollector: def getActivityByHourOfDay(self): return {} + + def getLineActivityByDayOfWeek(self): + return {} + + def getLineActivityByHourOfDay(self): + return {} # : get a dictionary of domains def getDomainInfo(self, domain): @@ -263,13 +290,16 @@ class DataCollector: def getTotalFiles(self): return -1 + def getTotalLines(self): + return -1 + def getTotalLOC(self): return -1 ## # Save cacheable data def saveCache(self, cachefile): - print( 'Saving cache...') + print('Saving cache...') tempfile = cachefile + '.tmp' f = open(tempfile, 'wb') #pickle.dump(self.cache, f) @@ -322,6 +352,8 @@ class GitDataCollector(DataCollector): parts = re.split('\s+', line, 2) commits = int(parts[1]) author = parts[2] + if author in conf["excluded_authors"]: + continue self.tags[tag]['commits'] += commits self.tags[tag]['authors'][author] = commits @@ -338,6 +370,8 @@ class GitDataCollector(DataCollector): timezone = parts[3] author, mail = parts[4].split('<', 1) author = author.rstrip() + if author in conf["excluded_authors"]: + continue mail = mail.rstrip('>') domain = '?' if mail.find('@') != -1: @@ -434,14 +468,18 @@ class GitDataCollector(DataCollector): self.commits_by_timezone[timezone] = self.commits_by_timezone.get(timezone, 0) + 1 # outputs " " for each revision - revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T" %s' % getlogrange('HEAD'), 'grep -v ^commit']).strip().split('\n') + revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T %%an" %s' % getlogrange('HEAD'), 'grep -v ^commit']).strip().split('\n') lines = [] revs_to_read = [] time_rev_count = [] #Look up rev in cache and take info from cache if found #If not append rev to list of rev to read from repo for revline in revlines: - time, rev = revline.split(' ') + _revline = revline.split(' ') + time, rev = _revline[:2] + author = ' '.join(_revline[2:]) + if author in conf["excluded_authors"]: + continue #if cache empty then add time and rev to list of new rev's #otherwise try to read needed info from cache if 'files_in_tree' not in self.cache.keys(): @@ -489,6 +527,14 @@ class GitDataCollector(DataCollector): blob_id = parts[2] size = int(parts[3]) fullpath = parts[4] + exclude = False + for path in conf["excluded_prefixes"]: + if fullpath.startswith(path): + exclude = True + break + + if exclude: + continue self.total_size += size self.total_files += 1 @@ -540,6 +586,7 @@ class GitDataCollector(DataCollector): lines.reverse() files = 0; inserted = 0; deleted = 0; total_lines = 0 author = None + last_line = "" for line in lines: if len(line) == 0: continue @@ -550,35 +597,72 @@ class GitDataCollector(DataCollector): if pos != -1: try: (stamp, author) = (int(line[:pos]), line[pos+1:]) - self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines } - - date = datetime.datetime.fromtimestamp(stamp) - yymm = date.strftime('%Y-%m') - self.lines_added_by_month[yymm] = self.lines_added_by_month.get(yymm, 0) + inserted - self.lines_removed_by_month[yymm] = self.lines_removed_by_month.get(yymm, 0) + deleted - - yy = date.year - self.lines_added_by_year[yy] = self.lines_added_by_year.get(yy,0) + inserted - self.lines_removed_by_year[yy] = self.lines_removed_by_year.get(yy, 0) + deleted - - files, inserted, deleted = 0, 0, 0 + if author not in conf["excluded_authors"]: + self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines } + + date = datetime.datetime.fromtimestamp(stamp) + yymm = date.strftime('%Y-%m') + self.lines_added_by_month[yymm] = self.lines_added_by_month.get(yymm, 0) + inserted + self.lines_removed_by_month[yymm] = self.lines_removed_by_month.get(yymm, 0) + deleted + + yy = date.year + self.lines_added_by_year[yy] = self.lines_added_by_year.get(yy,0) + inserted + self.lines_removed_by_year[yy] = self.lines_removed_by_year.get(yy, 0) + deleted + + # lineactivity + # hour + hour = date.hour + self.lineactivity_by_hour_of_day[hour] = self.lineactivity_by_hour_of_day.get(hour, 0) + inserted + deleted + # most active hour? + if self.lineactivity_by_hour_of_day[hour] > self.lineactivity_by_hour_of_day_busiest: + self.lineactivity_by_hour_of_day_busiest = self.lineactivity_by_hour_of_day[hour] + + # day of week + day = date.weekday() + self.lineactivity_by_day_of_week[day] = self.lineactivity_by_day_of_week.get(day, 0) + inserted + deleted + + # domain stats + #if domain not in self.domains: + #self.domains[domain] = {} + # lines + #self.domains[domain]['lines'] = self.domains[domain].get('lines', 0) + 1 + + # hour of week + if day not in self.lineactivity_by_hour_of_week: + self.lineactivity_by_hour_of_week[day] = {} + self.lineactivity_by_hour_of_week[day][hour] = self.lineactivity_by_hour_of_week[day].get(hour, 0) + inserted + deleted + # most active hour? + if self.lineactivity_by_hour_of_week[day][hour] > self.lineactivity_by_hour_of_week_busiest: + self.lineactivity_by_hour_of_week_busiest = self.lineactivity_by_hour_of_week[day][hour] + + # month of year + month = date.month + self.lineactivity_by_month_of_year[month] = self.lineactivity_by_month_of_year.get(month, 0) + inserted + deleted + + # yearly/weekly activity + yyw = date.strftime('%Y-%W') + self.lineactivity_by_year_week[yyw] = self.lineactivity_by_year_week.get(yyw, 0) + inserted + deleted + if self.lineactivity_by_year_week_peak < self.lineactivity_by_year_week[yyw]: + self.lineactivity_by_year_week_peak = self.lineactivity_by_year_week[yyw] + + files, inserted, deleted = 0, 0, 0 + + numbers = getstatsummarycounts(last_line) + if len(numbers) == 3: + (files, inserted, deleted) = map(lambda el : int(el), numbers) + 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) + (files, inserted, deleted) = (0, 0, 0) except ValueError: print('Warning: unexpected line "%s"' % line) else: print('Warning: unexpected line "%s"' % line) else: - numbers = getstatsummarycounts(line) - - if len(numbers) == 3: - (files, inserted, deleted) = list(map(lambda el : int(el), numbers)) - 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) - (files, inserted, deleted) = (0, 0, 0) + last_line = line #self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted } self.total_lines += total_lines @@ -606,32 +690,33 @@ class GitDataCollector(DataCollector): try: oldstamp = stamp (stamp, author) = (int(line[:pos]), line[pos+1:]) - if oldstamp > stamp: - # clock skew, keep old timestamp to avoid having ugly graph - stamp = oldstamp - if author not in self.authors: - self.authors[author] = { 'lines_added' : 0, 'lines_removed' : 0, 'commits' : 0} - self.authors[author]['commits'] = self.authors[author].get('commits', 0) + 1 - 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 - if stamp not in self.changes_by_date_by_author: - self.changes_by_date_by_author[stamp] = {} - if author not in self.changes_by_date_by_author[stamp]: - self.changes_by_date_by_author[stamp][author] = {} - self.changes_by_date_by_author[stamp][author]['lines_added'] = self.authors[author]['lines_added'] - self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits'] - files, inserted, deleted = 0, 0, 0 + if author not in conf["excluded_authors"]: + if oldstamp > stamp: + # clock skew, keep old timestamp to avoid having ugly graph + stamp = oldstamp + if author not in self.authors: + self.authors[author] = { 'lines_added' : 0, 'lines_removed' : 0, 'commits' : 0} + self.authors[author]['commits'] = self.authors[author].get('commits', 0) + 1 + 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 + if stamp not in self.changes_by_date_by_author: + self.changes_by_date_by_author[stamp] = {} + if author not in self.changes_by_date_by_author[stamp]: + self.changes_by_date_by_author[stamp][author] = {} + self.changes_by_date_by_author[stamp][author]['lines_added'] = self.authors[author]['lines_added'] + self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits'] + files, inserted, deleted = 0, 0, 0 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 = getstatsummarycounts(line); if len(numbers) == 3: (files, inserted, deleted) = list(map(lambda el : int(el), numbers)) else: - print( 'Warning: failed to handle line "%s"' % line) + print('Warning: failed to handle line "%s"' % line) (files, inserted, deleted) = (0, 0, 0) def refine(self): @@ -662,6 +747,12 @@ class GitDataCollector(DataCollector): def getActivityByHourOfDay(self): return self.activity_by_hour_of_day + + def getLineActivityByDayOfWeek(self): + return self.lineactivity_by_day_of_week + + def getLineActivityByHourOfDay(self): + return self.lineactivity_by_hour_of_day def getAuthorInfo(self, author): return self.authors[author] @@ -704,6 +795,9 @@ class GitDataCollector(DataCollector): def getTotalLOC(self): return self.total_lines + + def getTotalLines(self): + return self.total_lines_added + self.total_lines_removed def getTotalSize(self): return self.total_size @@ -728,6 +822,24 @@ def html_header(level, text): name = html_linkify(text) return '\n%s\n\n' % (level, name, name, text, level) + +class GitDataCollectorJSONEncoder(json.JSONEncoder): + def default(self, obj): + if isinstance(obj, set): + return list(obj) + if isinstance(obj, datetime.timedelta): + return str(obj) + if isinstance(obj, GitDataCollector): + return obj.__dict__ + # Let the base class default method raise the TypeError + return json.JSONEncoder.default(self, obj) + +class JSONReportCreator(ReportCreator): + def create(self, data, filename): + f = open(filename, 'w') + json.dump(data, f, indent=True, + cls=GitDataCollectorJSONEncoder) + f.close() class HTMLReportCreator(ReportCreator): def create(self, data, path): ReportCreator.create(self, data, path) @@ -744,7 +856,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' @@ -754,7 +866,7 @@ class HTMLReportCreator(ReportCreator): self.printNav(f) - f.write('
') + f.write('
') f.write('
Project name
%s
' % (data.projectname)) f.write('
Generated
%s (in %d seconds)
' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated())) f.write('
Generator
GitStats (version %s), %s, %s
' % (getversion(), getgitversion(), getgnuplotversion())) @@ -764,7 +876,7 @@ class HTMLReportCreator(ReportCreator): f.write('
Total Lines of Code
%s (%d added, %d removed)
' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed)) f.write('
Total Commits
%s (average %.1f commits per active day, %.1f per all days)
' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays())) f.write('
Authors
%s (average %.1f commits per author)
' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors())) - f.write('
') + f.write('
') f.write('\n') f.close() @@ -795,7 +907,7 @@ class HTMLReportCreator(ReportCreator): stampcur -= deltaweek # top row: commits & bar - f.write('
NameDateCommitsAuthors
') + f.write('
') for i in range(0, WEEKS): commits = 0 if weeks[i] in data.activity_by_year_week: @@ -811,12 +923,12 @@ class HTMLReportCreator(ReportCreator): f.write('') for i in range(0, WEEKS): f.write('' % (WEEKS - i)) - f.write('
%s
') + f.write('') # Hour of Day f.write(html_header(2, 'Hour of Day')) hour_of_day = data.getActivityByHourOfDay() - f.write('') + f.write('
Hour
') for i in range(0, 24): f.write('' % i) f.write('\n') @@ -895,7 +1007,7 @@ class HTMLReportCreator(ReportCreator): f.write('') f.write('') - f.write('
Hour%d
Commits
') + f.write('') # Month of Year f.write(html_header(2, 'Month of Year')) @@ -938,7 +1050,7 @@ class HTMLReportCreator(ReportCreator): # Commits by timezone f.write(html_header(2, 'Commits by Timezone')) - f.write('') + f.write('
') f.write('') f.write('') max_commits_on_tz = max(data.commits_by_timezone.values()) @@ -946,7 +1058,7 @@ class HTMLReportCreator(ReportCreator): commits = data.commits_by_timezone[i] r = 127 + int((float(commits) / max_commits_on_tz) * 128) f.write('' % (i, r, commits)) - f.write('
TimezoneCommits
%s%d
') + f.write('') f.write('') f.close() @@ -962,12 +1074,12 @@ class HTMLReportCreator(ReportCreator): # Authors :: List of authors f.write(html_header(2, 'List of Authors')) - f.write('') + f.write('
') f.write('') for author in data.getAuthors(conf['max_authors']): info = data.getAuthorInfo(author) f.write('' % (author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'], info['date_first'], info['date_last'], info['timedelta'], len(info['active_days']), info['place_by_commits'])) - f.write('
AuthorCommits (%)+ lines- linesFirst commitLast commitAgeActive days# by commits
%s%d (%.2f%%)%d%d%s%s%s%d%d
') + f.write('') allauthors = data.getAuthors() if len(allauthors) > conf['max_authors']: @@ -1134,6 +1246,173 @@ class HTMLReportCreator(ReportCreator): fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines'])) fg.close() + # Weekly activity + WEEKS = 32 + f.write(html_header(2, 'Weekly activity')) + f.write('

Last %d weeks

' % WEEKS) + + # generate weeks to show (previous N weeks from now) + now = datetime.datetime.now() + deltaweek = datetime.timedelta(7) + weeks = [] + stampcur = now + for i in range(0, WEEKS): + weeks.insert(0, stampcur.strftime('%Y-%W')) + stampcur -= deltaweek + + # top row: commits & bar + f.write('') + for i in range(0, WEEKS): + commits = 0 + if weeks[i] in data.lineactivity_by_year_week: + commits = data.lineactivity_by_year_week[weeks[i]] + + percentage = 0 + if weeks[i] in data.lineactivity_by_year_week: + percentage = float(data.lineactivity_by_year_week[weeks[i]]) / data.lineactivity_by_year_week_peak + height = max(1, int(200 * percentage)) + f.write('' % (commits, height)) + + # bottom row: year/week + f.write('') + for i in range(0, WEEKS): + f.write('' % (WEEKS - i)) + f.write('
%d
%s
') + + # Hour of Day + f.write(html_header(2, 'Hour of Day')) + hour_of_day = data.getLineActivityByHourOfDay() + f.write('') + for i in range(0, 24): + f.write('' % i) + f.write('\n') + fp = open(path + '/line_hour_of_day.dat', 'w') + for i in range(0, 24): + if i in hour_of_day: + r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128) + f.write('' % (r, hour_of_day[i])) + fp.write('%d %d\n' % (i, hour_of_day[i])) + else: + f.write('') + fp.write('%d 0\n' % i) + fp.close() + f.write('\n') + totallines = data.getTotalLines() + for i in range(0, 24): + if i in hour_of_day: + r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128) + f.write('' % (r, (100.0 * hour_of_day[i]) / totallines)) + else: + f.write('') + f.write('
Hour%d
Lines%d0
%%.2f0.00
') + f.write('Hour of Day') + fg = open(path + '/line_hour_of_day.dat', 'w') + for i in range(0, 24): + if i in hour_of_day: + fg.write('%d %d\n' % (i + 1, hour_of_day[i])) + else: + fg.write('%d 0\n' % (i + 1)) + fg.close() + + # Day of Week + f.write(html_header(2, 'Day of Week')) + day_of_week = data.getLineActivityByDayOfWeek() + f.write('
') + f.write('') + fp = open(path + '/line_day_of_week.dat', 'w') + for d in range(0, 7): + commits = 0 + if d in day_of_week: + commits = day_of_week[d] + fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits)) + f.write('') + f.write('' % (WEEKDAYS[d])) + if d in day_of_week: + f.write('' % (day_of_week[d], (100.0 * day_of_week[d]) / totallines)) + else: + f.write('') + f.write('') + f.write('
DayTotal (%)
%s%d (%.2f%%)0
') + f.write('Day of Week') + fp.close() + + # Hour of Week + f.write(html_header(2, 'Hour of Week')) + f.write('') + + f.write('') + for hour in range(0, 24): + f.write('' % (hour)) + f.write('') + + for weekday in range(0, 7): + f.write('' % (WEEKDAYS[weekday])) + for hour in range(0, 24): + try: + commits = data.lineactivity_by_hour_of_week[weekday][hour] + except KeyError: + commits = 0 + if commits != 0: + f.write('%d' % commits) + else: + f.write('') + f.write('') + + f.write('
Weekday%d
%s
') + + # Month of Year + f.write(html_header(2, 'Month of Year')) + f.write('
') + f.write('') + fp = open (path + '/line_month_of_year.dat', 'w') + for mm in range(1, 13): + commits = 0 + if mm in data.lineactivity_by_month_of_year: + commits = data.lineactivity_by_month_of_year[mm] + f.write('' % (mm, commits, (100.0 * commits) / data.getTotalLines())) + fp.write('%d %d\n' % (mm, commits)) + fp.close() + f.write('
MonthLines (%)
%d%d (%.2f %%)
') + f.write('Month of Year') + + # Lines by year/month + f.write(html_header(2, 'Lines by year/month')) + f.write('
') + for yymm in reversed(sorted(data.commits_by_month.keys())): + f.write('' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) + f.write('
MonthCommitsLines addedLines removed
%s%d%d%d
') + f.write('Commits by year/month') + fg = open(path + '/line_commits_by_year_month.dat', 'w') + for yymm in sorted(data.commits_by_month.keys()): + fg.write('%s %s\n' % (yymm, data.lines_added_by_month.get(yymm, 0) + data.lines_removed_by_month.get(yymm, 0))) + fg.close() + + # Lines by year + f.write(html_header(2, 'Lines by Year')) + f.write('
') + for yy in reversed(sorted(data.commits_by_year.keys())): + f.write('' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) + f.write('
YearCommits (% of all)Lines addedLines removed
%s%d (%.2f%%)%d%d
') + f.write('Commits by Year') + fg = open(path + '/line_commits_by_year.dat', 'w') + for yy in sorted(data.commits_by_year.keys()): + fg.write('%d %d\n' % (yy, data.lines_added_by_year.get(yy,0) + data.lines_removed_by_year.get(yy,0))) + fg.close() + + # Commits by timezone + f.write(html_header(2, 'Commits by Timezone')) + f.write('') + f.write('') + max_commits_on_tz = max(data.commits_by_timezone.values()) + for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)): + commits = data.commits_by_timezone[i] + r = 127 + int((float(commits) / max_commits_on_tz) * 128) + f.write('' % (i, r, commits)) + f.write('
TimezoneCommits
%s%d
') + f.write('') f.close() @@ -1168,7 +1447,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') @@ -1336,6 +1615,99 @@ plot """ f.close() + # hour of day + f = open(path + '/line_hour_of_day.plot', 'w') + f.write(GNUPLOT_COMMON) + f.write( +""" +set output 'line_hour_of_day.png' +unset key +set xrange [0.5:24.5] +set xtics 4 +set grid y +set ylabel "Lines" +plot 'line_hour_of_day.dat' using 1:2:(0.5) w boxes fs solid +""") + f.close() + + # day of week + f = open(path + '/line_day_of_week.plot', 'w') + f.write(GNUPLOT_COMMON) + f.write( +""" +set output 'line_day_of_week.png' +unset key +set xrange [0.5:7.5] +set xtics 1 +set grid y +set ylabel "Lines" +plot 'line_day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid +""") + f.close() + + # Domains +# f = open(path + '/domains.plot', 'w') +# f.write(GNUPLOT_COMMON) +# f.write( +#""" +#set output 'domains.png' +#unset key +#unset xtics +#set yrange [0:] +#set grid y +#set ylabel "Commits" +#plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1 +#""") +# f.close() + + # Month of Year + f = open(path + '/line_month_of_year.plot', 'w') + f.write(GNUPLOT_COMMON) + f.write( +""" +set output 'line_month_of_year.png' +unset key +set xrange [0.5:12.5] +set xtics 1 +set grid y +set ylabel "Lines" +plot 'line_month_of_year.dat' using 1:2:(0.5) w boxes fs solid +""") + f.close() + + # commits_by_year_month + f = open(path + '/line_commits_by_year_month.plot', 'w') + f.write(GNUPLOT_COMMON) + f.write( +""" +set output 'line_commits_by_year_month.png' +unset key +set xdata time +set timefmt "%Y-%m" +set format x "%Y-%m" +set xtics rotate +set bmargin 5 +set grid y +set ylabel "Lines" +plot 'line_commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid +""") + f.close() + + # commits_by_year + f = open(path + '/line_commits_by_year.plot', 'w') + f.write(GNUPLOT_COMMON) + f.write( +""" +set output 'line_commits_by_year.png' +unset key +set xtics 1 rotate +set grid y +set ylabel "Lines" +set yrange [0:] +plot 'line_commits_by_year.dat' using 1:2:(0.5) w boxes fs solid +""") + f.close() + # Commits per author f = open(path + '/commits_by_author.plot', 'w') f.write(GNUPLOT_COMMON) @@ -1370,7 +1742,7 @@ plot """ 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( @@ -1379,8 +1751,8 @@ plot """ GitStats - %s - - + + @@ -1425,6 +1797,8 @@ class GitStats: raise KeyError('no such key "%s" in config' % key) if isinstance(conf[key], int): conf[key] = int(value) + elif isinstance(conf[key], list): + conf[key].append(value) else: conf[key] = value elif o in ('-h', '--help'): @@ -1443,47 +1817,52 @@ class GitStats: 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) if not getgnuplotversion(): - print( 'gnuplot not found') + print('gnuplot not found') sys.exit(1) - print( 'Output path: %s' % outputpath) + print('Output path: %s' % outputpath) cachefile = os.path.join(outputpath, 'gitstats.cache') data = GitDataCollector() data.loadCache(cachefile) for gitpath in args[0:-1]: - print( 'Git path: %s' % gitpath) + print('Git path: %s' % gitpath) prevdir = os.getcwd() os.chdir(gitpath) - print( 'Collecting data...') + print('Collecting data...') data.collect(gitpath) os.chdir(prevdir) - print( 'Refining data...') + print('Refining data...') data.saveCache(cachefile) data.refine() os.chdir(rundir) - print( 'Generating report...') + print('Generating HTML report...') report = HTMLReportCreator() report.create(data, outputpath) + print('Generating JSON report...') + report = JSONReportCreator() + report.create(data, os.path.join(outputpath, JSONFILE)) + 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)) if sys.stdin.isatty(): - print( 'You may now run:') + print('You may now run:') print() - print( ' sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''")) + print(' sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''")) + print(' sensible-notepad \'%s\'' % os.path.join(outputpath, JSONFILE).replace("'", "'\\''")) print() if __name__=='__main__': From fc688b8cc3b154cc12a3f0f58dd7281ac5b6589b Mon Sep 17 00:00:00 2001 From: MFTECH Date: Wed, 28 Feb 2024 18:14:12 -0500 Subject: [PATCH 3/9] add changes_by_date_by_author --- gitstats | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/gitstats b/gitstats index e8502092..78256237 100755 --- a/gitstats +++ b/gitstats @@ -99,11 +99,11 @@ def getcommitrange(defaultrange = 'HEAD', end_only = False): return defaultrange def getkeyssortedbyvalues(dict): - return list(map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))) + return [el[1] for el in sorted([(el[1], el[0]) for el in list(dict.items())])] # dict['author'] = { 'commits': 512 } - ...key(dict, 'commits') def getkeyssortedbyvaluekey(d, key): - return list(map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys())))) + return [el[1] for el in sorted([(d[el][key], el) for el in list(d.keys())])] def getstatsummarycounts(line): numbers = re.findall('\d+', line) @@ -168,6 +168,7 @@ class DataCollector: self.lineactivity_by_hour_of_week_busiest = 0 self.lineactivity_by_year_week = {} # yy_wNN -> commits self.lineactivity_by_year_week_peak = 0 + self.changes_by_date_by_author = {} # stamp -> author -> lines_added self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp, last_active_day, active_days, lines_added, lines_removed} @@ -649,7 +650,7 @@ class GitDataCollector(DataCollector): numbers = getstatsummarycounts(last_line) if len(numbers) == 3: - (files, inserted, deleted) = map(lambda el : int(el), numbers) + (files, inserted, deleted) = [int(el) for el in numbers] total_lines += inserted total_lines -= deleted self.total_lines_added += inserted @@ -669,7 +670,6 @@ class GitDataCollector(DataCollector): # Per-author statistics # defined for stamp, author only if author commited at this timestamp. - self.changes_by_date_by_author = {} # stamp -> author -> lines_added # Similar to the above, but never use --first-parent # (we need to walk through every commit to know who @@ -714,7 +714,7 @@ class GitDataCollector(DataCollector): numbers = getstatsummarycounts(line); if len(numbers) == 3: - (files, inserted, deleted) = list(map(lambda el : int(el), numbers)) + (files, inserted, deleted) = [int(el) for el in numbers] else: print('Warning: failed to handle line "%s"' % line) (files, inserted, deleted) = (0, 0, 0) @@ -1432,7 +1432,7 @@ class HTMLReportCreator(ReportCreator): f.write('') f.write('') # sort the tags by date desc - tags_sorted_by_date_desc = list(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 list(data.tags.items())]))] for tag in tags_sorted_by_date_desc: authorinfo = [] self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors']) From ad81269df5e93722877ee129059dc1c61ff31525 Mon Sep 17 00:00:00 2001 From: MFTECH Date: Thu, 29 Feb 2024 02:06:37 -0500 Subject: [PATCH 4/9] add tailwindcss, total branch and tags --- gitstats | 503 ++++++++++++++++++++++++++++++--------------------- gitstats.css | 4 - sortable.js | 10 +- 3 files changed, 306 insertions(+), 211 deletions(-) diff --git a/gitstats b/gitstats index 78256237..708e9aa5 100755 --- a/gitstats +++ b/gitstats @@ -151,6 +151,8 @@ class DataCollector: def __init__(self): self.stamp_created = time.time() self.cache = {} + self.total_branches = 0 + self.total_tags = 0 self.total_authors = 0 self.activity_by_hour_of_day = {} # hour -> commits self.activity_by_day_of_week = {} # day -> commits @@ -318,6 +320,8 @@ class GitDataCollector(DataCollector): DataCollector.collect(self, dir) self.total_authors += int(getpipeoutput(['git shortlog -s %s' % getlogrange(), 'wc -l'])) + self.total_branches += int(getpipeoutput(['git branch -r', 'wc -l'])) + self.total_tags += int(getpipeoutput(['git tag', 'wc -l'])) #self.total_lines = int(getoutput('git-ls-files -z |xargs -0 cat |wc -l')) # tags @@ -862,40 +866,44 @@ class HTMLReportCreator(ReportCreator): format = '%Y-%m-%d %H:%M:%S' self.printHeader(f) - f.write('

GitStats - %s

' % data.projectname) - self.printNav(f) - f.write('
') - f.write('
Project name
%s
' % (data.projectname)) - f.write('
Generated
%s (in %d seconds)
' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated())) - f.write('
Generator
GitStats (version %s), %s, %s
' % (getversion(), getgitversion(), getgnuplotversion())) - f.write('
Report Period
%s to %s
' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format))) - f.write('
Age
%d days, %d active days (%3.2f%%)
' % (data.getCommitDeltaDays(), len(data.getActiveDays()), (100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()))) - f.write('
Total Files
%s
' % data.getTotalFiles()) - f.write('
Total Lines of Code
%s (%d added, %d removed)
' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed)) - f.write('
Total Commits
%s (average %.1f commits per active day, %.1f per all days)
' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays())) - f.write('
Authors
%s (average %.1f commits per author)
' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors())) - f.write('
') - - f.write('\n') + general_content = ['

GitStats - %s

' % data.projectname] + + general_content.append('
') + general_content.append('
Project name
%s (%s branches, %s tags)
' % (data.projectname, data.total_branches, data.total_tags)) + general_content.append('
Generated
%s (in %d seconds)
' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated())) + general_content.append('
Generator
GitStats (version %s), %s, %s
' % (getversion(), getgitversion(), getgnuplotversion())) + general_content.append('
Report Period
%s to %s
' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format))) + general_content.append('
Age
%d days, %d active days (%3.2f%%)
' % (data.getCommitDeltaDays(), len(data.getActiveDays()), (100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()))) + general_content.append('
Total Files
%s
' % data.getTotalFiles()) + general_content.append('
Total Lines of Code
%s (%d added, %d removed)
' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed)) + general_content.append('
Total Commits
%s (average %.1f commits per active day, %.1f per all days)
' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays())) + general_content.append('
Authors
%s (average %.1f commits per author)
' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors())) + general_content.append('
') + + self.printContent(f, general_content) + + f.write('\n') f.close() ### # Activity f = open(path + '/activity.html', 'w') self.printHeader(f) - f.write('

Activity

') + self.printNav(f) - #f.write('

Last 30 days

') + activity_content = [] + + #activity_content.append('

Last 30 days

') - #f.write('

Last 12 months

') + #activity_content.append('

Last 12 months

') # Weekly activity WEEKS = 32 - f.write(html_header(2, 'Weekly activity')) - f.write('

Last %d weeks

' % WEEKS) + activity_content.append(html_header(2, 'Weekly activity')) + activity_content.append('

Last %d weeks

' % WEEKS) # generate weeks to show (previous N weeks from now) now = datetime.datetime.now() @@ -907,7 +915,7 @@ class HTMLReportCreator(ReportCreator): stampcur -= deltaweek # top row: commits & bar - f.write('
NameDateCommitsAuthors
') + activity_content.append('
') for i in range(0, WEEKS): commits = 0 if weeks[i] in data.activity_by_year_week: @@ -917,41 +925,41 @@ class HTMLReportCreator(ReportCreator): if weeks[i] in data.activity_by_year_week: percentage = float(data.activity_by_year_week[weeks[i]]) / data.activity_by_year_week_peak height = max(1, int(200 * percentage)) - f.write('' % (commits, height)) + activity_content.append('' % (commits, height)) # bottom row: year/week - f.write('') + activity_content.append('') for i in range(0, WEEKS): - f.write('' % (WEEKS - i)) - f.write('
%d
%d
%s
') + activity_content.append('%s' % (WEEKS - i)) + activity_content.append('') # Hour of Day - f.write(html_header(2, 'Hour of Day')) + activity_content.append(html_header(2, 'Hour of Day')) hour_of_day = data.getActivityByHourOfDay() - f.write('
') + activity_content.append('
Hour
') for i in range(0, 24): - f.write('' % i) - f.write('\n') + activity_content.append('' % i) + activity_content.append('\n') fp = open(path + '/hour_of_day.dat', 'w') for i in range(0, 24): if i in hour_of_day: r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) - f.write('' % (r, hour_of_day[i])) + activity_content.append('' % (r, hour_of_day[i])) fp.write('%d %d\n' % (i, hour_of_day[i])) else: - f.write('') + activity_content.append('') fp.write('%d 0\n' % i) fp.close() - f.write('\n') + activity_content.append('\n') totalcommits = data.getTotalCommits() for i in range(0, 24): if i in hour_of_day: r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) - f.write('' % (r, (100.0 * hour_of_day[i]) / totalcommits)) + activity_content.append('' % (r, (100.0 * hour_of_day[i]) / totalcommits)) else: - f.write('') - f.write('
Hour%d
Commits%d
Commits%d%d00
%
%%.2f%.2f0.00
') - f.write('Hour of Day') + activity_content.append('0.00') + activity_content.append('') + activity_content.append('Hour of Day') fg = open(path + '/hour_of_day.dat', 'w') for i in range(0, 24): if i in hour_of_day: @@ -961,106 +969,108 @@ class HTMLReportCreator(ReportCreator): fg.close() # Day of Week - f.write(html_header(2, 'Day of Week')) + activity_content.append(html_header(2, 'Day of Week')) day_of_week = data.getActivityByDayOfWeek() - f.write('
') - f.write('') + activity_content.append('
DayTotal (%)
') + activity_content.append('') fp = open(path + '/day_of_week.dat', 'w') for d in range(0, 7): commits = 0 if d in day_of_week: commits = day_of_week[d] fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits)) - f.write('') - f.write('' % (WEEKDAYS[d])) + activity_content.append('') + activity_content.append('' % (WEEKDAYS[d])) if d in day_of_week: - f.write('' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits)) + activity_content.append('' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits)) else: - f.write('') - f.write('') - f.write('
DayTotal (%)
%s
%s%d (%.2f%%)%d (%.2f%%)0
') - f.write('Day of Week') + activity_content.append('0') + activity_content.append('') + activity_content.append('
') + activity_content.append('Day of Week') fp.close() # Hour of Week - f.write(html_header(2, 'Hour of Week')) - f.write('') + activity_content.append(html_header(2, 'Hour of Week')) + activity_content.append('
') - f.write('') + activity_content.append('') for hour in range(0, 24): - f.write('' % (hour)) - f.write('') + activity_content.append('' % (hour)) + activity_content.append('') for weekday in range(0, 7): - f.write('' % (WEEKDAYS[weekday])) + activity_content.append('' % (WEEKDAYS[weekday])) for hour in range(0, 24): try: commits = data.activity_by_hour_of_week[weekday][hour] except KeyError: commits = 0 if commits != 0: - f.write('%d' % commits) + activity_content.append(' style="background-color: rgb(%d, 0, 0)"' % r) + activity_content.append('>%d' % commits) else: - f.write('') - f.write('') + activity_content.append('') + activity_content.append('') - f.write('
Weekday
Weekday%d
%d
%s
%s
') + activity_content.append('') # Month of Year - f.write(html_header(2, 'Month of Year')) - f.write('
') - f.write('') + activity_content.append(html_header(2, 'Month of Year')) + activity_content.append('
MonthCommits (%)
') + activity_content.append('') fp = open (path + '/month_of_year.dat', 'w') for mm in range(1, 13): commits = 0 if mm in data.activity_by_month_of_year: commits = data.activity_by_month_of_year[mm] - f.write('' % (mm, commits, (100.0 * commits) / data.getTotalCommits())) + activity_content.append('' % (mm, commits, (100.0 * commits) / data.getTotalCommits())) fp.write('%d %d\n' % (mm, commits)) fp.close() - f.write('
MonthCommits (%)
%d%d (%.2f %%)
%d%d (%.2f %%)
') - f.write('Month of Year') + activity_content.append('') + activity_content.append('Month of Year') # Commits by year/month - f.write(html_header(2, 'Commits by year/month')) - f.write('
') + activity_content.append(html_header(2, 'Commits by year/month')) + activity_content.append('
MonthCommitsLines addedLines removed
') for yymm in reversed(sorted(data.commits_by_month.keys())): - f.write('' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) - f.write('
MonthCommitsLines addedLines removed
%s%d%d%d
') - f.write('Commits by year/month') + activity_content.append('%s%d%d%d' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) + activity_content.append('') + activity_content.append('Commits by year/month') fg = open(path + '/commits_by_year_month.dat', 'w') for yymm in sorted(data.commits_by_month.keys()): fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm])) fg.close() # Commits by year - f.write(html_header(2, 'Commits by Year')) - f.write('
') + activity_content.append(html_header(2, 'Commits by Year')) + activity_content.append('
YearCommits (% of all)Lines addedLines removed
') for yy in reversed(sorted(data.commits_by_year.keys())): - f.write('' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) - f.write('
YearCommits (% of all)Lines addedLines removed
%s%d (%.2f%%)%d%d
') - f.write('Commits by Year') + activity_content.append('%s%d (%.2f%%)%d%d' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) + activity_content.append('') + activity_content.append('Commits by Year') fg = open(path + '/commits_by_year.dat', 'w') for yy in sorted(data.commits_by_year.keys()): fg.write('%d %d\n' % (yy, data.commits_by_year[yy])) fg.close() # Commits by timezone - f.write(html_header(2, 'Commits by Timezone')) - f.write('
') - f.write('') - f.write('') + activity_content.append(html_header(2, 'Commits by Timezone')) + activity_content.append('
TimezoneCommits
') + activity_content.append('') + activity_content.append('') max_commits_on_tz = max(data.commits_by_timezone.values()) for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)): commits = data.commits_by_timezone[i] r = 127 + int((float(commits) / max_commits_on_tz) * 128) - f.write('' % (i, r, commits)) - f.write('
TimezoneCommits
%s%d
') + activity_content.append('%s%d' % (i, r, commits)) + activity_content.append('
') - f.write('') + self.printContent(f, activity_content, 'Activity') + + f.write('') f.close() ### @@ -1068,33 +1078,34 @@ class HTMLReportCreator(ReportCreator): f = open(path + '/authors.html', 'w') self.printHeader(f) - f.write('

Authors

') self.printNav(f) + authors_content = [] + # Authors :: List of authors - f.write(html_header(2, 'List of Authors')) + authors_content.append(html_header(2, 'List of Authors')) - f.write('
') - f.write('') + authors_content.append('
AuthorCommits (%)+ lines- linesFirst commitLast commitAgeActive days# by commits
') + authors_content.append('') for author in data.getAuthors(conf['max_authors']): info = data.getAuthorInfo(author) - f.write('' % (author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'], info['date_first'], info['date_last'], info['timedelta'], len(info['active_days']), info['place_by_commits'])) - f.write('
AuthorCommits (%)+ lines- linesFirst commitLast commitAgeActive days# by commits
%s%d (%.2f%%)%d%d%s%s%s%d%d
') + authors_content.append('%s%d (%.2f%%)%d%d%s%s%s%d%d' % (author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'], info['date_first'], info['date_last'], info['timedelta'], len(info['active_days']), info['place_by_commits'])) + authors_content.append('') allauthors = data.getAuthors() if len(allauthors) > conf['max_authors']: rest = allauthors[conf['max_authors']:] - f.write('

These didn\'t make it to the top: %s

' % ', '.join(rest)) + authors_content.append('

These didn\'t make it to the top: %s

' % ', '.join(rest)) - f.write(html_header(2, 'Cumulated Added Lines of Code per Author')) - f.write('Lines of code per Author') + authors_content.append(html_header(2, 'Cumulated Added Lines of Code per Author')) + authors_content.append('Lines of code per Author') if len(allauthors) > conf['max_authors']: - f.write('

Only top %d authors shown

' % conf['max_authors']) + authors_content.append('

Only top %d authors shown

' % conf['max_authors']) - f.write(html_header(2, 'Commits per Author')) - f.write('Commits per Author') + authors_content.append(html_header(2, 'Commits per Author')) + authors_content.append('Commits per Author') if len(allauthors) > conf['max_authors']: - f.write('

Only top %d authors shown

' % conf['max_authors']) + authors_content.append('

Only top %d authors shown

' % conf['max_authors']) fgl = open(path + '/lines_of_code_by_author.dat', 'w') fgc = open(path + '/commits_by_author.dat', 'w') @@ -1129,36 +1140,36 @@ class HTMLReportCreator(ReportCreator): fgc.close() # Authors :: Author of Month - f.write(html_header(2, 'Author of Month')) - f.write('') - f.write('' % conf['authors_top']) + authors_content.append(html_header(2, 'Author of Month')) + authors_content.append('
MonthAuthorCommits (%%)Next top %dNumber of authors
') + authors_content.append('' % conf['authors_top']) for yymm in reversed(sorted(data.author_of_month.keys())): authordict = data.author_of_month[yymm] authors = getkeyssortedbyvalues(authordict) authors.reverse() commits = data.author_of_month[yymm][authors[0]] next = ', '.join(authors[1:conf['authors_top']+1]) - f.write('' % (yymm, authors[0], commits, (100.0 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm], next, len(authors))) + authors_content.append('' % (yymm, authors[0], commits, (100.0 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm], next, len(authors))) - f.write('
MonthAuthorCommits (%%)Next top %dNumber of authors
%s%s%d (%.2f%% of %d)%s%d
%s%s%d (%.2f%% of %d)%s%d
') + authors_content.append('') - f.write(html_header(2, 'Author of Year')) - f.write('' % conf['authors_top']) + authors_content.append(html_header(2, 'Author of Year')) + authors_content.append('
YearAuthorCommits (%%)Next top %dNumber of authors
' % conf['authors_top']) for yy in reversed(sorted(data.author_of_year.keys())): authordict = data.author_of_year[yy] authors = getkeyssortedbyvalues(authordict) authors.reverse() commits = data.author_of_year[yy][authors[0]] next = ', '.join(authors[1:conf['authors_top']+1]) - f.write('' % (yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next, len(authors))) - f.write('
YearAuthorCommits (%%)Next top %dNumber of authors
%s%s%d (%.2f%% of %d)%s%d
') + authors_content.append('%s%s%d (%.2f%% of %d)%s%d' % (yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next, len(authors))) + authors_content.append('') # Domains - f.write(html_header(2, 'Commits by Domains')) + authors_content.append(html_header(2, 'Commits by Domains')) domains_by_commits = getkeyssortedbyvaluekey(data.domains, 'commits') domains_by_commits.reverse() # most first - f.write('
') - f.write('') + authors_content.append('
DomainsTotal (%)
') + authors_content.append('') fp = open(path + '/domains.dat', 'w') n = 0 for domain in domains_by_commits: @@ -1168,32 +1179,36 @@ class HTMLReportCreator(ReportCreator): n += 1 info = data.getDomainInfo(domain) fp.write('%s %d %d\n' % (domain, n , info['commits'])) - f.write('' % (domain, info['commits'], (100.0 * info['commits'] / totalcommits))) - f.write('
DomainsTotal (%)
%s%d (%.2f%%)
') - f.write('Commits by Domains') + authors_content.append('%s%d (%.2f%%)' % (domain, info['commits'], (100.0 * info['commits'] / totalcommits))) + authors_content.append('') + authors_content.append('Commits by Domains') fp.close() - f.write('') + self.printContent(f, authors_content, 'Authors') + f.write('') f.close() ### # Files f = open(path + '/files.html', 'w') self.printHeader(f) - f.write('

Files

') + self.printNav(f) - f.write('
\n') - f.write('
Total files
%d
' % data.getTotalFiles()) - f.write('
Total lines
%d
' % data.getTotalLOC()) + + files_content = [] + + files_content.append('
\n') + files_content.append('
Total files
%d
' % data.getTotalFiles()) + files_content.append('
Total lines
%d
' % data.getTotalLOC()) try: - f.write('
Average file size
%.2f bytes
' % (float(data.getTotalSize()) / data.getTotalFiles())) + files_content.append('
Average file size
%.2f bytes
' % (float(data.getTotalSize()) / data.getTotalFiles())) except ZeroDivisionError: pass - f.write('
\n') + files_content.append('
\n') # Files :: File count by date - f.write(html_header(2, 'File count by date')) + files_content.append(html_header(2, 'File count by date')) # use set to get rid of duplicate/unnecessary entries files_by_date = set() @@ -1207,13 +1222,13 @@ class HTMLReportCreator(ReportCreator): # fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp])) fg.close() - f.write('Files by Date') + files_content.append('Files by Date') - #f.write('

Average file size by date

') + #files_content.append('

Average file size by date

') # Files :: Extensions - f.write(html_header(2, 'Extensions')) - f.write('') + files_content.append(html_header(2, 'Extensions')) + files_content.append('
ExtensionFiles (%)Lines (%)Lines/file
') for ext in sorted(data.extensions.keys()): files = data.extensions[ext]['files'] lines = data.extensions[ext]['lines'] @@ -1221,25 +1236,28 @@ class HTMLReportCreator(ReportCreator): loc_percentage = (100.0 * lines) / data.getTotalLOC() except ZeroDivisionError: loc_percentage = 0 - f.write('' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files)) - f.write('
ExtensionFiles (%)Lines (%)Lines/file
%s%d (%.2f%%)%d (%.2f%%)%d
') + files_content.append('%s%d (%.2f%%)%d (%.2f%%)%d' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files)) + files_content.append('') - f.write('') + self.printContent(f, files_content, 'Files') + + f.write('') f.close() ### # Lines f = open(path + '/lines.html', 'w') self.printHeader(f) - f.write('

Lines

') + self.printNav(f) + lines_content=[] - f.write('
\n') - f.write('
Total lines
%d
' % data.getTotalLOC()) - f.write('
\n') + lines_content.append('
\n') + lines_content.append('
Total lines
%d
' % data.getTotalLOC()) + lines_content.append('
\n') - f.write(html_header(2, 'Lines of Code')) - f.write('Lines of Code') + lines_content.append(html_header(2, 'Lines of Code')) + lines_content.append('Lines of Code') fg = open(path + '/lines_of_code.dat', 'w') for stamp in sorted(data.changes_by_date.keys()): @@ -1248,8 +1266,8 @@ class HTMLReportCreator(ReportCreator): # Weekly activity WEEKS = 32 - f.write(html_header(2, 'Weekly activity')) - f.write('

Last %d weeks

' % WEEKS) + lines_content.append(html_header(2, 'Weekly activity')) + lines_content.append('

Last %d weeks

' % WEEKS) # generate weeks to show (previous N weeks from now) now = datetime.datetime.now() @@ -1261,7 +1279,7 @@ class HTMLReportCreator(ReportCreator): stampcur -= deltaweek # top row: commits & bar - f.write('') + lines_content.append('
') for i in range(0, WEEKS): commits = 0 if weeks[i] in data.lineactivity_by_year_week: @@ -1271,41 +1289,41 @@ class HTMLReportCreator(ReportCreator): if weeks[i] in data.lineactivity_by_year_week: percentage = float(data.lineactivity_by_year_week[weeks[i]]) / data.lineactivity_by_year_week_peak height = max(1, int(200 * percentage)) - f.write('' % (commits, height)) + lines_content.append('' % (commits, height)) # bottom row: year/week - f.write('') + lines_content.append('') for i in range(0, WEEKS): - f.write('' % (WEEKS - i)) - f.write('
%d
%d
%s
') + lines_content.append('%s' % (WEEKS - i)) + lines_content.append('') # Hour of Day - f.write(html_header(2, 'Hour of Day')) + lines_content.append(html_header(2, 'Hour of Day')) hour_of_day = data.getLineActivityByHourOfDay() - f.write('') + lines_content.append('
Hour
') for i in range(0, 24): - f.write('' % i) - f.write('\n') + lines_content.append('' % i) + lines_content.append('\n') fp = open(path + '/line_hour_of_day.dat', 'w') for i in range(0, 24): if i in hour_of_day: r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128) - f.write('' % (r, hour_of_day[i])) + lines_content.append('' % (r, hour_of_day[i])) fp.write('%d %d\n' % (i, hour_of_day[i])) else: - f.write('') + lines_content.append('') fp.write('%d 0\n' % i) fp.close() - f.write('\n') + lines_content.append('\n') totallines = data.getTotalLines() for i in range(0, 24): if i in hour_of_day: r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128) - f.write('' % (r, (100.0 * hour_of_day[i]) / totallines)) + lines_content.append('' % (r, (100.0 * hour_of_day[i]) / totallines)) else: - f.write('') - f.write('
Hour%d
Lines%d
Lines%d%d00
%
%%.2f%.2f0.00
') - f.write('Hour of Day') + lines_content.append('0.00') + lines_content.append('') + lines_content.append('Hour of Day') fg = open(path + '/line_hour_of_day.dat', 'w') for i in range(0, 24): if i in hour_of_day: @@ -1315,122 +1333,127 @@ class HTMLReportCreator(ReportCreator): fg.close() # Day of Week - f.write(html_header(2, 'Day of Week')) + lines_content.append(html_header(2, 'Day of Week')) day_of_week = data.getLineActivityByDayOfWeek() - f.write('
') - f.write('') + lines_content.append('
DayTotal (%)
') + lines_content.append('') fp = open(path + '/line_day_of_week.dat', 'w') for d in range(0, 7): commits = 0 if d in day_of_week: commits = day_of_week[d] fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits)) - f.write('') - f.write('' % (WEEKDAYS[d])) + lines_content.append('') + lines_content.append('' % (WEEKDAYS[d])) if d in day_of_week: - f.write('' % (day_of_week[d], (100.0 * day_of_week[d]) / totallines)) + lines_content.append('' % (day_of_week[d], (100.0 * day_of_week[d]) / totallines)) else: - f.write('') - f.write('') - f.write('
DayTotal (%)
%s
%s%d (%.2f%%)%d (%.2f%%)0
') - f.write('Day of Week') + lines_content.append('0') + lines_content.append('') + lines_content.append('') + lines_content.append('Day of Week') fp.close() # Hour of Week - f.write(html_header(2, 'Hour of Week')) - f.write('') + lines_content.append(html_header(2, 'Hour of Week')) + lines_content.append('
') - f.write('') + lines_content.append('') for hour in range(0, 24): - f.write('' % (hour)) - f.write('') + lines_content.append('' % (hour)) + lines_content.append('') for weekday in range(0, 7): - f.write('' % (WEEKDAYS[weekday])) + lines_content.append('' % (WEEKDAYS[weekday])) for hour in range(0, 24): try: commits = data.lineactivity_by_hour_of_week[weekday][hour] except KeyError: commits = 0 if commits != 0: - f.write('%d' % commits) + lines_content.append(' style="background-color: rgb(%d, 0, 0)"' % r) + lines_content.append('>%d' % commits) else: - f.write('') - f.write('') + lines_content.append('') + lines_content.append('') - f.write('
Weekday
Weekday%d
%d
%s
%s
') + lines_content.append('') # Month of Year - f.write(html_header(2, 'Month of Year')) - f.write('
') - f.write('') + lines_content.append(html_header(2, 'Month of Year')) + lines_content.append('
MonthLines (%)
') + lines_content.append('') fp = open (path + '/line_month_of_year.dat', 'w') for mm in range(1, 13): commits = 0 if mm in data.lineactivity_by_month_of_year: commits = data.lineactivity_by_month_of_year[mm] - f.write('' % (mm, commits, (100.0 * commits) / data.getTotalLines())) + lines_content.append('' % (mm, commits, (100.0 * commits) / data.getTotalLines())) fp.write('%d %d\n' % (mm, commits)) fp.close() - f.write('
MonthLines (%)
%d%d (%.2f %%)
%d%d (%.2f %%)
') - f.write('Month of Year') + lines_content.append('') + lines_content.append('Month of Year') # Lines by year/month - f.write(html_header(2, 'Lines by year/month')) - f.write('
') + lines_content.append(html_header(2, 'Lines by year/month')) + lines_content.append('
MonthCommitsLines addedLines removed
') for yymm in reversed(sorted(data.commits_by_month.keys())): - f.write('' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) - f.write('
MonthCommitsLines addedLines removed
%s%d%d%d
') - f.write('Commits by year/month') + lines_content.append('%s%d%d%d' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) + lines_content.append('') + lines_content.append('Commits by year/month') fg = open(path + '/line_commits_by_year_month.dat', 'w') for yymm in sorted(data.commits_by_month.keys()): fg.write('%s %s\n' % (yymm, data.lines_added_by_month.get(yymm, 0) + data.lines_removed_by_month.get(yymm, 0))) fg.close() # Lines by year - f.write(html_header(2, 'Lines by Year')) - f.write('
') + lines_content.append(html_header(2, 'Lines by Year')) + lines_content.append('
YearCommits (% of all)Lines addedLines removed
') for yy in reversed(sorted(data.commits_by_year.keys())): - f.write('' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) - f.write('
YearCommits (% of all)Lines addedLines removed
%s%d (%.2f%%)%d%d
') - f.write('Commits by Year') + lines_content.append('%s%d (%.2f%%)%d%d' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) + lines_content.append('') + lines_content.append('Commits by Year') fg = open(path + '/line_commits_by_year.dat', 'w') for yy in sorted(data.commits_by_year.keys()): fg.write('%d %d\n' % (yy, data.lines_added_by_year.get(yy,0) + data.lines_removed_by_year.get(yy,0))) fg.close() # Commits by timezone - f.write(html_header(2, 'Commits by Timezone')) - f.write('') - f.write('') + lines_content.append(html_header(2, 'Commits by Timezone')) + lines_content.append('
TimezoneCommits
') + lines_content.append('') max_commits_on_tz = max(data.commits_by_timezone.values()) for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)): commits = data.commits_by_timezone[i] r = 127 + int((float(commits) / max_commits_on_tz) * 128) - f.write('' % (i, r, commits)) - f.write('
TimezoneCommits
%s%d
') + lines_content.append('%s%d' % (i, r, commits)) + lines_content.append('') + + self.printContent(f, lines_content, 'Lines') - f.write('') + f.write('') f.close() ### # tags.html f = open(path + '/tags.html', 'w') self.printHeader(f) - f.write('

Tags

') + self.printNav(f) - f.write('
') - f.write('
Total tags
%d
' % len(data.tags)) + tags_content = [] + + + tags_content.append('
') + tags_content.append('
Total tags
%d
' % len(data.tags)) if len(data.tags) > 0: - f.write('
Average commits per tag
%.2f
' % (1.0 * data.getTotalCommits() / len(data.tags))) - f.write('
') + tags_content.append('
Average commits per tag
%.2f
' % (1.0 * data.getTotalCommits() / len(data.tags))) + tags_content.append('
') - f.write('') - f.write('') + tags_content.append('
NameDateCommitsAuthors
') + tags_content.append('') # sort the tags by date desc tags_sorted_by_date_desc = [el[1] for el in reversed(sorted([(el[1]['date'], el[0]) for el in list(data.tags.items())]))] for tag in tags_sorted_by_date_desc: @@ -1438,10 +1461,12 @@ class HTMLReportCreator(ReportCreator): self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors']) for i in reversed(self.authors_by_commits): authorinfo.append('%s (%d)' % (i, data.tags[tag]['authors'][i])) - f.write('' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo))) - f.write('
NameDateCommitsAuthors
%s%s%d%s
') + tags_content.append('%s%s%d%s' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo))) + tags_content.append('') - f.write('') + self.printContent(f, tags_content, 'Tags') + + f.write('') f.close() self.createGraphs(path) @@ -1754,11 +1779,85 @@ plot """ + +
""" % (self.title, conf['style'], getversion())) + + def printContent(self, f, content, title="Front's Stats"): + content = '\n'.join(content) + f.write(f''' +
+ +
+ +
+ + +
{content}
+
+ ''') + def printNav(self, f): + + menu = ''.join([f''' + + + {label} + +''' for (label, href) in [ + ("General", "index.html"), + ("Activity", "activity.html"), + ("Authors","authors.html"), + ("Files","files.html"), + ("Lines", "lines.html"), + ("Tags", "tags.html") + ]]) + + f.write(f''' + + +''') + + def printNav_old(self, f): f.write("""
') - self.printContent(f, general_content) - - f.write('\n') - f.close() + self.printPage(general_content, f'{path}/index.html', "FRONT'S STATS") ### + # activity.html # Activity - f = open(path + '/activity.html', 'w') - self.printHeader(f) - - self.printNav(f) - activity_content = [] #activity_content.append('

Last 30 days

') @@ -1068,18 +1059,12 @@ class HTMLReportCreator(ReportCreator): activity_content.append('%s%d' % (i, r, commits)) activity_content.append('') - self.printContent(f, activity_content, 'Activity') + self.printPage(activity_content, f'{path}/activity.html', 'Activity') - f.write('') - f.close() ### + # authors.html # Authors - f = open(path + '/authors.html', 'w') - self.printHeader(f) - - self.printNav(f) - authors_content = [] # Authors :: List of authors @@ -1184,17 +1169,11 @@ class HTMLReportCreator(ReportCreator): authors_content.append('Commits by Domains') fp.close() - self.printContent(f, authors_content, 'Authors') - f.write('') - f.close() + self.printPage(authors_content, f'{path}/authors.html', title='Authors') ### + # files.html # Files - f = open(path + '/files.html', 'w') - self.printHeader(f) - - self.printNav(f) - files_content = [] @@ -1239,17 +1218,11 @@ class HTMLReportCreator(ReportCreator): files_content.append('%s%d (%.2f%%)%d (%.2f%%)%d' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files)) files_content.append('') - self.printContent(f, files_content, 'Files') - - f.write('') - f.close() + self.printPage(files_content, f'{path}/files.html', title='Files') ### + # lines.html # Lines - f = open(path + '/lines.html', 'w') - self.printHeader(f) - - self.printNav(f) lines_content=[] lines_content.append('
\n') @@ -1431,20 +1404,11 @@ class HTMLReportCreator(ReportCreator): lines_content.append('%s%d' % (i, r, commits)) lines_content.append('') - self.printContent(f, lines_content, 'Lines') - - f.write('') - f.close() + self.printPage(lines_content, f'{path}/tags.html', title='Lines') ### # tags.html - f = open(path + '/tags.html', 'w') - self.printHeader(f) - - self.printNav(f) - tags_content = [] - tags_content.append('
') tags_content.append('
Total tags
%d
' % len(data.tags)) @@ -1464,10 +1428,7 @@ class HTMLReportCreator(ReportCreator): tags_content.append('%s%s%d%s' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo))) tags_content.append('') - self.printContent(f, tags_content, 'Tags') - - f.write('') - f.close() + self.printPage(tags_content, f'{path}/tags.html', title='Tags') self.createGraphs(path) @@ -1769,67 +1730,82 @@ plot """ if len(out) > 0: print(out) - def printHeader(self, f, title = ''): - f.write( -""" - + def printPage(self, content, path='/index.html', title=None): + f = open(path, 'w') + head = self.getHeader(title=title) + body = self.getBody(content, title=title) + html = self.getHTML(head, body) + + f.write(html) + f.close() + + + def getHTML(self, head, body): + return f'''{head}{body}''' + + + def getHeader(self, title = None): + title = title or f'GitStats - {self.title}' + return """ - GitStats - %s + + + %s - - -
-""" % (self.title, conf['style'], getversion())) - +""" % (title, conf['style'], getversion()) + - def printContent(self, f, content, title="Front's Stats"): + def getBody(self, content, title): + sidebar = self.getSideBar() + topBar = self.getTopBar(title=title) content = '\n'.join(content) - f.write(f''' -
- -
- -
+ return f''' + + + + + + + +
+ {sidebar} + +
+ {topBar}
{content}
-
- ''') - - def printNav(self, f): +
+ + +
+ + + + +''' + + def getSideBar(self): menu = ''.join([f''' - - - {label} - + + + {label} + ''' for (label, href) in [ ("General", "index.html"), ("Activity", "activity.html"), @@ -1839,7 +1815,7 @@ plot """ ("Tags", "tags.html") ]]) - f.write(f''' + return f''' -''') - - def printNav_old(self, f): - f.write(""" - -""") +''' + + def getTopBar(self, title): + return f''' + +
+ +
''' + def usage(): text = """ Usage: gitstats [options] @@ -1968,3 +1998,5 @@ if __name__=='__main__': g = GitStats() g.run(sys.argv[1:]) + +# https://github.com/TailAdmin/tailadmin-free-tailwind-dashboard-template/tree/main \ No newline at end of file diff --git a/gitstats.css b/gitstats.css index 741fd851..b957c4a7 100644 --- a/gitstats.css +++ b/gitstats.css @@ -65,7 +65,7 @@ td { } /* Navigation bar; tabbed style */ -.nav { +/* .nav { border-bottom: 1px solid black; padding: 0.3em; } @@ -93,9 +93,9 @@ td { .nav li a:hover { background-color: #ddd; border-bottom: 1px solid #ddf; -} +} */ -img { +main img { border: 1px solid black; padding: 0.5em; background-color: white; @@ -112,17 +112,17 @@ h1 a, h2 a { text-decoration: none; } -h1:hover a:after, -h2:hover a:after { +main h1:hover a:after, +main h2:hover a:after { content: '¶'; color: #555; } -h1 { +main h1 { font-size: x-large; } -h2 { +main h2 { background-color: #564; border: 1px solid black; padding-left: 0.5em; @@ -132,10 +132,10 @@ h2 { clear: both; } -h2 a { +main h2 a { color: white; } -.moreauthors { +main .moreauthors { font-size: 80%; } From 6588508ea83d761c0246bf27b72595d1bab156ce Mon Sep 17 00:00:00 2001 From: Christhoval Barba Date: Tue, 5 Mar 2024 17:36:16 -0500 Subject: [PATCH 6/9] feat(tailwind): add tailwind config, update general pages using tiles --- Makefile | 2 +- gitstats | 340 +++++++++++++++++++++++++++++++++++++++++++------- tailwind.json | 222 ++++++++++++++++++++++++++++++++ 3 files changed, 519 insertions(+), 45 deletions(-) create mode 100644 tailwind.json diff --git a/Makefile b/Makefile index 8476553b..a899fce7 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PREFIX=/usr/local BINDIR=$(PREFIX)/bin RESOURCEDIR=$(PREFIX)/share/gitstats -RESOURCES=gitstats.css sortable.js *.gif +RESOURCES=gitstats.css sortable.js *.gif tailwind.json BINARIES=gitstats VERSION=$(shell git describe 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || date +%Y-%m-%d) SEDVERSION=perl -pi -e 's/VERSION = 0/VERSION = "$(VERSION)"/' -- diff --git a/gitstats b/gitstats index dfa0cef0..facf9a98 100755 --- a/gitstats +++ b/gitstats @@ -853,7 +853,7 @@ class HTMLReportCreator(ReportCreator): binarypath = os.path.dirname(os.path.abspath(__file__)) secondarypath = os.path.join(binarypath, '..', 'share', 'gitstats') basedirs = [binarypath, secondarypath, '/usr/share/gitstats'] - for file in (conf['style'], 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif'): + for file in (conf['style'], 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif', 'tailwind.json'): for base in basedirs: src = base + '/' + file if os.path.exists(src): @@ -866,21 +866,47 @@ class HTMLReportCreator(ReportCreator): # General format = '%Y-%m-%d %H:%M:%S' - general_content = ['

GitStats - %s

' % data.projectname] + # general_content = ['

GitStats - %s

' % data.projectname] + + general_content = [] + + + tiles = '\n'.join([ + self.tilesItemStat(title='Project name', info=data.projectname), + self.tilesItemStat(title='Generated', info=datetime.datetime.now().strftime(format)), + self.tilesItemStat(title='Report Period', info=f'{data.getFirstCommitDate().strftime(format)} to {data.getLastCommitDate().strftime(format)}'), + ]) + + cards = '\n'.join([ + self.cardItemStat(title='Branches', count=data.total_branches), + self.cardItemStat(title='Tags', count=data.total_tags), + self.cardItemStat(title='Age', count=f'{data.getCommitDeltaDays():.1f} days'), + self.cardItemStat(title='Active days', count=f'{len(data.getActiveDays())}', stat=f'{(100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()):3.2f}%', arrow='up'), + self.cardItemStat(title='Total files', count=data.getTotalFiles()), + self.cardItemStat(title='Total LOC', count=data.getTotalLOC()), + self.cardItemStat(title='Total lines added', count=data.total_lines_added, stat=f'{((data.total_lines_added/data.getTotalLOC())*100):.2f}%', arrow='up'), + self.cardItemStat(title='Total lines removed', count=data.total_lines_removed, stat=f'{((data.total_lines_removed/data.getTotalLOC())*100):.2f}%', arrow='up'), + self.cardItemStat(title='Total commits', count=data.getTotalCommits(), stat=f'{(float(data.getTotalCommits()) / len(data.getActiveDays())):.1f}', arrow='up'), + self.cardItemStat(title='Authors', count=data.getTotalAuthors(), stat=f'{((1.0 * data.getTotalCommits()) / data.getTotalAuthors()):.1f}', arrow='up'), + ]) + + general_content.append(f'
{tiles}
') + + general_content.append(f'
{cards}
') - general_content.append('
') - general_content.append('
Project name
%s (%s branches, %s tags)
' % (data.projectname, data.total_branches, data.total_tags)) - general_content.append('
Generated
%s (in %d seconds)
' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated())) - general_content.append('
Generator
GitStats (version %s), %s, %s
' % (getversion(), getgitversion(), getgnuplotversion())) - general_content.append('
Report Period
%s to %s
' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format))) - general_content.append('
Age
%d days, %d active days (%3.2f%%)
' % (data.getCommitDeltaDays(), len(data.getActiveDays()), (100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()))) - general_content.append('
Total Files
%s
' % data.getTotalFiles()) - general_content.append('
Total Lines of Code
%s (%d added, %d removed)
' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed)) - general_content.append('
Total Commits
%s (average %.1f commits per active day, %.1f per all days)
' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays())) - general_content.append('
Authors
%s (average %.1f commits per author)
' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors())) - general_content.append('
') + # general_content.append('
') + # general_content.append('
Project name
%s (%s branches, %s tags)
' % (data.projectname, data.total_branches, data.total_tags)) + # general_content.append('
Generated
%s (in %d seconds)
' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated())) + # general_content.append('
Generator
GitStats (version %s), %s, %s
' % (getversion(), getgitversion(), getgnuplotversion())) + # general_content.append('
Report Period
%s to %s
' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format))) + # general_content.append('
Age
%d days, %d active days (%3.2f%%)
' % (data.getCommitDeltaDays(), len(data.getActiveDays()), (100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()))) + # general_content.append('
Total Files
%s
' % data.getTotalFiles()) + # general_content.append('
Total Lines of Code
%s (%d added, %d removed)
' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed)) + # general_content.append('
Total Commits
%s (average %.1f commits per active day, %.1f per all days)
' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays())) + # general_content.append('
Authors
%s (average %.1f commits per author)
' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors())) + # general_content.append('
') - self.printPage(general_content, f'{path}/index.html', "FRONT'S STATS") + self.printPage(general_content, f'{path}/index.html', f"{data.projectname}'S STATS") ### # activity.html @@ -1404,7 +1430,7 @@ class HTMLReportCreator(ReportCreator): lines_content.append('%s%d' % (i, r, commits)) lines_content.append('') - self.printPage(lines_content, f'{path}/tags.html', title='Lines') + self.printPage(lines_content, f'{path}/lines.html', title='Lines') ### # tags.html @@ -1746,7 +1772,8 @@ plot """ def getHeader(self, title = None): title = title or f'GitStats - {self.title}' - return """ + config = json.load(open('tailwind.json')) + return ''' @@ -1755,8 +1782,9 @@ plot """ + -""" % (title, conf['style'], getversion()) +''' % (title, conf['style'], getversion(), json.dumps(config)) def getBody(self, content, title): @@ -1775,37 +1803,131 @@ plot """ -
+
{sidebar}
{topBar} -
{content}
+
+
+ {content} +
+
- + + + + + + ''' + def tilesItemStat(self, title='', info='', icon=None, stat=None): + if icon is not None: + icon = f''' +
+ + + + + +
''' + + if stat is not None: + stat = ''' +
+

Home Loan Account

+
+
+ deposit +
+
+
''' + + return f''' +
+
+
+
+
+
+ {icon or ''} +
+
+

{info}

+

{title}

+
+
+
+
+
+ {stat or ''} +
+
+
+
+
''' + + def cardItemStat(self, count='$3.456K', title='Total views', stat = None, arrow= 'up', icon = None): + + stat_html = '' + if stat is not None: + stat_html = f''' + + {stat} + + {arrow is 'up' and ''} + {arrow is 'down' and ''} + +''' + + if icon is None: + icon = ''' + + + +''' + + return f''' +
+
+ {icon} +
+ +
+
+

{count}

+ {title} +
+ + {stat_html} +
+
''' def getSideBar(self): menu = ''.join([f''' - - - {label} - +
  • + + + {label} + +
  • ''' for (label, href) in [ ("General", "index.html"), ("Activity", "activity.html"), @@ -1817,18 +1939,49 @@ plot """ return f''' -
    ''' @@ -1915,7 +2166,6 @@ Please see the manual page for more details. """ % conf print(text) - class GitStats: def run(self, args_orig): optlist, args = getopt.getopt(args_orig, 'hc:', ["help"]) @@ -1999,4 +2249,6 @@ if __name__=='__main__': g.run(sys.argv[1:]) -# https://github.com/TailAdmin/tailadmin-free-tailwind-dashboard-template/tree/main \ No newline at end of file +# https://github.com/TailAdmin/tailadmin-free-tailwind-dashboard-template/tree/main +# https://codepen.io/Zsena/pen/ZEMaaoX?editors=1010 +# https://sebastiandedeyne.com/non-reactive-data-in-alpine-js/ \ No newline at end of file diff --git a/tailwind.json b/tailwind.json new file mode 100644 index 00000000..fa1af752 --- /dev/null +++ b/tailwind.json @@ -0,0 +1,222 @@ +{ + "darkMode": "class", + "theme": { + "extend": { + "colors": { + "current": "currentColor", + "transparent": "transparent", + "white": "#FFFFFF", + "black": "#1C2434", + "black-2": "#010101", + "body": "#64748B", + "bodydark": "#AEB7C0", + "bodydark1": "#DEE4EE", + "bodydark2": "#8A99AF", + "primary": "#3C50E0", + "secondary": "#80CAEE", + "stroke": "#E2E8F0", + "gray": "#EFF4FB", + "graydark": "#333A48", + "gray-2": "#F7F9FC", + "gray-3": "#FAFAFA", + "whiten": "#F1F5F9", + "whiter": "#F5F7FD", + "boxdark": "#24303F", + "boxdark-2": "#1A222C", + "strokedark": "#2E3A47", + "form-strokedark": "#3d4d60", + "form-input": "#1d2a39", + "meta-1": "#DC3545", + "meta-2": "#EFF2F7", + "meta-3": "#10B981", + "meta-4": "#313D4A", + "meta-5": "#259AE6", + "meta-6": "#FFBA00", + "meta-7": "#FF6766", + "meta-8": "#F0950C", + "meta-9": "#E5E7EB", + "success": "#219653", + "danger": "#D34053", + "warning": "#FFA70B" + }, + "fontSize": { + "title-xxl": ["44px", "55px"], + "title-xl": ["36px", "45px"], + "title-xl2": ["33px", "45px"], + "title-lg": ["28px", "35px"], + "title-md": ["24px", "30px"], + "title-md2": ["26px", "30px"], + "title-sm": ["20px", "26px"], + "title-xsm": ["18px", "24px"] + }, + "spacing": { + "4.5": "1.125rem", + "5.5": "1.375rem", + "6.5": "1.625rem", + "7.5": "1.875rem", + "8.5": "2.125rem", + "9.5": "2.375rem", + "10.5": "2.625rem", + "11": "2.75rem", + "11.5": "2.875rem", + "12.5": "3.125rem", + "13": "3.25rem", + "13.5": "3.375rem", + "14": "3.5rem", + "14.5": "3.625rem", + "15": "3.75rem", + "15.5": "3.875rem", + "16": "4rem", + "16.5": "4.125rem", + "17": "4.25rem", + "17.5": "4.375rem", + "18": "4.5rem", + "18.5": "4.625rem", + "19": "4.75rem", + "19.5": "4.875rem", + "21": "5.25rem", + "21.5": "5.375rem", + "22": "5.5rem", + "22.5": "5.625rem", + "24.5": "6.125rem", + "25": "6.25rem", + "25.5": "6.375rem", + "26": "6.5rem", + "27": "6.75rem", + "27.5": "6.875rem", + "29": "7.25rem", + "29.5": "7.375rem", + "30": "7.5rem", + "31": "7.75rem", + "32.5": "8.125rem", + "34": "8.5rem", + "34.5": "8.625rem", + "35": "8.75rem", + "36.5": "9.125rem", + "37.5": "9.375rem", + "39": "9.75rem", + "39.5": "9.875rem", + "40": "10rem", + "42.5": "10.625rem", + "44": "11rem", + "45": "11.25rem", + "46": "11.5rem", + "47.5": "11.875rem", + "49": "12.25rem", + "50": "12.5rem", + "52": "13rem", + "52.5": "13.125rem", + "54": "13.5rem", + "54.5": "13.625rem", + "55": "13.75rem", + "55.5": "13.875rem", + "59": "14.75rem", + "60": "15rem", + "62.5": "15.625rem", + "65": "16.25rem", + "67": "16.75rem", + "67.5": "16.875rem", + "70": "17.5rem", + "72.5": "18.125rem", + "73": "18.25rem", + "75": "18.75rem", + "90": "22.5rem", + "94": "23.5rem", + "95": "23.75rem", + "100": "25rem", + "115": "28.75rem", + "125": "31.25rem", + "132.5": "33.125rem", + "150": "37.5rem", + "171.5": "42.875rem", + "180": "45rem", + "187.5": "46.875rem", + "203": "50.75rem", + "230": "57.5rem", + "242.5": "60.625rem" + }, + "maxWidth": { + "2.5": "0.625rem", + "3": "0.75rem", + "4": "1rem", + "11": "2.75rem", + "13": "3.25rem", + "14": "3.5rem", + "15": "3.75rem", + "22.5": "5.625rem", + "25": "6.25rem", + "30": "7.5rem", + "34": "8.5rem", + "35": "8.75rem", + "40": "10rem", + "42.5": "10.625rem", + "44": "11rem", + "45": "11.25rem", + "60": "15rem", + "70": "17.5rem", + "90": "22.5rem", + "94": "23.5rem", + "125": "31.25rem", + "132.5": "33.125rem", + "142.5": "35.625rem", + "150": "37.5rem", + "180": "45rem", + "203": "50.75rem", + "230": "57.5rem", + "242.5": "60.625rem", + "270": "67.5rem", + "280": "70rem", + "292.5": "73.125rem" + }, + "maxHeight": { + "35": "8.75rem", + "70": "17.5rem", + "90": "22.5rem", + "550": "34.375rem", + "300": "18.75rem" + }, + "minWidth": { + "22.5": "5.625rem", + "42.5": "10.625rem", + "47.5": "11.875rem", + "75": "18.75rem" + }, + "zIndex": { + "999999": "999999", + "99999": "99999", + "9999": "9999", + "999": "999", + "99": "99", + "9": "9", + "1": "1" + }, + "opacity": { + "65": ".65" + }, + "transitionProperty": { "width": "width", "stroke": "stroke" }, + "borderWidth": { + "6": "6px" + }, + "boxShadow": { + "default": "0px 8px 13px -3px rgba(0, 0, 0, 0.07)", + "card": "0px 1px 3px rgba(0, 0, 0, 0.12)", + "card-2": "0px 1px 2px rgba(0, 0, 0, 0.05)", + "switcher": + "0px 2px 4px rgba(0, 0, 0, 0.2), inset 0px 2px 2px #FFFFFF, inset 0px -1px 1px rgba(0, 0, 0, 0.1)", + "switch-1": "0px 0px 5px rgba(0, 0, 0, 0.15)", + "1": "0px 1px 3px rgba(0, 0, 0, 0.08)", + "2": "0px 1px 4px rgba(0, 0, 0, 0.12)", + "3": "0px 1px 5px rgba(0, 0, 0, 0.14)", + "4": "0px 4px 10px rgba(0, 0, 0, 0.12)", + "5": "0px 1px 1px rgba(0, 0, 0, 0.15)", + "6": "0px 3px 15px rgba(0, 0, 0, 0.1)", + "7": "-5px 0 0 #313D4A, 5px 0 0 #313D4A", + "8": "1px 0 0 #313D4A, -1px 0 0 #313D4A, 0 1px 0 #313D4A, 0 -1px 0 #313D4A, 0 3px 13px rgb(0 0 0 / 8%)" + }, + "dropShadow": { + "1": "0px 1px 0px #E2E8F0", + "2": "0px 1px 4px rgba(0, 0, 0, 0.12)" + } + } + } + } \ No newline at end of file From dddbdcc185527dd56eb8241126576cdccacc0a61 Mon Sep 17 00:00:00 2001 From: Christhoval Barba Date: Wed, 6 Mar 2024 01:39:47 -0500 Subject: [PATCH 7/9] feat(activity/html): update chart to use apexchart. --- Makefile | 2 +- chart.json | 94 +++++++ gitstats | 756 +++++++++++++++------------------------------------ gitstats.css | 1 + html.py | 441 ++++++++++++++++++++++++++++++ 5 files changed, 758 insertions(+), 536 deletions(-) create mode 100644 chart.json create mode 100644 html.py diff --git a/Makefile b/Makefile index a899fce7..6de5c49d 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ PREFIX=/usr/local BINDIR=$(PREFIX)/bin RESOURCEDIR=$(PREFIX)/share/gitstats -RESOURCES=gitstats.css sortable.js *.gif tailwind.json +RESOURCES=gitstats.css sortable.js *.gif tailwind.json html.py BINARIES=gitstats VERSION=$(shell git describe 2>/dev/null || git rev-parse --short HEAD 2>/dev/null || date +%Y-%m-%d) SEDVERSION=perl -pi -e 's/VERSION = 0/VERSION = "$(VERSION)"/' -- diff --git a/chart.json b/chart.json new file mode 100644 index 00000000..f5560bea --- /dev/null +++ b/chart.json @@ -0,0 +1,94 @@ +{ + "series": [ + { + "data": [] + } + ], + "colors": ["#3C50E0"], + "chart": { + "fontFamily": "Satoshi, sans-serif", + "type": "bar", + "height": 350, + "toolbar": { + "show": false + } + }, + "plotOptions": { + "bar": { + "horizontal": false, + "columnWidth": "55%", + "endingShape": "rounded", + "borderRadius": 2 + } + }, + "dataLabels": { + "enabled": false + }, + "stroke": { + "show": true, + "width": 4, + "colors": ["transparent"] + }, + "xaxis": { + "categories": [], + "floating": false, + "labels": { + "show": true, + "style": { + "fontFamily": "Inter, sans-serif", + "cssClass": "text-xs font-normal !fill-body dark:!fill-bodydark" + } + }, + "axisBorder": { + "show": false + }, + "axisTicks": { + "show": false + } + }, + "legend": { + "show": true, + "position": "top", + "horizontalAlign": "left", + "fontFamily": "Satoshi", + "markers": { + "radius": 99 + } + }, + "yaxis": { + "title": false, + "labels": { + "show": true, + "style": { + "fontFamily": "Inter, sans-serif", + "cssClass": "text-xs font-normal !fill-body dark:!fill-bodydark" + } + } + }, + "grid": { + "show": false, + "strokeDashArray": 4, + "padding": { + "left": 2, + "right": 2, + "top": -14 + }, + "yaxis": { + "lines": { + "show": false + } + } + }, + "fill": { + "opacity": 1 + }, + + "tooltip": { + "shared": true, + "intersect": false, + "x": { + "show": false + }, + "y": {} + } +} diff --git a/gitstats b/gitstats index facf9a98..8aa27cd8 100755 --- a/gitstats +++ b/gitstats @@ -22,6 +22,8 @@ if sys.version_info < (2, 6): from multiprocessing import Pool +import html + os.environ['LC_ALL'] = 'C' GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n' @@ -853,7 +855,7 @@ class HTMLReportCreator(ReportCreator): binarypath = os.path.dirname(os.path.abspath(__file__)) secondarypath = os.path.join(binarypath, '..', 'share', 'gitstats') basedirs = [binarypath, secondarypath, '/usr/share/gitstats'] - for file in (conf['style'], 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif', 'tailwind.json'): + for file in (conf['style'], 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif', 'tailwind.json', 'html.py'): for base in basedirs: src = base + '/' + file if os.path.exists(src): @@ -865,29 +867,29 @@ class HTMLReportCreator(ReportCreator): # index.html # General format = '%Y-%m-%d %H:%M:%S' - # general_content = ['

    GitStats - %s

    ' % data.projectname] general_content = [] + general_html = html.HTML(path=f'{path}/index.html', title=f"{data.projectname}'S STATS", version= getversion()) tiles = '\n'.join([ - self.tilesItemStat(title='Project name', info=data.projectname), - self.tilesItemStat(title='Generated', info=datetime.datetime.now().strftime(format)), - self.tilesItemStat(title='Report Period', info=f'{data.getFirstCommitDate().strftime(format)} to {data.getLastCommitDate().strftime(format)}'), + general_html.tilesItemStat(title='Project name', info=data.projectname), + general_html.tilesItemStat(title='Generated', info=datetime.datetime.now().strftime(format)), + general_html.tilesItemStat(title='Report Period', info=f'{data.getFirstCommitDate().strftime(format)} to {data.getLastCommitDate().strftime(format)}'), ]) cards = '\n'.join([ - self.cardItemStat(title='Branches', count=data.total_branches), - self.cardItemStat(title='Tags', count=data.total_tags), - self.cardItemStat(title='Age', count=f'{data.getCommitDeltaDays():.1f} days'), - self.cardItemStat(title='Active days', count=f'{len(data.getActiveDays())}', stat=f'{(100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()):3.2f}%', arrow='up'), - self.cardItemStat(title='Total files', count=data.getTotalFiles()), - self.cardItemStat(title='Total LOC', count=data.getTotalLOC()), - self.cardItemStat(title='Total lines added', count=data.total_lines_added, stat=f'{((data.total_lines_added/data.getTotalLOC())*100):.2f}%', arrow='up'), - self.cardItemStat(title='Total lines removed', count=data.total_lines_removed, stat=f'{((data.total_lines_removed/data.getTotalLOC())*100):.2f}%', arrow='up'), - self.cardItemStat(title='Total commits', count=data.getTotalCommits(), stat=f'{(float(data.getTotalCommits()) / len(data.getActiveDays())):.1f}', arrow='up'), - self.cardItemStat(title='Authors', count=data.getTotalAuthors(), stat=f'{((1.0 * data.getTotalCommits()) / data.getTotalAuthors()):.1f}', arrow='up'), + general_html.cardItemStat(title='Branches', count=data.total_branches), + general_html.cardItemStat(title='Tags', count=data.total_tags), + general_html.cardItemStat(title='Age', count=f'{data.getCommitDeltaDays():.1f} days'), + general_html.cardItemStat(title='Active days', count=f'{len(data.getActiveDays())}', stat=f'{(100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()):3.2f}%', arrow='up'), + general_html.cardItemStat(title='Total files', count=data.getTotalFiles()), + general_html.cardItemStat(title='Total LOC', count=data.getTotalLOC()), + general_html.cardItemStat(title='Total lines added', count=data.total_lines_added, stat=f'{((data.total_lines_added/data.getTotalLOC())*100):.2f}%', arrow='up'), + general_html.cardItemStat(title='Total lines removed', count=data.total_lines_removed, stat=f'{((data.total_lines_removed/data.getTotalLOC())*100):.2f}%', arrow='up'), + general_html.cardItemStat(title='Total commits', count=data.getTotalCommits(), stat=f'{(float(data.getTotalCommits()) / len(data.getActiveDays())):.1f}', arrow='up'), + general_html.cardItemStat(title='Authors', count=data.getTotalAuthors(), stat=f'{((1.0 * data.getTotalCommits()) / data.getTotalAuthors()):.1f}', arrow='up'), ]) general_content.append(f'
    {tiles}
    ') @@ -906,12 +908,20 @@ class HTMLReportCreator(ReportCreator): # general_content.append('
    Authors
    %s (average %.1f commits per author)
    ' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors())) # general_content.append('
    ') - self.printPage(general_content, f'{path}/index.html', f"{data.projectname}'S STATS") + general_html.create(general_content) + + + chart_default_config = json.load(open('chart.json')) ### # activity.html # Activity + totalcommits = data.getTotalCommits() + activity_content = [] + activity_html = html.HTML(path=f'{path}/activity.html', title='Activity', version= getversion()) + + activity_content.append('
    ') #activity_content.append('

    Last 30 days

    ') @@ -919,9 +929,6 @@ class HTMLReportCreator(ReportCreator): # Weekly activity WEEKS = 32 - activity_content.append(html_header(2, 'Weekly activity')) - activity_content.append('

    Last %d weeks

    ' % WEEKS) - # generate weeks to show (previous N weeks from now) now = datetime.datetime.now() deltaweek = datetime.timedelta(7) @@ -930,168 +937,240 @@ class HTMLReportCreator(ReportCreator): for i in range(0, WEEKS): weeks.insert(0, stampcur.strftime('%Y-%W')) stampcur -= deltaweek - - # top row: commits & bar - activity_content.append('
    ') + + activity_per_weekly_serie = [] for i in range(0, WEEKS): - commits = 0 - if weeks[i] in data.activity_by_year_week: - commits = data.activity_by_year_week[weeks[i]] + commits = data.activity_by_year_week[weeks[i]] if weeks[i] in data.activity_by_year_week else 0 + activity_per_weekly_serie.append({"x": f'{WEEKS-i}', "y": commits, "percentage": (100.0 * commits) / totalcommits}) + + activity_per_weekly_config = chart_default_config.copy() - percentage = 0 - if weeks[i] in data.activity_by_year_week: - percentage = float(data.activity_by_year_week[weeks[i]]) / data.activity_by_year_week_peak - height = max(1, int(200 * percentage)) - activity_content.append('' % (commits, height)) + activity_per_weekly_config.update({"series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": activity_per_weekly_serie}]}) - # bottom row: year/week - activity_content.append('') - for i in range(0, WEEKS): - activity_content.append('' % (WEEKS - i)) - activity_content.append('
    %d
    %s
    ') + # current_config.update({"series": [{ + # "name": "Organic", + # "color": "#1A56DB", + # "data": commits_per_days_values + # }]}) + # current_config['xaxis']['categories'] = commits_per_days_labels + + activity_content.append(activity_html.addChart(activity_per_weekly_config, name='chartWeeklyActivity', title=f'Weekly activity Last {WEEKS} weeks', className="")) + # Hour of Day - activity_content.append(html_header(2, 'Hour of Day')) hour_of_day = data.getActivityByHourOfDay() - activity_content.append('
    ') - for i in range(0, 24): - activity_content.append('' % i) - activity_content.append('\n') - fp = open(path + '/hour_of_day.dat', 'w') - for i in range(0, 24): - if i in hour_of_day: - r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) - activity_content.append('' % (r, hour_of_day[i])) - fp.write('%d %d\n' % (i, hour_of_day[i])) - else: - activity_content.append('') - fp.write('%d 0\n' % i) - fp.close() - activity_content.append('\n') - totalcommits = data.getTotalCommits() - for i in range(0, 24): - if i in hour_of_day: - r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) - activity_content.append('' % (r, (100.0 * hour_of_day[i]) / totalcommits)) - else: - activity_content.append('') - activity_content.append('
    Hour%d
    Commits%d0
    %%.2f0.00
    ') - activity_content.append('Hour of Day') - fg = open(path + '/hour_of_day.dat', 'w') + + activity_per_hours_day_serie = [] + for i in range(0, 24): - if i in hour_of_day: - fg.write('%d %d\n' % (i + 1, hour_of_day[i])) - else: - fg.write('%d 0\n' % (i + 1)) - fg.close() + commits = hour_of_day[i] if i in hour_of_day else 0 + activity_per_hours_day_serie.append({"x": f'{i}', "y": commits, "percentage": (100.0 * commits) / totalcommits}) + + activity_per_hours_day_config = chart_default_config.copy() + + activity_per_hours_day_config.update({"series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": activity_per_hours_day_serie}]}) + + activity_content.append(activity_html.addChart(activity_per_hours_day_config, name='chartHourOfDay', title='Hour of Day', className="")) + + + # activity_content.append('
    ') + # for i in range(0, 24): + # activity_content.append('' % i) + # activity_content.append('\n') + # fp = open(path + '/hour_of_day.dat', 'w') + # for i in range(0, 24): + # if i in hour_of_day: + # r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) + # activity_content.append('' % (r, hour_of_day[i])) + # fp.write('%d %d\n' % (i, hour_of_day[i])) + # else: + # activity_content.append('') + # fp.write('%d 0\n' % i) + # fp.close() + # activity_content.append('\n') + + # for i in range(0, 24): + # if i in hour_of_day: + # r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) + # activity_content.append('' % (r, (100.0 * hour_of_day[i]) / totalcommits)) + # else: + # activity_content.append('') + # activity_content.append('
    Hour%d
    Commits%d0
    %%.2f0.00
    ') + # Day of Week - activity_content.append(html_header(2, 'Day of Week')) + # activity_content.append(html_header(2, 'Day of Week')) day_of_week = data.getActivityByDayOfWeek() - activity_content.append('
    ') - activity_content.append('') - fp = open(path + '/day_of_week.dat', 'w') + # activity_content.append('
    DayTotal (%)
    ') + # activity_content.append('') + + activity_per_day_week_serie =[] + for d in range(0, 7): - commits = 0 - if d in day_of_week: - commits = day_of_week[d] - fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits)) - activity_content.append('') - activity_content.append('' % (WEEKDAYS[d])) - if d in day_of_week: - activity_content.append('' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits)) - else: - activity_content.append('') - activity_content.append('') - activity_content.append('
    DayTotal (%)
    %s%d (%.2f%%)0
    ') - activity_content.append('Day of Week') - fp.close() + commits = day_of_week[d] if d in day_of_week else 0 + activity_per_day_week_serie.append({"x": WEEKDAYS[d], "y": commits, "percentage": (100.0 * commits) / totalcommits}) + + # activity_content.append('') + # activity_content.append('%s' % (WEEKDAYS[d])) + # if d in day_of_week: + # activity_content.append('%d (%.2f%%)' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits)) + # else: + # activity_content.append('0') + # activity_content.append('') + # activity_content.append('
    ') + + activity_per_day_week_config = chart_default_config.copy() + + activity_per_day_week_config.update({"series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": activity_per_day_week_serie}]}) + + activity_content.append(activity_html.addChart(activity_per_day_week_config, name='chartDayofWeek', title='Day of Week', className="xl:col-span-4")) # Hour of Week - activity_content.append(html_header(2, 'Hour of Week')) - activity_content.append('') + activity_hour_of_week__content= ['
    '] - activity_content.append('') + activity_hour_of_week__content.append('') for hour in range(0, 24): - activity_content.append('' % (hour)) - activity_content.append('') + activity_hour_of_week__content.append('' % (hour)) + activity_hour_of_week__content.append('') for weekday in range(0, 7): - activity_content.append('' % (WEEKDAYS[weekday])) + activity_hour_of_week__content.append('' % (WEEKDAYS[weekday])) for hour in range(0, 24): try: commits = data.activity_by_hour_of_week[weekday][hour] except KeyError: commits = 0 if commits != 0: - activity_content.append('%d' % commits) + activity_hour_of_week__content.append(' style="background-color: rgb(%d, 0, 0)"' % r) + activity_hour_of_week__content.append('>%d' % commits) else: - activity_content.append('') - activity_content.append('') + activity_hour_of_week__content.append('') + activity_hour_of_week__content.append('') + + activity_hour_of_week__content.append('
    Weekday
    Weekday%d
    %d
    %s
    %s
    ') + + activity_content.append(activity_html.addCard(activity_hour_of_week__content, title='Hour of Week', className='xl:col-span-8')) - activity_content.append('
    ') # Month of Year - activity_content.append(html_header(2, 'Month of Year')) - activity_content.append('
    ') - activity_content.append('') - fp = open (path + '/month_of_year.dat', 'w') + activity_per_month_of_year_serie = [] + # activity_content.append(html_header(2, 'Month of Year')) + # activity_content.append('
    MonthCommits (%)
    ') + # activity_content.append('') + # fp = open (path + '/month_of_year.dat', 'w') for mm in range(1, 13): - commits = 0 - if mm in data.activity_by_month_of_year: - commits = data.activity_by_month_of_year[mm] - activity_content.append('' % (mm, commits, (100.0 * commits) / data.getTotalCommits())) - fp.write('%d %d\n' % (mm, commits)) - fp.close() - activity_content.append('
    MonthCommits (%)
    %d%d (%.2f %%)
    ') - activity_content.append('Month of Year') + commits = data.activity_by_month_of_year[mm] if mm in data.activity_by_month_of_year else 0 + # activity_content.append('%d%d (%.2f %%)' % (mm, commits, (100.0 * commits) / data.getTotalCommits())) + # fp.write('%d %d\n' % (mm, commits)) + activity_per_month_of_year_serie.append({"x": f'{mm}', "y": commits, "percentage": (100.0 * commits) /totalcommits}) + # fp.close() + # activity_content.append('
    ') + # activity_content.append('Month of Year') + + activity_per_month_of_year_config = chart_default_config.copy() + + activity_per_month_of_year_config.update({"series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": activity_per_month_of_year_serie}]}) + + activity_content.append(activity_html.addChart(activity_per_month_of_year_config, name='chartMonthOfYear', title='Month of Year', className="xl:col-span-5")) + # Commits by year/month - activity_content.append(html_header(2, 'Commits by year/month')) - activity_content.append('
    ') - for yymm in reversed(sorted(data.commits_by_month.keys())): - activity_content.append('' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) - activity_content.append('
    MonthCommitsLines addedLines removed
    %s%d%d%d
    ') - activity_content.append('Commits by year/month') - fg = open(path + '/commits_by_year_month.dat', 'w') + activity_per_year_month_serie = [] + # activity_content.append(html_header(2, 'Commits by year/month')) + # activity_content.append('
    ') + # for yymm in reversed(sorted(data.commits_by_month.keys())): + # activity_content.append('' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) + # activity_content.append('
    MonthCommitsLines addedLines removed
    %s%d%d%d
    ') + # activity_content.append('Commits by year/month') + # fg = open(path + '/commits_by_year_month.dat', 'w') for yymm in sorted(data.commits_by_month.keys()): - fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm])) - fg.close() + # fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm])) + activity_per_year_month_serie.append({"x": f'{yymm}', "y": data.commits_by_month.get(yymm,0), "lines_added": data.lines_added_by_month.get(yymm,0), "lines_removed": data.lines_removed_by_month.get(yymm,0), "percentage": (100.0 * data.commits_by_month.get(yymm,0)) /totalcommits}) + # fg.close() + + activity_per_year_month_config = chart_default_config.copy() + + activity_per_year_month_config.update({"series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": activity_per_year_month_serie}]}) + + activity_per_year_month_config["xaxis"]["labels"]["show"] = False + + activity_content.append(activity_html.addChart(activity_per_year_month_config, name='chartCommitsByYearMonth', title='Commits by year/month', className="xl:col-span-7")) + # Commits by year - activity_content.append(html_header(2, 'Commits by Year')) - activity_content.append('
    ') - for yy in reversed(sorted(data.commits_by_year.keys())): - activity_content.append('' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) - activity_content.append('
    YearCommits (% of all)Lines addedLines removed
    %s%d (%.2f%%)%d%d
    ') - activity_content.append('Commits by Year') - fg = open(path + '/commits_by_year.dat', 'w') + activity_by_year_serie = [] + # activity_content.append(html_header(2, 'Commits by Year')) + # activity_content.append('
    ') + # for yy in reversed(sorted(data.commits_by_year.keys())): + # activity_content.append('' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) + # activity_content.append('
    YearCommits (% of all)Lines addedLines removed
    %s%d (%.2f%%)%d%d
    ') + # activity_content.append('Commits by Year') + # fg = open(path + '/commits_by_year.dat', 'w') for yy in sorted(data.commits_by_year.keys()): - fg.write('%d %d\n' % (yy, data.commits_by_year[yy])) - fg.close() + # fg.write('%d %d\n' % (yy, data.commits_by_year[yy])) + activity_by_year_serie.append({"x": f'{yy}', "y": data.commits_by_year.get(yy,0), "lines_added": data.lines_added_by_year.get(yymm,0), "lines_removed": data.lines_removed_by_year.get(yymm,0), "percentage": (100.0 * data.commits_by_year.get(yy,0)) /totalcommits}) + # fg.close() + + activity_by_year_config = chart_default_config.copy() + + activity_by_year_config.update({"series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": activity_by_year_serie}]}) + + activity_by_year_config["xaxis"]["labels"]["show"] = True + + activity_content.append(activity_html.addChart(activity_by_year_config, name='chartCommitsByYear', title='Commits by Year', className="xl:col-span-6")) # Commits by timezone - activity_content.append(html_header(2, 'Commits by Timezone')) - activity_content.append('
    ') - activity_content.append('') - activity_content.append('') + activity_by_timezone_serie = [] + # activity_content.append(html_header(2, 'Commits by Timezone')) + # activity_content.append('
    TimezoneCommits
    ') + # activity_content.append('') + # activity_content.append('') max_commits_on_tz = max(data.commits_by_timezone.values()) for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)): - commits = data.commits_by_timezone[i] - r = 127 + int((float(commits) / max_commits_on_tz) * 128) - activity_content.append('' % (i, r, commits)) - activity_content.append('
    TimezoneCommits
    %s%d
    ') + commits = data.commits_by_timezone.get(i, 0) + activity_by_timezone_serie.append({"x": f'{i}', "y": commits, "percentage": (100.0 * commits) /totalcommits}) + # r = 127 + int((float(commits) / max_commits_on_tz) * 128) + # activity_content.append('%s%d' % (i, r, commits)) + # activity_content.append('
    ') - self.printPage(activity_content, f'{path}/activity.html', 'Activity') + activity_by_timezone_config = chart_default_config.copy() + activity_by_timezone_config.update({"series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": activity_by_timezone_serie}]}) + + activity_content.append(activity_html.addChart(activity_by_timezone_config, name='chartCommitsByTimezone', title='Commits by Timezone', className="xl:col-span-6")) + + activity_content.append('
    ') + + activity_html.create(activity_content) ### # authors.html # Authors authors_content = [] + authors_html = html.HTML(path=f'{path}/authors.html', title='Authors', version= getversion()) # Authors :: List of authors authors_content.append(html_header(2, 'List of Authors')) @@ -1195,13 +1274,14 @@ class HTMLReportCreator(ReportCreator): authors_content.append('Commits by Domains') fp.close() - self.printPage(authors_content, f'{path}/authors.html', title='Authors') + authors_html.create(authors_content) ### # files.html # Files files_content = [] + files_html = html.HTML(path=f'{path}/files.html', title='Files', version= getversion()) files_content.append('
    \n') files_content.append('
    Total files
    %d
    ' % data.getTotalFiles()) @@ -1244,12 +1324,13 @@ class HTMLReportCreator(ReportCreator): files_content.append('%s%d (%.2f%%)%d (%.2f%%)%d' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files)) files_content.append('') - self.printPage(files_content, f'{path}/files.html', title='Files') + files_html.create(files_content) ### # lines.html # Lines lines_content=[] + lines_html = html.HTML(path=f'{path}/lines.html', title='Lines', version= getversion()) lines_content.append('
    \n') lines_content.append('
    Total lines
    %d
    ' % data.getTotalLOC()) @@ -1430,11 +1511,12 @@ class HTMLReportCreator(ReportCreator): lines_content.append('%s%d' % (i, r, commits)) lines_content.append('') - self.printPage(lines_content, f'{path}/lines.html', title='Lines') + lines_html.create(lines_content) ### # tags.html tags_content = [] + tags_html = html.HTML(path=f'{path}/tags.html', title='Tags', version= getversion()) tags_content.append('
    ') tags_content.append('
    Total tags
    %d
    ' % len(data.tags)) @@ -1454,7 +1536,7 @@ class HTMLReportCreator(ReportCreator): tags_content.append('%s%s%d%s' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo))) tags_content.append('') - self.printPage(tags_content, f'{path}/tags.html', title='Tags') + tags_html.create(tags_content) self.createGraphs(path) @@ -1755,402 +1837,6 @@ plot """ out = getpipeoutput([gnuplot_cmd + ' "%s"' % f]) if len(out) > 0: print(out) - - def printPage(self, content, path='/index.html', title=None): - f = open(path, 'w') - head = self.getHeader(title=title) - body = self.getBody(content, title=title) - html = self.getHTML(head, body) - - f.write(html) - f.close() - - - def getHTML(self, head, body): - return f'''{head}{body}''' - - - def getHeader(self, title = None): - title = title or f'GitStats - {self.title}' - config = json.load(open('tailwind.json')) - return ''' - - - - - %s - - - - - -''' % (title, conf['style'], getversion(), json.dumps(config)) - - - def getBody(self, content, title): - sidebar = self.getSideBar() - topBar = self.getTopBar(title=title) - content = '\n'.join(content) - return f''' - - - - - - - -
    - {sidebar} - - -
    - {topBar} - -
    -
    - {content} -
    -
    -
    - - -
    - - - - - - - - - - -''' - def tilesItemStat(self, title='', info='', icon=None, stat=None): - if icon is not None: - icon = f''' -
    - - - - - -
    ''' - - if stat is not None: - stat = ''' -
    -

    Home Loan Account

    -
    -
    - deposit -
    -
    -
    ''' - - return f''' -
    -
    -
    -
    -
    -
    - {icon or ''} -
    -
    -

    {info}

    -

    {title}

    -
    -
    -
    -
    -
    - {stat or ''} -
    -
    -
    -
    -
    ''' - - def cardItemStat(self, count='$3.456K', title='Total views', stat = None, arrow= 'up', icon = None): - - stat_html = '' - if stat is not None: - stat_html = f''' - - {stat} - - {arrow is 'up' and ''} - {arrow is 'down' and ''} - -''' - - if icon is None: - icon = ''' - - - -''' - - return f''' -
    -
    - {icon} -
    - -
    -
    -

    {count}

    - {title} -
    - - {stat_html} -
    -
    ''' - - def getSideBar(self): - menu = ''.join([f''' -
  • - - - {label} - -
  • -''' for (label, href) in [ - ("General", "index.html"), - ("Activity", "activity.html"), - ("Authors","authors.html"), - ("Files","files.html"), - ("Lines", "lines.html"), - ("Tags", "tags.html") - ]]) - - return f''' - - -''' - - def getTopBar(self, title): - return f''' - -
    -
    -
    - - - - - Logo - -
    - - - -
    -
      -
    • - - - -
    • -
    - - - -
    - - -
    -
    ''' def usage(): text = """ diff --git a/gitstats.css b/gitstats.css index b957c4a7..31881fbe 100644 --- a/gitstats.css +++ b/gitstats.css @@ -139,3 +139,4 @@ main h2 a { main .moreauthors { font-size: 80%; } + diff --git a/html.py b/html.py new file mode 100644 index 00000000..7416e956 --- /dev/null +++ b/html.py @@ -0,0 +1,441 @@ +import json +import time + +from typing import List, Dict + + +class HTML(object): + def __init__(self, path='/index.html', title=None, styles='gitstats.css', version='v0.0.1') -> None: + self.path = path + self.title = title + self.styles = styles + self.version = version + + def create(self, content): + f = open(self.path, 'w') + head = self.getHeader(title=self.title) + body = self.getBody(content, title=self.title) + html = self.getHTML(head, body) + + f.write(html) + f.close() + + def getHTML(self, head: str, body: str) -> str: + return f'''{head}{body}''' + + def getHeader(self, title: str = None) -> str: + title = title or f'GitStats - {self.title}' + config = json.load(open('tailwind.json')) + return ''' + + + + + %s + + + + + +''' % (title, self.styles, self.version, json.dumps(config)) + + def getBody(self, content: List[str], title: str) -> str: + sidebar = self.getSideBar() + topBar = self.getTopBar(title=title) + content = '\n'.join(content) + return f''' + + + + + + + +
    + {sidebar} + + +
    + {topBar} + +
    +
    + {content} +
    +
    +
    + + +
    + + + + + + + + + + + + + +''' + + def tilesItemStat(self, title: str = '', info: str = '', icon: str = None, stat: str = None) -> str: + if icon is not None: + icon = f''' +
    + + + + + +
    ''' + + if stat is not None: + stat = ''' +
    +

    Home Loan Account

    +
    +
    + deposit +
    +
    +
    ''' + + return f''' +
    +
    +
    +
    +
    +
    + {icon or ''} +
    +
    +

    {info}

    +

    {title}

    +
    +
    +
    +
    +
    + {stat or ''} +
    +
    +
    +
    +
    ''' + + def cardItemStat(self, count: str = '$3.456K', title: str = 'Total views', stat: str = None, arrow: str = 'up', icon=None) -> str: + + stat_html = '' + if stat is not None: + stat_html = f''' + + {stat} + + {arrow == 'up' and ''} + {arrow == 'down' and ''} + +''' + + if icon is None: + icon = ''' + + + +''' + + return f''' +
    +
    + {icon} +
    + +
    +
    +

    {count}

    + {title} +
    + + {stat_html} +
    +
    ''' + + def getSideBar(self) -> str: + menu = ''.join([f''' +
  • + + + {label} + +
  • +''' for (label, href) in [ + ("General", "index.html"), + ("Activity", "activity.html"), + ("Authors", "authors.html"), + ("Files", "files.html"), + ("Lines", "lines.html"), + ("Tags", "tags.html") + ]]) + + return f''' + + +''' + + def getTopBar(self, title: str) -> str: + return f''' + +
    +
    +
    + + + + + Logo + +
    + + + +
    +
      +
    • + + + +
    • +
    + + + +
    + + +
    +
    ''' + + def addChart(self, config: Dict, name: str = None, title: str = 'Chart', className: str=None): + if name is None: + name = f'chart_{int(time.time())}' + return self.addCard([f'
    '], title=title, className=className, extra=f''' +''') + + def addCard(self, content, title: str = 'Chart', className: str=None, extra: str=None): + content = '\n'.join(content) + return f''' +
    +
    +

    + {title} +

    +
    +
    {content}
    + {extra or ""} +
    ''' From f6e1b8fe46eae5725053397ab3c45c2c49983372 Mon Sep 17 00:00:00 2001 From: Christhoval Barba Date: Wed, 6 Mar 2024 19:37:12 -0500 Subject: [PATCH 8/9] feat(authors.html): update chart to use apexchart. --- chart.json | 3 + gitstats | 422 +++++++++++++++++++++++++++------------------------ gitstats.css | 158 +++++++++++++------ 3 files changed, 336 insertions(+), 247 deletions(-) diff --git a/chart.json b/chart.json index f5560bea..7bda54ee 100644 --- a/chart.json +++ b/chart.json @@ -19,6 +19,9 @@ "columnWidth": "55%", "endingShape": "rounded", "borderRadius": 2 + }, + "heatmap": { + "enableShades": false } }, "dataLabels": { diff --git a/gitstats b/gitstats index 8aa27cd8..a1e4da8c 100755 --- a/gitstats +++ b/gitstats @@ -45,7 +45,7 @@ conf = { 'max_domains': 10, 'max_ext_length': 10, 'style': 'gitstats.css', - 'max_authors': 20, + 'max_authors': 7, 'authors_top': 5, 'commit_begin': '', 'commit_end': 'HEAD', @@ -923,9 +923,8 @@ class HTMLReportCreator(ReportCreator): activity_content.append('
    ') - #activity_content.append('

    Last 30 days

    ') - - #activity_content.append('

    Last 12 months

    ') + # Last 30 days + # Last 12 months # Weekly activity WEEKS = 32 @@ -938,24 +937,19 @@ class HTMLReportCreator(ReportCreator): weeks.insert(0, stampcur.strftime('%Y-%W')) stampcur -= deltaweek - activity_per_weekly_serie = [] + activity_per_weekly_series = [ + {"name": "Commits", "color": "#1A56DB", "data": []}, + {"name": "Percentage", "color": "#779EF1", "data": []}, + ] for i in range(0, WEEKS): commits = data.activity_by_year_week[weeks[i]] if weeks[i] in data.activity_by_year_week else 0 - activity_per_weekly_serie.append({"x": f'{WEEKS-i}', "y": commits, "percentage": (100.0 * commits) / totalcommits}) + activity_per_weekly_series[0]['data'].append({"x": f'{WEEKS-i}', "y": commits}) + activity_per_weekly_series[1]['data'].append({"x": f'{WEEKS-i}', "y": f'{((100.0 * commits) / totalcommits):.2f}'}) - activity_per_weekly_config = chart_default_config.copy() - - activity_per_weekly_config.update({"series": [{ - "name": "Commits", - "color": "#1A56DB", - "data": activity_per_weekly_serie}]}) - - # current_config.update({"series": [{ - # "name": "Organic", - # "color": "#1A56DB", - # "data": commits_per_days_values - # }]}) - # current_config['xaxis']['categories'] = commits_per_days_labels + activity_per_weekly_config = { + **chart_default_config, + "series": activity_per_weekly_series + } activity_content.append(activity_html.addChart(activity_per_weekly_config, name='chartWeeklyActivity', title=f'Weekly activity Last {WEEKS} weeks', className="")) @@ -963,202 +957,146 @@ class HTMLReportCreator(ReportCreator): # Hour of Day hour_of_day = data.getActivityByHourOfDay() - activity_per_hours_day_serie = [] + activity_per_hours_day_series = [ + {"name": "Commits", "color": "#1A56DB", "data": []}, + {"name": "Percentage", "color": "#779EF1", "data": []}, + ] for i in range(0, 24): commits = hour_of_day[i] if i in hour_of_day else 0 - activity_per_hours_day_serie.append({"x": f'{i}', "y": commits, "percentage": (100.0 * commits) / totalcommits}) - - activity_per_hours_day_config = chart_default_config.copy() + activity_per_hours_day_series[0]["data"].append({"x": f'{i}', "y": commits}) + activity_per_hours_day_series[1]["data"].append({"x": f'{i}', "y": f'{((100.0 * commits) / totalcommits):.2f}'}) - activity_per_hours_day_config.update({"series": [{ - "name": "Commits", - "color": "#1A56DB", - "data": activity_per_hours_day_serie}]}) + activity_per_hours_day_config = { + **chart_default_config, + "series": activity_per_hours_day_series + } activity_content.append(activity_html.addChart(activity_per_hours_day_config, name='chartHourOfDay', title='Hour of Day', className="")) - - - # activity_content.append('
    ') - # for i in range(0, 24): - # activity_content.append('' % i) - # activity_content.append('\n') - # fp = open(path + '/hour_of_day.dat', 'w') - # for i in range(0, 24): - # if i in hour_of_day: - # r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) - # activity_content.append('' % (r, hour_of_day[i])) - # fp.write('%d %d\n' % (i, hour_of_day[i])) - # else: - # activity_content.append('') - # fp.write('%d 0\n' % i) - # fp.close() - # activity_content.append('\n') - - # for i in range(0, 24): - # if i in hour_of_day: - # r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128) - # activity_content.append('' % (r, (100.0 * hour_of_day[i]) / totalcommits)) - # else: - # activity_content.append('') - # activity_content.append('
    Hour%d
    Commits%d0
    %%.2f0.00
    ') # Day of Week - # activity_content.append(html_header(2, 'Day of Week')) day_of_week = data.getActivityByDayOfWeek() - # activity_content.append('
    ') - # activity_content.append('') - activity_per_day_week_serie =[] + activity_per_day_week_series = [ + {"name": "Commits", "color": "#1A56DB", "data": []}, + {"name": "Percentage", "color": "#779EF1", "data": []}, + ] for d in range(0, 7): commits = day_of_week[d] if d in day_of_week else 0 - activity_per_day_week_serie.append({"x": WEEKDAYS[d], "y": commits, "percentage": (100.0 * commits) / totalcommits}) + activity_per_day_week_series[0]["data"].append({"x": WEEKDAYS[d], "y": commits}) + activity_per_day_week_series[1]["data"].append({"x": WEEKDAYS[d], "y": f'{((100.0 * commits) / totalcommits):.2f}'}) - # activity_content.append('') - # activity_content.append('' % (WEEKDAYS[d])) - # if d in day_of_week: - # activity_content.append('' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits)) - # else: - # activity_content.append('') - # activity_content.append('') - # activity_content.append('
    DayTotal (%)
    %s%d (%.2f%%)0
    ') - - activity_per_day_week_config = chart_default_config.copy() - - activity_per_day_week_config.update({"series": [{ - "name": "Commits", - "color": "#1A56DB", - "data": activity_per_day_week_serie}]}) + activity_per_day_week_config = { + **chart_default_config, + "series": activity_per_day_week_series + } activity_content.append(activity_html.addChart(activity_per_day_week_config, name='chartDayofWeek', title='Day of Week', className="xl:col-span-4")) # Hour of Week - activity_hour_of_week__content= [''] - - activity_hour_of_week__content.append('') - for hour in range(0, 24): - activity_hour_of_week__content.append('' % (hour)) - activity_hour_of_week__content.append('') + activity_hour_of_week_series = [] for weekday in range(0, 7): - activity_hour_of_week__content.append('' % (WEEKDAYS[weekday])) + activity_hour_of_week_series.append({"name": WEEKDAYS[weekday], "data": []}) for hour in range(0, 24): try: commits = data.activity_by_hour_of_week[weekday][hour] except KeyError: commits = 0 - if commits != 0: - activity_hour_of_week__content.append('%d' % commits) - else: - activity_hour_of_week__content.append('') - activity_hour_of_week__content.append('') - - activity_hour_of_week__content.append('
    Weekday%d
    %s
    ') + + activity_hour_of_week_series[weekday]["data"].append({"x": f'{hour}', "y": commits}) - activity_content.append(activity_html.addCard(activity_hour_of_week__content, title='Hour of Week', className='xl:col-span-8')) + activity_hour_of_week_series.reverse() + activity_hour_of_week_config = { + "series": activity_hour_of_week_series, + "chart": {**chart_default_config["chart"], "type": 'heatmap'}, + "dataLabels": chart_default_config["dataLabels"], + "colors": ["#3C50E0"], + "xaxis": chart_default_config["xaxis"], + "yaxis": chart_default_config["yaxis"], + } + activity_content.append(activity_html.addChart(activity_hour_of_week_config, name='chartHourOfWeek', title='Hour of Week', className="xl:col-span-8")) + # Month of Year - activity_per_month_of_year_serie = [] - # activity_content.append(html_header(2, 'Month of Year')) - # activity_content.append('
    ') - # activity_content.append('') - # fp = open (path + '/month_of_year.dat', 'w') + activity_per_month_of_year_series = [ + {"name": "Commits", "color": "#1A56DB", "data": []}, + {"name": "Percentage", "color": "#779EF1", "data": []}, + ] for mm in range(1, 13): commits = data.activity_by_month_of_year[mm] if mm in data.activity_by_month_of_year else 0 - # activity_content.append('' % (mm, commits, (100.0 * commits) / data.getTotalCommits())) - # fp.write('%d %d\n' % (mm, commits)) - activity_per_month_of_year_serie.append({"x": f'{mm}', "y": commits, "percentage": (100.0 * commits) /totalcommits}) - # fp.close() - # activity_content.append('
    MonthCommits (%)
    %d%d (%.2f %%)
    ') - # activity_content.append('Month of Year') - - activity_per_month_of_year_config = chart_default_config.copy() + activity_per_month_of_year_series[0]["data"].append({"x": f'{mm}', "y": commits, "percentage": (100.0 * commits) /totalcommits}) + activity_per_month_of_year_series[1]["data"].append({"x": f'{mm}', "y": f'{((100.0 * commits) /totalcommits):.2f}'}) - activity_per_month_of_year_config.update({"series": [{ - "name": "Commits", - "color": "#1A56DB", - "data": activity_per_month_of_year_serie}]}) + activity_per_month_of_year_config = { + **chart_default_config, + "series": activity_per_month_of_year_series + } activity_content.append(activity_html.addChart(activity_per_month_of_year_config, name='chartMonthOfYear', title='Month of Year', className="xl:col-span-5")) # Commits by year/month activity_per_year_month_serie = [] - # activity_content.append(html_header(2, 'Commits by year/month')) - # activity_content.append('
    ') - # for yymm in reversed(sorted(data.commits_by_month.keys())): - # activity_content.append('' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) - # activity_content.append('
    MonthCommitsLines addedLines removed
    %s%d%d%d
    ') - # activity_content.append('Commits by year/month') - # fg = open(path + '/commits_by_year_month.dat', 'w') for yymm in sorted(data.commits_by_month.keys()): - # fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm])) activity_per_year_month_serie.append({"x": f'{yymm}', "y": data.commits_by_month.get(yymm,0), "lines_added": data.lines_added_by_month.get(yymm,0), "lines_removed": data.lines_removed_by_month.get(yymm,0), "percentage": (100.0 * data.commits_by_month.get(yymm,0)) /totalcommits}) - # fg.close() - activity_per_year_month_config = chart_default_config.copy() + activity_per_year_month_config = dict(chart_default_config) - activity_per_year_month_config.update({"series": [{ + activity_per_year_month_config = { + **chart_default_config, + "series": [{ "name": "Commits", "color": "#1A56DB", - "data": activity_per_year_month_serie}]}) - - activity_per_year_month_config["xaxis"]["labels"]["show"] = False + "data": activity_per_year_month_serie}], + "xaxis": { + **chart_default_config["xaxis"], + "labels": { + **activity_per_year_month_config["xaxis"]["labels"] , + "show" : False + }}} activity_content.append(activity_html.addChart(activity_per_year_month_config, name='chartCommitsByYearMonth', title='Commits by year/month', className="xl:col-span-7")) # Commits by year - activity_by_year_serie = [] - # activity_content.append(html_header(2, 'Commits by Year')) - # activity_content.append('
    ') - # for yy in reversed(sorted(data.commits_by_year.keys())): - # activity_content.append('' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) - # activity_content.append('
    YearCommits (% of all)Lines addedLines removed
    %s%d (%.2f%%)%d%d
    ') - # activity_content.append('Commits by Year') - # fg = open(path + '/commits_by_year.dat', 'w') + activity_by_year_series = [ + {"name": "Commits", "color": "#1A56DB", "data": []}, + {"name": "Lines Added", "color": "#23961B","data": []}, + {"name": "Lines Removed", "color": "#DB1A1A","data": []}, + {"name": "Percentage", "color": "#779EF1","data": []}] + for yy in sorted(data.commits_by_year.keys()): - # fg.write('%d %d\n' % (yy, data.commits_by_year[yy])) - activity_by_year_serie.append({"x": f'{yy}', "y": data.commits_by_year.get(yy,0), "lines_added": data.lines_added_by_year.get(yymm,0), "lines_removed": data.lines_removed_by_year.get(yymm,0), "percentage": (100.0 * data.commits_by_year.get(yy,0)) /totalcommits}) - # fg.close() + activity_by_year_series[0]["data"].append({"x": f'{yy}', "y": data.commits_by_year.get(yy,0)}) + activity_by_year_series[1]["data"].append({"x": f'{yy}', "y": data.lines_added_by_year.get(yy,0)}) + activity_by_year_series[2]["data"].append({"x": f'{yy}', "y": data.lines_removed_by_year.get(yy,0)}) + activity_by_year_series[3]["data"].append({"x": f'{yy}', "y": f'{((100.0 * data.commits_by_year.get(yy,0)) /totalcommits):.2f}'}) - activity_by_year_config = chart_default_config.copy() - - activity_by_year_config.update({"series": [{ - "name": "Commits", - "color": "#1A56DB", - "data": activity_by_year_serie}]}) - - activity_by_year_config["xaxis"]["labels"]["show"] = True + activity_by_year_config = { + **chart_default_config, + "series": activity_by_year_series + } activity_content.append(activity_html.addChart(activity_by_year_config, name='chartCommitsByYear', title='Commits by Year', className="xl:col-span-6")) # Commits by timezone - activity_by_timezone_serie = [] - # activity_content.append(html_header(2, 'Commits by Timezone')) - # activity_content.append('
    ') - # activity_content.append('') - # activity_content.append('') + activity_by_timezone_series = [ + {"name": "Commits", "color": "#1A56DB", "data": []}, + {"name": "Percentage", "color": "#779EF1", "data": []}, + ] max_commits_on_tz = max(data.commits_by_timezone.values()) for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)): commits = data.commits_by_timezone.get(i, 0) - activity_by_timezone_serie.append({"x": f'{i}', "y": commits, "percentage": (100.0 * commits) /totalcommits}) - # r = 127 + int((float(commits) / max_commits_on_tz) * 128) - # activity_content.append('' % (i, r, commits)) - # activity_content.append('
    TimezoneCommits
    %s%d
    ') + activity_by_timezone_series[0]["data"].append({"x": f'{i}', "y": commits}) + activity_by_timezone_series[1]["data"].append({"x": f'{i}', "y": f'{((100.0 * commits) /totalcommits):.2f}'}) - activity_by_timezone_config = chart_default_config.copy() - - activity_by_timezone_config.update({"series": [{ - "name": "Commits", - "color": "#1A56DB", - "data": activity_by_timezone_serie}]}) + activity_by_timezone_config = { + **chart_default_config, + "series": activity_by_timezone_series + } activity_content.append(activity_html.addChart(activity_by_timezone_config, name='chartCommitsByTimezone', title='Commits by Timezone', className="xl:col-span-6")) @@ -1170,35 +1108,35 @@ class HTMLReportCreator(ReportCreator): # authors.html # Authors authors_content = [] + authors_content.append('
    ') + authors_html = html.HTML(path=f'{path}/authors.html', title='Authors', version= getversion()) # Authors :: List of authors - authors_content.append(html_header(2, 'List of Authors')) + list_authors_content = [] - authors_content.append('
    ') - authors_content.append('') + list_authors_content.append('
    AuthorCommits (%)+ lines- linesFirst commitLast commitAgeActive days# by commits
    ') + list_authors_content.append('') for author in data.getAuthors(conf['max_authors']): info = data.getAuthorInfo(author) - authors_content.append('' % (author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'], info['date_first'], info['date_last'], info['timedelta'], len(info['active_days']), info['place_by_commits'])) - authors_content.append('
    AuthorCommits (%)+ lines- linesFirst commitLast commitAgeActive days# by commits
    %s%d (%.2f%%)%d%d%s%s%s%d%d
    ') + list_authors_content.append('%s%d (%.2f%%)%d%d%s%s%s%d%d' % (author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'], info['date_first'], info['date_last'], info['timedelta'], len(info['active_days']), info['place_by_commits'])) + list_authors_content.append('
    ') allauthors = data.getAuthors() if len(allauthors) > conf['max_authors']: rest = allauthors[conf['max_authors']:] - authors_content.append('

    These didn\'t make it to the top: %s

    ' % ', '.join(rest)) - - authors_content.append(html_header(2, 'Cumulated Added Lines of Code per Author')) - authors_content.append('Lines of code per Author') - if len(allauthors) > conf['max_authors']: - authors_content.append('

    Only top %d authors shown

    ' % conf['max_authors']) + list_authors_content.append('

    These didn\'t make it to the top: %s

    ' % ', '.join(rest)) + + authors_content.append(authors_html.addCard(list_authors_content, title='List of Authors')) - authors_content.append(html_header(2, 'Commits per Author')) - authors_content.append('Commits per Author') - if len(allauthors) > conf['max_authors']: - authors_content.append('

    Only top %d authors shown

    ' % conf['max_authors']) + # Authors :: Commits + author_disclaimer = '' + max_authors = conf['max_authors'] + if len(allauthors) > max_authors: + author_disclaimer =f'Only top {max_authors} authors shown' - fgl = open(path + '/lines_of_code_by_author.dat', 'w') - fgc = open(path + '/commits_by_author.dat', 'w') + # fgl = open(path + '/lines_of_code_by_author.dat', 'w') + # fgc = open(path + '/commits_by_author.dat', 'w') lines_by_authors = {} # cumulated added lines by # author. to save memory, @@ -1211,56 +1149,133 @@ class HTMLReportCreator(ReportCreator): # time. Be robust and keep the list in a variable. commits_by_authors = {} # cumulated added lines by + colors = [ + "#4B0082", + "#2E8B57", + "#7B68EE", + "#BA55D3", + "#DB7093", + "#FFD700", + "#006400", + "#008080", + "#191970", + "#0000CD", + "#CD5C5C", + "#FAFAD2", + "#7FFF00", + "#9966CC", + "#D2B48C", + "#000080", + "#AFEEEE", + "#8B008B", + "#008000", + "#6A5ACD"] + + authors_cumulated_commits_series = {} + authors_commits_series = {} + self.authors_to_plot = data.getAuthors(conf['max_authors']) - for author in self.authors_to_plot: + for idx, author in enumerate(self.authors_to_plot): lines_by_authors[author] = 0 commits_by_authors[author] = 0 + authors_cumulated_commits_series[author]= {"name": author, "color": colors[idx], "data": []} + authors_commits_series[author]= {"name": author, "color": colors[idx], "data": []} + for stamp in sorted(data.changes_by_date_by_author.keys()): - fgl.write('%d' % stamp) - fgc.write('%d' % stamp) + # fgl.write('%d' % stamp) + # fgc.write('%d' % stamp) for author in self.authors_to_plot: if author in data.changes_by_date_by_author[stamp].keys(): lines_by_authors[author] = data.changes_by_date_by_author[stamp][author]['lines_added'] commits_by_authors[author] = data.changes_by_date_by_author[stamp][author]['commits'] - fgl.write(' %d' % lines_by_authors[author]) - fgc.write(' %d' % commits_by_authors[author]) - fgl.write('\n') - fgc.write('\n') - fgl.close() - fgc.close() + # fgl.write(' %d' % lines_by_authors[author]) + # fgc.write(' %d' % commits_by_authors[author]) + # authors_cumulated_commits_series[author]['data'].append({"x": stamp, "y": lines_by_authors[author]}) + + authors_cumulated_commits_series[author]['data'].append(lines_by_authors[author]) + authors_commits_series[author]['data'].append(commits_by_authors[author]) + + # fgl.write('\n') + # fgc.write('\n') + # fgl.close() + # fgc.close() + + # Authors :: Cumulated added LoC per author + authors_cumulated_commits_config = { + **chart_default_config, + "chart": {**chart_default_config["chart"], "type": 'line'}, + "series": list(authors_cumulated_commits_series.values()), + "markers": {"size": 0,"hover": {"sizeOffset": 6}}, + "xaxis": { + **chart_default_config["xaxis"], + "labels": { + **chart_default_config["xaxis"]["labels"] , + "show" : False + }} + } + + # authors_content.append(activity_html.addChart(authors_cumulated_commits_config, name='chartCumulatedAddedLoCAuthor', title=f'Cumulated Added LoC per Author {author_disclaimer}', className="xl:col-span-6")) + + # Authors :: Commits per Author + authors_commits_config = { + **chart_default_config, + "chart": {**chart_default_config["chart"], "type": 'line'}, + "series": list(authors_commits_series.values()), + "markers": {"size": 0,"hover": {"sizeOffset": 6}}, + "xaxis": { + **chart_default_config["xaxis"], + "labels": { + **chart_default_config["xaxis"]["labels"] , + "show" : False + }} + } + + # authors_content.append(activity_html.addChart(authors_commits_config, name='chartCommitsPerAuthor', title=f'Commits per Author {author_disclaimer}', className="xl:col-span-6")) + # Authors :: Author of Month - authors_content.append(html_header(2, 'Author of Month')) - authors_content.append('') - authors_content.append('' % conf['authors_top']) + author_of_month_content = [] + author_of_month_content.append('
    MonthAuthorCommits (%%)Next top %dNumber of authors
    ') + author_of_month_content.append('' % conf['authors_top']) for yymm in reversed(sorted(data.author_of_month.keys())): authordict = data.author_of_month[yymm] authors = getkeyssortedbyvalues(authordict) authors.reverse() commits = data.author_of_month[yymm][authors[0]] next = ', '.join(authors[1:conf['authors_top']+1]) - authors_content.append('' % (yymm, authors[0], commits, (100.0 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm], next, len(authors))) + author_of_month_content.append('' % (yymm, authors[0], commits, (100.0 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm], next, len(authors))) + + author_of_month_content.append('
    MonthAuthorCommits (%%)Next top %dNumber of authors
    %s%s%d (%.2f%% of %d)%s%d
    %s%s%d (%.2f%% of %d)%s%d
    ') - authors_content.append('') + authors_content.append(authors_html.addCard(author_of_month_content, title='Author of Month')) - authors_content.append(html_header(2, 'Author of Year')) - authors_content.append('' % conf['authors_top']) + # Authors :: Author of Year + author_of_year_content = [] + author_of_year_content.append('
    YearAuthorCommits (%%)Next top %dNumber of authors
    ' % conf['authors_top']) for yy in reversed(sorted(data.author_of_year.keys())): authordict = data.author_of_year[yy] authors = getkeyssortedbyvalues(authordict) authors.reverse() commits = data.author_of_year[yy][authors[0]] next = ', '.join(authors[1:conf['authors_top']+1]) - authors_content.append('' % (yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next, len(authors))) - authors_content.append('
    YearAuthorCommits (%%)Next top %dNumber of authors
    %s%s%d (%.2f%% of %d)%s%d
    ') + author_of_year_content.append('%s%s%d (%.2f%% of %d)%s%d' % (yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next, len(authors))) + author_of_year_content.append('') - # Domains - authors_content.append(html_header(2, 'Commits by Domains')) + authors_content.append(authors_html.addCard(author_of_year_content, title='Author of Year', className="xl:col-span-6")) + + # Authors :: Domains domains_by_commits = getkeyssortedbyvaluekey(data.domains, 'commits') domains_by_commits.reverse() # most first - authors_content.append('
    ') - authors_content.append('') - fp = open(path + '/domains.dat', 'w') + + authors_commits_by_domains_series = [{ + "name": "Commits", + "color": "#1A56DB", + "data": [] + },{ + "name": "Percentage", + "color": "#8FB0F6", + "data": [] + }] n = 0 for domain in domains_by_commits: if n == conf['max_domains']: @@ -1268,11 +1283,18 @@ class HTMLReportCreator(ReportCreator): commits = 0 n += 1 info = data.getDomainInfo(domain) - fp.write('%s %d %d\n' % (domain, n , info['commits'])) - authors_content.append('' % (domain, info['commits'], (100.0 * info['commits'] / totalcommits))) - authors_content.append('
    DomainsTotal (%)
    %s%d (%.2f%%)
    ') - authors_content.append('Commits by Domains') - fp.close() + authors_commits_by_domains_series[0]['data'].append({"x": domain, "y": info['commits']}) + p = (100.0 * info['commits'] / totalcommits) + authors_commits_by_domains_series[1]['data'].append({"x": domain, "y": f'{p:.2f}'}) + + authors_commits_by_domains_config = { + **chart_default_config, + "series": authors_commits_by_domains_series, + } + + authors_content.append(activity_html.addChart(authors_commits_by_domains_config, name='chartCommitsbyDomains', title='Commits by Domains', className="xl:col-span-6")) + + authors_content.append('
    ') authors_html.create(authors_content) diff --git a/gitstats.css b/gitstats.css index 31881fbe..8db4d3b4 100644 --- a/gitstats.css +++ b/gitstats.css @@ -3,65 +3,65 @@ */ dt { - font-weight: bold; - float: left; - margin-right: 1em; + font-weight: bold; + float: left; + margin-right: 1em; } dt:after { - content: ': '; + content: ": "; } dd { - display: block; - clear: left; + display: block; + clear: left; } table { - border: 1px solid black; - border-collapse: collapse; - font-size: 80%; - margin-bottom: 1em; + border: 1px solid black; + border-collapse: collapse; + font-size: 80%; + margin-bottom: 1em; } table.noborders { - border: none; + border: none; } table.noborders td { - border: none; + border: none; } .vtable { - float: right; - clear: both; + float: right; + clear: both; } table.tags td { - vertical-align: top; + vertical-align: top; } td { - background-color: white; + background-color: white; } th { - background-color: #ddf; + background-color: #ddf; } th a { - text-decoration: none; + text-decoration: none; } tr:hover { - background-color: #ddf; + background-color: #ddf; } td { - border: 1px solid black; - padding: 0.2em; - padding-left: 0.3em; - padding-right: 0.2em; + border: 1px solid black; + padding: 0.2em; + padding-left: 0.3em; + padding-right: 0.2em; } /* Navigation bar; tabbed style */ @@ -96,47 +96,111 @@ td { } */ main img { - border: 1px solid black; - padding: 0.5em; - background-color: white; + border: 1px solid black; + padding: 0.5em; + background-color: white; } th img { - border: 0px; - padding: 0px; - background-color: #ddf; + border: 0px; + padding: 0px; + background-color: #ddf; } -h1 a, h2 a { - color: black; - text-decoration: none; +h1 a, +h2 a { + color: black; + text-decoration: none; } -main h1:hover a:after, -main h2:hover a:after { - content: '¶'; - color: #555; +main h1:hover a:after, +main h2:hover a:after { + content: "¶"; + color: #555; } main h1 { - font-size: x-large; + font-size: x-large; } -main h2 { - background-color: #564; - border: 1px solid black; - padding-left: 0.5em; - padding-right: 0.5em; - color: white; - font-size: large; - clear: both; +main h2 { + background-color: #564; + border: 1px solid black; + padding-left: 0.5em; + padding-right: 0.5em; + color: white; + font-size: large; + clear: both; } main h2 a { - color: white; + color: white; } main .moreauthors { - font-size: 80%; + font-size: 80%; } +.dark .apexcharts-canvas .apexcharts-legend-text { + color: #aeb7c0 !important; +} +.apexcharts-canvas .apexcharts-legend-text { + color: #64748b !important; +} + +.dark .apexcharts-canvas .apexcharts-text { + fill: #aeb7c0 !important; +} + +.apexcharts-canvas .apexcharts-text { + fill: #64748b !important; +} + +.dark .apexcharts-canvas .apexcharts-xcrosshairs { + fill: #2e3a47 !important; +} +.apexcharts-canvas .apexcharts-xcrosshairs { + fill: #e2e8f0 !important; +} + + +.dark .apexcharts-canvas .apexcharts-gridline { + stroke: #2e3a47 !important; +} +.apexcharts-canvas .apexcharts-gridline { + stroke: #e2e8f0 !important; +} + +.dark .apexcharts-canvas .apexcharts-series.apexcharts-pie-series path { + stroke: transparent !important; +} + +.apexcharts-canvas .apexcharts-legend-series { + display: inline-flex; + gap: 0.375rem +} + +/* +.apexcharts-tooltip.apexcharts-theme-light { + @apply dark:!border-strokedark dark:!bg-boxdark; +} +.apexcharts-tooltip.apexcharts-theme-light .apexcharts-tooltip-title { + @apply dark:!border-strokedark dark:!bg-meta-4; +} +.apexcharts-xaxistooltip, +.apexcharts-yaxistooltip { + @apply dark:!border-meta-4 dark:!bg-meta-4 dark:!text-bodydark1; +} +.apexcharts-xaxistooltip-bottom:after { + @apply !border-b-gray dark:!border-b-meta-4; +} +.apexcharts-xaxistooltip-bottom:before { + @apply !border-b-gray dark:!border-b-meta-4; +} +.apexcharts-xaxistooltip-bottom { + @apply !rounded !border-none !bg-gray !text-xs !font-medium !text-black dark:!text-white; +} +.apexcharts-tooltip-series-group { + @apply !pl-1.5; +} +*/ \ No newline at end of file From 62365ee3d5f8c1d5b33034db7b3fc40236558f37 Mon Sep 17 00:00:00 2001 From: Christhoval Barba Date: Thu, 7 Mar 2024 13:31:33 -0500 Subject: [PATCH 9/9] feat(*.html): update chart to use apexchart. --- gitstats | 919 ++++++++++++++++++++----------------------------------- html.py | 23 +- 2 files changed, 354 insertions(+), 588 deletions(-) diff --git a/gitstats b/gitstats index a1e4da8c..7e12c421 100755 --- a/gitstats +++ b/gitstats @@ -16,17 +16,12 @@ import time import zlib import multiprocessing -if sys.version_info < (2, 6): - print >> sys.stderr, "Python 2.6 or higher is required for gitstats" - sys.exit(1) - from multiprocessing import Pool import html os.environ['LC_ALL'] = 'C' -GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n' ON_LINUX = (platform.system() == 'Linux') WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') JSONFILE = 'gitstats.json' @@ -35,17 +30,11 @@ exectime_internal = 0.0 exectime_external = 0.0 time_start = time.time() -# By default, gnuplot is searched from path, but can be overridden with the -# environment variable "GNUPLOT" -gnuplot_cmd = 'gnuplot' -if 'GNUPLOT' in os.environ: - gnuplot_cmd = os.environ['GNUPLOT'] - conf = { 'max_domains': 10, 'max_ext_length': 10, 'style': 'gitstats.css', - 'max_authors': 7, + 'max_authors': 20, 'authors_top': 5, 'commit_begin': '', 'commit_end': 'HEAD', @@ -131,9 +120,6 @@ def getversion(): def getgitversion(): return getpipeoutput(['git --version']).split('\n')[0] -def getgnuplotversion(): - return getpipeoutput(['%s --version' % gnuplot_cmd]).split('\n')[0] - def getnumoffilesfromrev(time_rev): """ Get number of files changed in commit @@ -173,6 +159,7 @@ class DataCollector: self.lineactivity_by_year_week = {} # yy_wNN -> commits self.lineactivity_by_year_week_peak = 0 self.changes_by_date_by_author = {} # stamp -> author -> lines_added + self.changes_by_month_by_author = {} # stamp -> author -> lines_added self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp, last_active_day, active_days, lines_added, lines_removed} @@ -212,12 +199,15 @@ class DataCollector: self.tags = {} self.files_by_stamp = {} # stamp -> files + self.files_by_month = {} # year-month -> files # extensions self.extensions = {} # extension -> files, lines # line statistics self.changes_by_date = {} # stamp -> { files, ins, del } + self.changes_by_month = {} # yy-MM -> { files, ins, del } + self.changes_by_year = {} # yy -> { files, ins, del } ## # This should be the main function to extract data from the repository. @@ -516,8 +506,11 @@ class GitDataCollector(DataCollector): if len(parts) != 2: continue (stamp, files) = parts[0:2] + date = datetime.datetime.fromtimestamp(int(stamp)) + yymm = date.strftime('%Y-%m') try: self.files_by_stamp[int(stamp)] = int(files) + self.files_by_month[yymm] = int(files) except ValueError: print('Warning: failed to parse line "%s"' % line) @@ -584,6 +577,8 @@ class GitDataCollector(DataCollector): # N files changed, N insertions (+), N deletions(-) # self.changes_by_date = {} # stamp -> { files, ins, del } + self.changes_by_month = {} # yyMM -> { files, ins, del } + self.changes_by_year = {} # yy -> { files, ins, del } # computation of lines of code by date is better done # on a linear history. extra = '' @@ -604,11 +599,24 @@ class GitDataCollector(DataCollector): if pos != -1: try: (stamp, author) = (int(line[:pos]), line[pos+1:]) + date = datetime.datetime.fromtimestamp(stamp) + yymm = date.strftime('%Y-%m') + if author not in conf["excluded_authors"]: self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines } + self.changes_by_year[date.year] = { + 'files': files, + 'ins': inserted, + 'del': deleted, + 'lines': total_lines + } + self.changes_by_month[yymm] = { + 'files': files, + 'ins': inserted, + 'del': deleted, + 'lines': total_lines + } - date = datetime.datetime.fromtimestamp(stamp) - yymm = date.strftime('%Y-%m') self.lines_added_by_month[yymm] = self.lines_added_by_month.get(yymm, 0) + inserted self.lines_removed_by_month[yymm] = self.lines_removed_by_month.get(yymm, 0) + deleted @@ -696,6 +704,8 @@ class GitDataCollector(DataCollector): try: oldstamp = stamp (stamp, author) = (int(line[:pos]), line[pos+1:]) + date = datetime.datetime.fromtimestamp(float(stamp)) + yyMM = date.strftime('%Y-%m') if author not in conf["excluded_authors"]: if oldstamp > stamp: # clock skew, keep old timestamp to avoid having ugly graph @@ -705,12 +715,24 @@ class GitDataCollector(DataCollector): self.authors[author]['commits'] = self.authors[author].get('commits', 0) + 1 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 + if stamp not in self.changes_by_date_by_author: self.changes_by_date_by_author[stamp] = {} + if yyMM not in self.changes_by_month_by_author: + self.changes_by_month_by_author[yyMM] = {} + if author not in self.changes_by_date_by_author[stamp]: self.changes_by_date_by_author[stamp][author] = {} + if author not in self.changes_by_month_by_author[yyMM]: + self.changes_by_month_by_author[yyMM][author] = {} + self.changes_by_date_by_author[stamp][author]['lines_added'] = self.authors[author]['lines_added'] + self.changes_by_date_by_author[stamp][author]['lines_removed'] = self.authors[author]['lines_removed'] self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits'] + + self.changes_by_month_by_author[yyMM][author]['lines_added'] = self.changes_by_month_by_author[yyMM][author].get('lines_added', 0) + self.authors[author]['lines_added'] + self.changes_by_month_by_author[yyMM][author]['lines_removed'] = self.changes_by_month_by_author[yyMM][author].get('lines_removed', 0) + self.authors[author]['lines_removed'] + self.changes_by_month_by_author[yyMM][author]['commits'] = self.changes_by_month_by_author[yyMM][author].get('commits', 0) + self.authors[author]['commits'] files, inserted, deleted = 0, 0, 0 except ValueError: print('Warning: unexpected line "%s"' % line) @@ -867,34 +889,28 @@ class HTMLReportCreator(ReportCreator): # index.html # General format = '%Y-%m-%d %H:%M:%S' - # general_content = ['

    GitStats - %s

    ' % data.projectname] - general_content = [] general_html = html.HTML(path=f'{path}/index.html', title=f"{data.projectname}'S STATS", version= getversion()) + general_html.add('
    ') + general_html.tilesItemStat(title='Project name', info=data.projectname) + general_html.tilesItemStat(title='Generated', info=datetime.datetime.now().strftime(format)) + general_html.tilesItemStat(title='Report Period', info=f'{data.getFirstCommitDate().strftime(format)} to {data.getLastCommitDate().strftime(format)}') + general_html.add('
    ') - tiles = '\n'.join([ - general_html.tilesItemStat(title='Project name', info=data.projectname), - general_html.tilesItemStat(title='Generated', info=datetime.datetime.now().strftime(format)), - general_html.tilesItemStat(title='Report Period', info=f'{data.getFirstCommitDate().strftime(format)} to {data.getLastCommitDate().strftime(format)}'), - ]) - - cards = '\n'.join([ - general_html.cardItemStat(title='Branches', count=data.total_branches), - general_html.cardItemStat(title='Tags', count=data.total_tags), - general_html.cardItemStat(title='Age', count=f'{data.getCommitDeltaDays():.1f} days'), - general_html.cardItemStat(title='Active days', count=f'{len(data.getActiveDays())}', stat=f'{(100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()):3.2f}%', arrow='up'), - general_html.cardItemStat(title='Total files', count=data.getTotalFiles()), - general_html.cardItemStat(title='Total LOC', count=data.getTotalLOC()), - general_html.cardItemStat(title='Total lines added', count=data.total_lines_added, stat=f'{((data.total_lines_added/data.getTotalLOC())*100):.2f}%', arrow='up'), - general_html.cardItemStat(title='Total lines removed', count=data.total_lines_removed, stat=f'{((data.total_lines_removed/data.getTotalLOC())*100):.2f}%', arrow='up'), - general_html.cardItemStat(title='Total commits', count=data.getTotalCommits(), stat=f'{(float(data.getTotalCommits()) / len(data.getActiveDays())):.1f}', arrow='up'), - general_html.cardItemStat(title='Authors', count=data.getTotalAuthors(), stat=f'{((1.0 * data.getTotalCommits()) / data.getTotalAuthors()):.1f}', arrow='up'), - ]) - general_content.append(f'
    {tiles}
    ') - - general_content.append(f'
    {cards}
    ') + general_html.add('
    ') + general_html.cardItemStat(title='Branches', count=data.total_branches) + general_html.cardItemStat(title='Tags', count=data.total_tags) + general_html.cardItemStat(title='Age', count=f'{data.getCommitDeltaDays():.1f} days') + general_html.cardItemStat(title='Active days', count=f'{len(data.getActiveDays())}', stat=f'{(100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays()):3.2f}%', arrow='up') + general_html.cardItemStat(title='Total files', count=data.getTotalFiles()) + general_html.cardItemStat(title='Total LOC', count=data.getTotalLOC()) + general_html.cardItemStat(title='Total lines added', count=data.total_lines_added, stat=f'{((data.total_lines_added/data.getTotalLOC())*100):.2f}%', arrow='up') + general_html.cardItemStat(title='Total lines removed', count=data.total_lines_removed, stat=f'{((data.total_lines_removed/data.getTotalLOC())*100):.2f}%', arrow='up') + general_html.cardItemStat(title='Total commits', count=data.getTotalCommits(), stat=f'{(float(data.getTotalCommits()) / len(data.getActiveDays())):.1f}', arrow='up') + general_html.cardItemStat(title='Authors', count=data.getTotalAuthors(), stat=f'{((1.0 * data.getTotalCommits()) / data.getTotalAuthors()):.1f}', arrow='up') + general_html.add('
    ') # general_content.append('
    ') # general_content.append('
    Project name
    %s (%s branches, %s tags)
    ' % (data.projectname, data.total_branches, data.total_tags)) @@ -908,7 +924,7 @@ class HTMLReportCreator(ReportCreator): # general_content.append('
    Authors
    %s (average %.1f commits per author)
    ' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors())) # general_content.append('
    ') - general_html.create(general_content) + general_html.create() chart_default_config = json.load(open('chart.json')) @@ -918,15 +934,14 @@ class HTMLReportCreator(ReportCreator): # Activity totalcommits = data.getTotalCommits() - activity_content = [] activity_html = html.HTML(path=f'{path}/activity.html', title='Activity', version= getversion()) - activity_content.append('
    ') + activity_html.add('
    ') # Last 30 days # Last 12 months - # Weekly activity + # Activity :: Weekly activity WEEKS = 32 # generate weeks to show (previous N weeks from now) now = datetime.datetime.now() @@ -951,10 +966,10 @@ class HTMLReportCreator(ReportCreator): "series": activity_per_weekly_series } - activity_content.append(activity_html.addChart(activity_per_weekly_config, name='chartWeeklyActivity', title=f'Weekly activity Last {WEEKS} weeks', className="")) + activity_html.addChart(activity_per_weekly_config, name='chartWeeklyActivity', title=f'Weekly activity Last {WEEKS} weeks', className="") - # Hour of Day + # Activity :: Hour of Day hour_of_day = data.getActivityByHourOfDay() activity_per_hours_day_series = [ @@ -972,10 +987,10 @@ class HTMLReportCreator(ReportCreator): "series": activity_per_hours_day_series } - activity_content.append(activity_html.addChart(activity_per_hours_day_config, name='chartHourOfDay', title='Hour of Day', className="")) + activity_html.addChart(activity_per_hours_day_config, name='chartHourOfDay', title='Hour of Day', className="") - # Day of Week + # Activity :: Day of Week day_of_week = data.getActivityByDayOfWeek() activity_per_day_week_series = [ @@ -993,9 +1008,9 @@ class HTMLReportCreator(ReportCreator): "series": activity_per_day_week_series } - activity_content.append(activity_html.addChart(activity_per_day_week_config, name='chartDayofWeek', title='Day of Week', className="xl:col-span-4")) + activity_html.addChart(activity_per_day_week_config, name='chartDayofWeek', title='Day of Week', className="xl:col-span-4") - # Hour of Week + # Activity :: Hour of Week activity_hour_of_week_series = [] for weekday in range(0, 7): @@ -1019,9 +1034,9 @@ class HTMLReportCreator(ReportCreator): "yaxis": chart_default_config["yaxis"], } - activity_content.append(activity_html.addChart(activity_hour_of_week_config, name='chartHourOfWeek', title='Hour of Week', className="xl:col-span-8")) + activity_html.addChart(activity_hour_of_week_config, name='chartHourOfWeek', title='Hour of Week', className="xl:col-span-8") - # Month of Year + # Activity :: Month of Year activity_per_month_of_year_series = [ {"name": "Commits", "color": "#1A56DB", "data": []}, {"name": "Percentage", "color": "#779EF1", "data": []}, @@ -1036,16 +1051,14 @@ class HTMLReportCreator(ReportCreator): "series": activity_per_month_of_year_series } - activity_content.append(activity_html.addChart(activity_per_month_of_year_config, name='chartMonthOfYear', title='Month of Year', className="xl:col-span-5")) + activity_html.addChart(activity_per_month_of_year_config, name='chartMonthOfYear', title='Month of Year', className="xl:col-span-5") - # Commits by year/month + # Activity :: Commits by year/month activity_per_year_month_serie = [] for yymm in sorted(data.commits_by_month.keys()): activity_per_year_month_serie.append({"x": f'{yymm}', "y": data.commits_by_month.get(yymm,0), "lines_added": data.lines_added_by_month.get(yymm,0), "lines_removed": data.lines_removed_by_month.get(yymm,0), "percentage": (100.0 * data.commits_by_month.get(yymm,0)) /totalcommits}) - activity_per_year_month_config = dict(chart_default_config) - activity_per_year_month_config = { **chart_default_config, "series": [{ @@ -1055,14 +1068,14 @@ class HTMLReportCreator(ReportCreator): "xaxis": { **chart_default_config["xaxis"], "labels": { - **activity_per_year_month_config["xaxis"]["labels"] , + **chart_default_config["xaxis"]["labels"] , "show" : False }}} - activity_content.append(activity_html.addChart(activity_per_year_month_config, name='chartCommitsByYearMonth', title='Commits by year/month', className="xl:col-span-7")) + activity_html.addChart(activity_per_year_month_config, name='chartCommitsByYearMonth', title='Commits by year/month', className="xl:col-span-7") - # Commits by year + # Activity :: Commits by year activity_by_year_series = [ {"name": "Commits", "color": "#1A56DB", "data": []}, {"name": "Lines Added", "color": "#23961B","data": []}, @@ -1080,9 +1093,9 @@ class HTMLReportCreator(ReportCreator): "series": activity_by_year_series } - activity_content.append(activity_html.addChart(activity_by_year_config, name='chartCommitsByYear', title='Commits by Year', className="xl:col-span-6")) + activity_html.addChart(activity_by_year_config, name='chartCommitsByYear', title='Commits by Year', className="xl:col-span-6") - # Commits by timezone + # Activity :: Commits by timezone activity_by_timezone_series = [ {"name": "Commits", "color": "#1A56DB", "data": []}, {"name": "Percentage", "color": "#779EF1", "data": []}, @@ -1098,19 +1111,18 @@ class HTMLReportCreator(ReportCreator): "series": activity_by_timezone_series } - activity_content.append(activity_html.addChart(activity_by_timezone_config, name='chartCommitsByTimezone', title='Commits by Timezone', className="xl:col-span-6")) + activity_html.addChart(activity_by_timezone_config, name='chartCommitsByTimezone', title='Commits by Timezone', className="xl:col-span-6") - activity_content.append('
    ') + activity_html.add('
    ') - activity_html.create(activity_content) + activity_html.create() ### # authors.html # Authors - authors_content = [] - authors_content.append('
    ') - authors_html = html.HTML(path=f'{path}/authors.html', title='Authors', version= getversion()) + authors_html.add('
    ') + # Authors :: List of authors list_authors_content = [] @@ -1127,7 +1139,7 @@ class HTMLReportCreator(ReportCreator): rest = allauthors[conf['max_authors']:] list_authors_content.append('

    These didn\'t make it to the top: %s

    ' % ', '.join(rest)) - authors_content.append(authors_html.addCard(list_authors_content, title='List of Authors')) + authors_html.addCard(list_authors_content, title='List of Authors') # Authors :: Commits author_disclaimer = '' @@ -1135,10 +1147,8 @@ class HTMLReportCreator(ReportCreator): if len(allauthors) > max_authors: author_disclaimer =f'Only top {max_authors} authors shown' - # fgl = open(path + '/lines_of_code_by_author.dat', 'w') - # fgc = open(path + '/commits_by_author.dat', 'w') - lines_by_authors = {} # cumulated added lines by + # lines_removed_by_authors = {} # author. to save memory, # changes_by_date_by_author[stamp][author] is defined # only at points where author commits. @@ -1171,40 +1181,48 @@ class HTMLReportCreator(ReportCreator): "#008000", "#6A5ACD"] - authors_cumulated_commits_series = {} + authors_cumulated_lines_added_series = {} + # authors_cumulated_lines_removed_series = {} authors_commits_series = {} self.authors_to_plot = data.getAuthors(conf['max_authors']) for idx, author in enumerate(self.authors_to_plot): lines_by_authors[author] = 0 + # lines_removed_by_authors[author] = 0 commits_by_authors[author] = 0 - authors_cumulated_commits_series[author]= {"name": author, "color": colors[idx], "data": []} + + authors_cumulated_lines_added_series[author]= {"name": author, "color": colors[idx], "data": []} + # authors_cumulated_lines_removed_series[author]= {"name": author, "color": colors[idx], "data": []} authors_commits_series[author]= {"name": author, "color": colors[idx], "data": []} - for stamp in sorted(data.changes_by_date_by_author.keys()): - # fgl.write('%d' % stamp) - # fgc.write('%d' % stamp) - for author in self.authors_to_plot: - if author in data.changes_by_date_by_author[stamp].keys(): - lines_by_authors[author] = data.changes_by_date_by_author[stamp][author]['lines_added'] - commits_by_authors[author] = data.changes_by_date_by_author[stamp][author]['commits'] - # fgl.write(' %d' % lines_by_authors[author]) - # fgc.write(' %d' % commits_by_authors[author]) - # authors_cumulated_commits_series[author]['data'].append({"x": stamp, "y": lines_by_authors[author]}) + # for stamp in sorted(data.changes_by_date_by_author.keys()): + # for author in self.authors_to_plot: + # if author in data.changes_by_date_by_author[stamp].keys(): + # lines_by_authors[author] = data.changes_by_date_by_author[stamp][author]['lines_added'] + # commits_by_authors[author] = data.changes_by_date_by_author[stamp][author]['commits'] + # # authors_cumulated_commits_series[author]['data'].append({"x": stamp, "y": lines_by_authors[author]}) - authors_cumulated_commits_series[author]['data'].append(lines_by_authors[author]) - authors_commits_series[author]['data'].append(commits_by_authors[author]) + # authors_cumulated_commits_series[author]['data'].append(lines_by_authors[author]) + # authors_commits_series[author]['data'].append(commits_by_authors[author]) - # fgl.write('\n') - # fgc.write('\n') - # fgl.close() - # fgc.close() + for yyMM in sorted(data.changes_by_month_by_author.keys()): + for author in self.authors_to_plot: + if author in data.changes_by_month_by_author[yyMM].keys(): + lines_by_authors[author] = lines_by_authors[author] + data.changes_by_month_by_author[yyMM][author]['lines_added'] + # lines_removed_by_authors[author] = lines_removed_by_authors[author] + data.changes_by_month_by_author[yyMM][author]['lines_removed'] + commits_by_authors[author] = commits_by_authors[author] + data.changes_by_month_by_author[yyMM][author]['commits'] + + # authors_cumulated_commits_series[author]['data'].append(lines_by_authors[author]) + authors_cumulated_lines_added_series[author]['data'].append({"x": yyMM, "y": lines_by_authors[author]}) + # authors_cumulated_lines_removed_series[author]['data'].append({"x": yyMM, "y": lines_removed_by_authors[author]}) + authors_commits_series[author]['data'].append({"x": yyMM, "y": commits_by_authors[author]}) + # authors_commits_series[author]['data'].append(commits_by_authors[author]) # Authors :: Cumulated added LoC per author authors_cumulated_commits_config = { **chart_default_config, "chart": {**chart_default_config["chart"], "type": 'line'}, - "series": list(authors_cumulated_commits_series.values()), + "series": list(authors_cumulated_lines_added_series.values()), "markers": {"size": 0,"hover": {"sizeOffset": 6}}, "xaxis": { **chart_default_config["xaxis"], @@ -1214,7 +1232,24 @@ class HTMLReportCreator(ReportCreator): }} } - # authors_content.append(activity_html.addChart(authors_cumulated_commits_config, name='chartCumulatedAddedLoCAuthor', title=f'Cumulated Added LoC per Author {author_disclaimer}', className="xl:col-span-6")) + authors_html.addChart(authors_cumulated_commits_config, name='chartCumulatedAddedLoCAuthor', title=f'Cumulated Added LoC per Author {author_disclaimer}', className="xl:col-span-6") + + # Authors :: Cumulated removed LoC per author + # authors_cumulated_removed_loc_config = { + # **chart_default_config, + # "chart": {**chart_default_config["chart"], "type": 'line'}, + # "series": list(authors_cumulated_lines_removed_series.values()), + # "markers": {"size": 0,"hover": {"sizeOffset": 6}}, + # "xaxis": { + # **chart_default_config["xaxis"], + # "labels": { + # **chart_default_config["xaxis"]["labels"] , + # "show" : False + # }} + # } + + # authors_html.addChart(authors_cumulated_removed_loc_config, name='chartCumulatedRemovedLoCAuthor', title=f'Cumulated removed LoC per Author {author_disclaimer}', className="xl:col-span-6") + # Authors :: Commits per Author authors_commits_config = { @@ -1230,7 +1265,7 @@ class HTMLReportCreator(ReportCreator): }} } - # authors_content.append(activity_html.addChart(authors_commits_config, name='chartCommitsPerAuthor', title=f'Commits per Author {author_disclaimer}', className="xl:col-span-6")) + authors_html.addChart(authors_commits_config, name='chartCommitsPerAuthor', title=f'Commits per Author {author_disclaimer}', className="xl:col-span-6") # Authors :: Author of Month @@ -1247,7 +1282,7 @@ class HTMLReportCreator(ReportCreator): author_of_month_content.append('') - authors_content.append(authors_html.addCard(author_of_month_content, title='Author of Month')) + authors_html.addCard(author_of_month_content, title='Author of Month') # Authors :: Author of Year author_of_year_content = [] @@ -1261,7 +1296,7 @@ class HTMLReportCreator(ReportCreator): author_of_year_content.append('%s%s%d (%.2f%% of %d)%s%d' % (yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next, len(authors))) author_of_year_content.append('') - authors_content.append(authors_html.addCard(author_of_year_content, title='Author of Year', className="xl:col-span-6")) + authors_html.addCard(author_of_year_content, title='Author of Year', className="xl:col-span-6") # Authors :: Domains domains_by_commits = getkeyssortedbyvaluekey(data.domains, 'commits') @@ -1292,50 +1327,60 @@ class HTMLReportCreator(ReportCreator): "series": authors_commits_by_domains_series, } - authors_content.append(activity_html.addChart(authors_commits_by_domains_config, name='chartCommitsbyDomains', title='Commits by Domains', className="xl:col-span-6")) + authors_html.addChart(authors_commits_by_domains_config, name='chartCommitsbyDomains', title='Commits by Domains', className="xl:col-span-6") - authors_content.append('
    ') + authors_html.add('
    ') - authors_html.create(authors_content) + authors_html.create() ### # files.html # Files - - files_content = [] files_html = html.HTML(path=f'{path}/files.html', title='Files', version= getversion()) - files_content.append('
    \n') - files_content.append('
    Total files
    %d
    ' % data.getTotalFiles()) - files_content.append('
    Total lines
    %d
    ' % data.getTotalLOC()) + files_html.add('
    ') + files_html.cardItemStat(title='Total files', count=data.getTotalFiles()) + files_html.cardItemStat(title='Total LoC', count=data.getTotalLOC()) try: - files_content.append('
    Average file size
    %.2f bytes
    ' % (float(data.getTotalSize()) / data.getTotalFiles())) + files_html.cardItemStat(title='Average file size', count=f'{(float(data.getTotalSize()) / data.getTotalFiles()):2f} bytes') except ZeroDivisionError: pass - files_content.append('
    \n') + files_html.add('
    ') + + files_html.add('
    ') # Files :: File count by date - files_content.append(html_header(2, 'File count by date')) - - # use set to get rid of duplicate/unnecessary entries - files_by_date = set() - for stamp in sorted(data.files_by_stamp.keys()): - files_by_date.add('%s %d' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp])) - - fg = open(path + '/files_by_date.dat', 'w') - for line in sorted(list(files_by_date)): - fg.write('%s\n' % line) - #for stamp in sorted(data.files_by_stamp.keys()): - # fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp])) - fg.close() - - files_content.append('Files by Date') + files_by_month_series = {"name": 'Files', "color": "#3C50E0", "data": []} + # # use set to get rid of duplicate/unnecessary entries + # files_by_date = set() + # for stamp in sorted(data.files_by_stamp.keys()): + # files_by_date.add('%s %d' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp])) + + for yyMM in sorted(data.files_by_month.keys()): + files_by_month_series["data"].append({"x": yyMM, "y": data.files_by_month[yyMM]}) + + files_by_month_config = { + **chart_default_config, + "chart": {**chart_default_config["chart"], "type": 'line'}, + "series": [files_by_month_series], + "markers": {"size": 0,"hover": {"sizeOffset": 6}}, + "xaxis": { + **chart_default_config["xaxis"], + "labels": { + **chart_default_config["xaxis"]["labels"] , + "show" : False + }} + } + + files_html.addChart(files_by_month_config, name='chartFilesByMonth', title='File count by month', className="xl:col-span-12") + #files_content.append('

    Average file size by date

    ') # Files :: Extensions - files_content.append(html_header(2, 'Extensions')) - files_content.append('') + files_extensions_series = {"name": 'Extensions', "color": "#3C50E0", "data": []} + files_extensions_content = [] + files_extensions_content.append('
    ExtensionFiles (%)Lines (%)Lines/file
    ') for ext in sorted(data.extensions.keys()): files = data.extensions[ext]['files'] lines = data.extensions[ext]['lines'] @@ -1343,10 +1388,23 @@ class HTMLReportCreator(ReportCreator): loc_percentage = (100.0 * lines) / data.getTotalLOC() except ZeroDivisionError: loc_percentage = 0 - files_content.append('' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files)) - files_content.append('
    ExtensionFiles (%)Lines (%)Lines/file
    %s%d (%.2f%%)%d (%.2f%%)%d
    ') + files_extensions_content.append('%s%d (%.2f%%)%d (%.2f%%)%d' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files)) + files_extensions_series["data"].append({"x": ext, "y": files}) + files_extensions_content.append('') + + files_extensions_config = { + "chart": {**chart_default_config["chart"], "type": 'treemap'}, + "series": [files_extensions_series], + } + + files_html.addChart(files_extensions_config, name='chartFilesByExtensions', title='Extensions treemap', className="xl:col-span-12") + + files_html.addCard(files_extensions_content, title='Extensions', className="xl:col-span-4") - files_html.create(files_content) + + files_html.add('
    ') + + files_html.create() ### # lines.html @@ -1354,22 +1412,53 @@ class HTMLReportCreator(ReportCreator): lines_content=[] lines_html = html.HTML(path=f'{path}/lines.html', title='Lines', version= getversion()) - lines_content.append('
    \n') - lines_content.append('
    Total lines
    %d
    ' % data.getTotalLOC()) - lines_content.append('
    \n') + lines_html.add('
    ') + lines_html.cardItemStat(title='Total LoC', count=data.getTotalLOC()) + lines_html.add('
    ') + + lines_html.add('
    ') + + + lines_by_year_series = {"name": 'Lines', "color": "#3C50E0", "data": []} + + for year in sorted(data.changes_by_year.keys()): + lines_by_year_series["data"].append({"x": year, "y": data.changes_by_year[year]['lines']}) + + + lines_by_year_config = { + **chart_default_config, + "chart": {**chart_default_config["chart"], "type": 'line'}, + "series": [lines_by_year_series], + "markers": {"size": 0,"hover": {"sizeOffset": 6}}, + "xaxis": { + **chart_default_config["xaxis"], + "labels": { + **chart_default_config["xaxis"]["labels"] , + "show" : False + }} + } + + lines_html.addChart(lines_by_year_config, name='chartLinesOfCodeByYear', title='Lines of Code by Year', className="xl:col-span-6") + + lines_by_month_series = {"name": 'Lines', "color": "#3C50E0", "data": []} - lines_content.append(html_header(2, 'Lines of Code')) - lines_content.append('Lines of Code') + for yyMM in sorted(data.changes_by_month.keys()): + lines_by_month_series["data"].append({"x": yyMM, "y": data.changes_by_month[yyMM]['lines']}) + + lines_by_month_config = { + **lines_by_year_config, + "series": [lines_by_month_series], + } - fg = open(path + '/lines_of_code.dat', 'w') - for stamp in sorted(data.changes_by_date.keys()): - fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines'])) - fg.close() + lines_html.addChart(lines_by_month_config, name='chartLinesByMonth', title='Lines of Code by Month', className="xl:col-span-6") - # Weekly activity + + + + # Lines :: Weekly activity WEEKS = 32 - lines_content.append(html_header(2, 'Weekly activity')) - lines_content.append('

    Last %d weeks

    ' % WEEKS) + # lines_content.append(html_header(2, 'Weekly activity')) + # lines_content.append('

    Last %d weeks

    ' % WEEKS) # generate weeks to show (previous N weeks from now) now = datetime.datetime.now() @@ -1380,485 +1469,159 @@ class HTMLReportCreator(ReportCreator): weeks.insert(0, stampcur.strftime('%Y-%W')) stampcur -= deltaweek - # top row: commits & bar - lines_content.append('') + lines_per_weekly_serie = {"name": "LoC", "color": "#1A56DB", "data": []} for i in range(0, WEEKS): - commits = 0 - if weeks[i] in data.lineactivity_by_year_week: - commits = data.lineactivity_by_year_week[weeks[i]] - - percentage = 0 - if weeks[i] in data.lineactivity_by_year_week: - percentage = float(data.lineactivity_by_year_week[weeks[i]]) / data.lineactivity_by_year_week_peak - height = max(1, int(200 * percentage)) - lines_content.append('' % (commits, height)) + lines = data.lineactivity_by_year_week[weeks[i]] if weeks[i] in data.lineactivity_by_year_week else 0 + lines_per_weekly_serie['data'].append({"x": f'{WEEKS-i}', "y": lines}) + + lines_per_weekly_config = { + **chart_default_config, + "series": [lines_per_weekly_serie] + } - # bottom row: year/week - lines_content.append('') - for i in range(0, WEEKS): - lines_content.append('' % (WEEKS - i)) - lines_content.append('
    %d
    %s
    ') + lines_html.addChart(lines_per_weekly_config, name='chartLinesWeeklyActivity', title=f'Weekly activity Last {WEEKS} weeks', className="") - # Hour of Day - lines_content.append(html_header(2, 'Hour of Day')) + # Lines :: Hour of Day hour_of_day = data.getLineActivityByHourOfDay() - lines_content.append('') - for i in range(0, 24): - lines_content.append('' % i) - lines_content.append('\n') - fp = open(path + '/line_hour_of_day.dat', 'w') - for i in range(0, 24): - if i in hour_of_day: - r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128) - lines_content.append('' % (r, hour_of_day[i])) - fp.write('%d %d\n' % (i, hour_of_day[i])) - else: - lines_content.append('') - fp.write('%d 0\n' % i) - fp.close() - lines_content.append('\n') - totallines = data.getTotalLines() - for i in range(0, 24): - if i in hour_of_day: - r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128) - lines_content.append('' % (r, (100.0 * hour_of_day[i]) / totallines)) - else: - lines_content.append('') - lines_content.append('
    Hour%d
    Lines%d0
    %%.2f0.00
    ') - lines_content.append('Hour of Day') - fg = open(path + '/line_hour_of_day.dat', 'w') + + lines_per_hours_day_serie = {"name": "LoC", "color": "#1A56DB", "data": []} + for i in range(0, 24): - if i in hour_of_day: - fg.write('%d %d\n' % (i + 1, hour_of_day[i])) - else: - fg.write('%d 0\n' % (i + 1)) - fg.close() + lines = hour_of_day[i] if i in hour_of_day else 0 + lines_per_hours_day_serie["data"].append({"x": f'{i}', "y": lines}) + + lines_per_hours_day_config = { + **chart_default_config, + "series": [lines_per_hours_day_serie] + } + + lines_html.addChart(lines_per_hours_day_config, name='chartLinesHourOfDay', title='Hour of Day', className="") - # Day of Week - lines_content.append(html_header(2, 'Day of Week')) + # Lines :: Day of Week day_of_week = data.getLineActivityByDayOfWeek() - lines_content.append('
    ') - lines_content.append('') - fp = open(path + '/line_day_of_week.dat', 'w') + + lines_per_day_week_serie= {"name": "LoC", "color": "#1A56DB", "data": []} + for d in range(0, 7): - commits = 0 - if d in day_of_week: - commits = day_of_week[d] - fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits)) - lines_content.append('') - lines_content.append('' % (WEEKDAYS[d])) - if d in day_of_week: - lines_content.append('' % (day_of_week[d], (100.0 * day_of_week[d]) / totallines)) - else: - lines_content.append('') - lines_content.append('') - lines_content.append('
    DayTotal (%)
    %s%d (%.2f%%)0
    ') - lines_content.append('Day of Week') - fp.close() + lines = day_of_week[d] if d in day_of_week else 0 + lines_per_day_week_serie["data"].append({"x": WEEKDAYS[d], "y": lines}) + + lines_per_day_week_config = { + **chart_default_config, + "series": [lines_per_day_week_serie] + } + + lines_html.addChart(lines_per_day_week_config, name='chartLinesDayofWeek', title='Day of Week', className="xl:col-span-4") - # Hour of Week - lines_content.append(html_header(2, 'Hour of Week')) - lines_content.append('') - lines_content.append('') - for hour in range(0, 24): - lines_content.append('' % (hour)) - lines_content.append('') + # Lines :: Hour of Week + lines_hour_of_week_series = [] for weekday in range(0, 7): - lines_content.append('' % (WEEKDAYS[weekday])) + lines_hour_of_week_series.append({"name": WEEKDAYS[weekday], "data": []}) for hour in range(0, 24): try: - commits = data.lineactivity_by_hour_of_week[weekday][hour] + lines = data.lineactivity_by_hour_of_week[weekday][hour] except KeyError: - commits = 0 - if commits != 0: - lines_content.append('%d' % commits) - else: - lines_content.append('') - lines_content.append('') - - lines_content.append('
    Weekday%d
    %s
    ') + lines = 0 + + lines_hour_of_week_series[weekday]["data"].append({"x": f'{hour}', "y": lines}) - # Month of Year - lines_content.append(html_header(2, 'Month of Year')) - lines_content.append('
    ') - lines_content.append('') - fp = open (path + '/line_month_of_year.dat', 'w') - for mm in range(1, 13): - commits = 0 - if mm in data.lineactivity_by_month_of_year: - commits = data.lineactivity_by_month_of_year[mm] - lines_content.append('' % (mm, commits, (100.0 * commits) / data.getTotalLines())) - fp.write('%d %d\n' % (mm, commits)) - fp.close() - lines_content.append('
    MonthLines (%)
    %d%d (%.2f %%)
    ') - lines_content.append('Month of Year') - - # Lines by year/month - lines_content.append(html_header(2, 'Lines by year/month')) - lines_content.append('
    ') - for yymm in reversed(sorted(data.commits_by_month.keys())): - lines_content.append('' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0))) - lines_content.append('
    MonthCommitsLines addedLines removed
    %s%d%d%d
    ') - lines_content.append('Commits by year/month') - fg = open(path + '/line_commits_by_year_month.dat', 'w') - for yymm in sorted(data.commits_by_month.keys()): - fg.write('%s %s\n' % (yymm, data.lines_added_by_month.get(yymm, 0) + data.lines_removed_by_month.get(yymm, 0))) - fg.close() - - # Lines by year - lines_content.append(html_header(2, 'Lines by Year')) - lines_content.append('
    ') - for yy in reversed(sorted(data.commits_by_year.keys())): - lines_content.append('' % (yy, data.commits_by_year.get(yy,0), (100.0 * data.commits_by_year.get(yy,0)) / data.getTotalCommits(), data.lines_added_by_year.get(yy,0), data.lines_removed_by_year.get(yy,0))) - lines_content.append('
    YearCommits (% of all)Lines addedLines removed
    %s%d (%.2f%%)%d%d
    ') - lines_content.append('Commits by Year') - fg = open(path + '/line_commits_by_year.dat', 'w') - for yy in sorted(data.commits_by_year.keys()): - fg.write('%d %d\n' % (yy, data.lines_added_by_year.get(yy,0) + data.lines_removed_by_year.get(yy,0))) - fg.close() + lines_hour_of_week_series.reverse() - # Commits by timezone - lines_content.append(html_header(2, 'Commits by Timezone')) - lines_content.append('') - lines_content.append('') - max_commits_on_tz = max(data.commits_by_timezone.values()) - for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)): - commits = data.commits_by_timezone[i] - r = 127 + int((float(commits) / max_commits_on_tz) * 128) - lines_content.append('' % (i, r, commits)) - lines_content.append('
    TimezoneCommits
    %s%d
    ') - - lines_html.create(lines_content) + lines_hour_of_week_config = { + "series": lines_hour_of_week_series, + "chart": {**chart_default_config["chart"], "type": 'heatmap'}, + "dataLabels": chart_default_config["dataLabels"], + "colors": ["#3C50E0"], + "xaxis": chart_default_config["xaxis"], + "yaxis": chart_default_config["yaxis"], + } - ### - # tags.html - tags_content = [] - tags_html = html.HTML(path=f'{path}/tags.html', title='Tags', version= getversion()) + lines_html.addChart(lines_hour_of_week_config, name='chartLinesHourOfWeek', title='Hour of Week', className="xl:col-span-8") + - tags_content.append('
    ') - tags_content.append('
    Total tags
    %d
    ' % len(data.tags)) - if len(data.tags) > 0: - tags_content.append('
    Average commits per tag
    %.2f
    ' % (1.0 * data.getTotalCommits() / len(data.tags))) - tags_content.append('
    ') - tags_content.append('') - tags_content.append('') - # sort the tags by date desc - tags_sorted_by_date_desc = [el[1] for el in reversed(sorted([(el[1]['date'], el[0]) for el in list(data.tags.items())]))] - for tag in tags_sorted_by_date_desc: - authorinfo = [] - self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors']) - for i in reversed(self.authors_by_commits): - authorinfo.append('%s (%d)' % (i, data.tags[tag]['authors'][i])) - tags_content.append('' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo))) - tags_content.append('
    NameDateCommitsAuthors
    %s%s%d%s
    ') + # Lines :: Month of Year + lines_per_month_of_year_series= {"name": "LoC", "color": "#1A56DB", "data": []} + + for mm in range(1, 13): + lines = data.lineactivity_by_month_of_year[mm] if mm in data.lineactivity_by_month_of_year else 0 + lines_per_month_of_year_series["data"].append({"x": f'{mm}', "y": lines, "percentage": (100.0 * lines) /data.getTotalLines()}) - tags_html.create(tags_content) + lines_per_month_of_year_config = { + **chart_default_config, + "series": [lines_per_month_of_year_series] + } - self.createGraphs(path) - - def createGraphs(self, path): - print('Generating graphs...') - - # hour of day - f = open(path + '/hour_of_day.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'hour_of_day.png' -unset key -set xrange [0.5:24.5] -set yrange [0:] -set xtics 4 -set grid y -set ylabel "Commits" -plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() + lines_html.addChart(lines_per_month_of_year_config, name='chartLinesMonthOfYear', title='Month of Year', className="xl:col-span-5") - # day of week - f = open(path + '/day_of_week.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'day_of_week.png' -unset key -set xrange [0.5:7.5] -set yrange [0:] -set xtics 1 -set grid y -set ylabel "Commits" -plot 'day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid -""") - f.close() - # Domains - f = open(path + '/domains.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'domains.png' -unset key -unset xtics -set yrange [0:] -set grid y -set ylabel "Commits" -plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1 -""") - f.close() + # Lines :: Lines by year/month + lines_per_year_month_serie = [] + + for yymm in sorted(data.commits_by_month.keys()): + lines_per_year_month_serie.append({"x": f'{yymm}', "y": data.lines_added_by_month.get(yymm, 0) + data.lines_removed_by_month.get(yymm, 0)}) - # Month of Year - f = open(path + '/month_of_year.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'month_of_year.png' -unset key -set xrange [0.5:12.5] -set yrange [0:] -set xtics 1 -set grid y -set ylabel "Commits" -plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() + lines_per_year_month_config = { + **chart_default_config, + "series": [{ + "name": "Commits", + "color": "#1A56DB", + "data": lines_per_year_month_serie}], + "xaxis": { + **chart_default_config["xaxis"], + "labels": { + **activity_per_year_month_config["xaxis"]["labels"] , + "show" : False + }}} - # commits_by_year_month - f = open(path + '/commits_by_year_month.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'commits_by_year_month.png' -unset key -set yrange [0:] -set xdata time -set timefmt "%Y-%m" -set format x "%Y-%m" -set xtics rotate -set bmargin 5 -set grid y -set ylabel "Commits" -plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() + lines_html.addChart(lines_per_year_month_config, name='chartCommitsByYearMonth', title='Lines by year/month', className="xl:col-span-7") - # commits_by_year - f = open(path + '/commits_by_year.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'commits_by_year.png' -unset key -set yrange [0:] -set xtics 1 rotate -set grid y -set ylabel "Commits" -set yrange [0:] -plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() - # Files by date - f = open(path + '/files_by_date.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'files_by_date.png' -unset key -set yrange [0:] -set xdata time -set timefmt "%Y-%m-%d" -set format x "%Y-%m-%d" -set grid y -set ylabel "Files" -set xtics rotate -set ytics autofreq -set bmargin 6 -plot 'files_by_date.dat' using 1:2 w steps -""") - f.close() + # Lines :: Lines by year + lines_by_year_serie= {"name": "Lines", "color": "#1A56DB", "data": []} - # Lines of Code - f = open(path + '/lines_of_code.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'lines_of_code.png' -unset key -set yrange [0:] -set xdata time -set timefmt "%s" -set format x "%Y-%m-%d" -set grid y -set ylabel "Lines" -set xtics rotate -set bmargin 6 -plot 'lines_of_code.dat' using 1:2 w lines -""") - f.close() + for yy in sorted(data.commits_by_year.keys()): + lines_by_year_serie["data"].append({"x": f'{yy}', "y": data.lines_added_by_year.get(yy,0) - data.lines_removed_by_year.get(yy,0)}) - # Lines of Code Added per author - f = open(path + '/lines_of_code_by_author.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set terminal png transparent size 640,480 -set output 'lines_of_code_by_author.png' -set key left top -set yrange [0:] -set xdata time -set timefmt "%s" -set format x "%Y-%m-%d" -set grid y -set ylabel "Lines" -set xtics rotate -set bmargin 6 -plot """ -) - i = 1 - plots = [] - for a in self.authors_to_plot: - i = i + 1 - author = a.replace("\"", "\\\"").replace("`", "") - plots.append("""'lines_of_code_by_author.dat' using 1:%d title "%s" w lines""" % (i, author)) - f.write(", ".join(plots)) - f.write('\n') + lines_by_year_config = { + **chart_default_config, + "series": [lines_by_year_serie] + } - f.close() + lines_html.addChart(lines_by_year_config, name='chartLinesByYear', title='Lines by Year', className="xl:col-span-6") - # hour of day - f = open(path + '/line_hour_of_day.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'line_hour_of_day.png' -unset key -set xrange [0.5:24.5] -set xtics 4 -set grid y -set ylabel "Lines" -plot 'line_hour_of_day.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() - # day of week - f = open(path + '/line_day_of_week.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'line_day_of_week.png' -unset key -set xrange [0.5:7.5] -set xtics 1 -set grid y -set ylabel "Lines" -plot 'line_day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid -""") - f.close() + lines_html.add('
    ') - # Domains -# f = open(path + '/domains.plot', 'w') -# f.write(GNUPLOT_COMMON) -# f.write( -#""" -#set output 'domains.png' -#unset key -#unset xtics -#set yrange [0:] -#set grid y -#set ylabel "Commits" -#plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1 -#""") -# f.close() - - # Month of Year - f = open(path + '/line_month_of_year.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'line_month_of_year.png' -unset key -set xrange [0.5:12.5] -set xtics 1 -set grid y -set ylabel "Lines" -plot 'line_month_of_year.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() + lines_html.create(lines_content) - # commits_by_year_month - f = open(path + '/line_commits_by_year_month.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'line_commits_by_year_month.png' -unset key -set xdata time -set timefmt "%Y-%m" -set format x "%Y-%m" -set xtics rotate -set bmargin 5 -set grid y -set ylabel "Lines" -plot 'line_commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() + ### + # tags.html + tags_html = html.HTML(path=f'{path}/tags.html', title='Tags', version= getversion()) - # commits_by_year - f = open(path + '/line_commits_by_year.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set output 'line_commits_by_year.png' -unset key -set xtics 1 rotate -set grid y -set ylabel "Lines" -set yrange [0:] -plot 'line_commits_by_year.dat' using 1:2:(0.5) w boxes fs solid -""") - f.close() + tags_html.add('
    ') + tags_html.cardItemStat(title='Total tags', count=len(data.tags)) + tags_html.cardItemStat(title='Average commits per tag', count=f'{(1.0 * data.getTotalCommits() / len(data.tags)):.2f}') + tags_html.add('
    ') - # Commits per author - f = open(path + '/commits_by_author.plot', 'w') - f.write(GNUPLOT_COMMON) - f.write( -""" -set terminal png transparent size 640,480 -set output 'commits_by_author.png' -set key left top -set yrange [0:] -set xdata time -set timefmt "%s" -set format x "%Y-%m-%d" -set grid y -set ylabel "Commits" -set xtics rotate -set bmargin 6 -plot """ -) - i = 1 - plots = [] - for a in self.authors_to_plot: - i = i + 1 - author = a.replace("\"", "\\\"").replace("`", "") - plots.append("""'commits_by_author.dat' using 1:%d title "%s" w lines""" % (i, author)) - f.write(", ".join(plots)) - f.write('\n') + tags_table_content = [''] + tags_table_content.append('') + # sort the tags by date desc + tags_sorted_by_date_desc = [el[1] for el in reversed(sorted([(el[1]['date'], el[0]) for el in list(data.tags.items())]))] + for tag in tags_sorted_by_date_desc: + authorinfo = [] + self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors']) + for i in reversed(self.authors_by_commits): + authorinfo.append('%s (%d)' % (i, data.tags[tag]['authors'][i])) + tags_table_content.append('' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo))) + tags_table_content.append('
    NameDateCommitsAuthors
    %s%s%d%s
    ') - f.close() + tags_html.addCard(tags_table_content, title='Tags information') - os.chdir(path) - files = glob.glob(path + '/*.plot') - for f in files: - out = getpipeoutput([gnuplot_cmd + ' "%s"' % f]) - if len(out) > 0: - print(out) + tags_html.create() + def usage(): text = """ @@ -1907,10 +1670,6 @@ class GitStats: print('FATAL: Output path is not a directory or does not exist') sys.exit(1) - if not getgnuplotversion(): - print('gnuplot not found') - sys.exit(1) - print('Output path: %s' % outputpath) cachefile = os.path.join(outputpath, 'gitstats.cache') diff --git a/html.py b/html.py index 7416e956..c719fd0e 100644 --- a/html.py +++ b/html.py @@ -10,8 +10,12 @@ def __init__(self, path='/index.html', title=None, styles='gitstats.css', versio self.title = title self.styles = styles self.version = version + self.content = [] - def create(self, content): + def add(self, content): + self.content.append(content) + + def create(self, content = []): f = open(self.path, 'w') head = self.getHeader(title=self.title) body = self.getBody(content, title=self.title) @@ -42,6 +46,9 @@ def getHeader(self, title: str = None) -> str: def getBody(self, content: List[str], title: str) -> str: sidebar = self.getSideBar() topBar = self.getTopBar(title=title) + if len(self.content) > 0 : + content = [*self.content, *content] + content = '\n'.join(content) return f'''
    ''' - return f''' + self.add(f'''
    @@ -130,7 +137,7 @@ def tilesItemStat(self, title: str = '', info: str = '', icon: str = None, stat:
    -''' +''') def cardItemStat(self, count: str = '$3.456K', title: str = 'Total views', stat: str = None, arrow: str = 'up', icon=None) -> str: @@ -152,7 +159,7 @@ def cardItemStat(self, count: str = '$3.456K', title: str = 'Total views', stat: ''' - return f''' + self.add(f'''
    {icon} @@ -166,7 +173,7 @@ def cardItemStat(self, count: str = '$3.456K', title: str = 'Total views', stat: {stat_html}
    -
    ''' +''') def getSideBar(self) -> str: menu = ''.join([f''' @@ -411,7 +418,7 @@ def getTopBar(self, title: str) -> str: def addChart(self, config: Dict, name: str = None, title: str = 'Chart', className: str=None): if name is None: name = f'chart_{int(time.time())}' - return self.addCard([f'
    '], title=title, className=className, extra=f''' + self.addCard([f'
    '], title=title, className=className, extra=f'''