12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970
  1. #!/usr/bin/env python3
  2. # Copyright (c) 2007-2014 Heikki Hokkanen <hoxu@users.sf.net> & others (see doc/AUTHOR)
  3. # GPLv2 / GPLv3
  4. import datetime
  5. import getopt
  6. import glob
  7. import json
  8. import os
  9. import pickle
  10. import platform
  11. import re
  12. import shutil
  13. import subprocess
  14. import sys
  15. import time
  16. import zlib
  17. import multiprocessing
  18. if sys.version_info < (2, 6):
  19. print >> sys.stderr, "Python 2.6 or higher is required for gitstats"
  20. sys.exit(1)
  21. from multiprocessing import Pool
  22. os.environ['LC_ALL'] = 'C'
  23. GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n'
  24. ON_LINUX = (platform.system() == 'Linux')
  25. WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
  26. JSONFILE = 'gitstats.json'
  27. exectime_internal = 0.0
  28. exectime_external = 0.0
  29. time_start = time.time()
  30. # By default, gnuplot is searched from path, but can be overridden with the
  31. # environment variable "GNUPLOT"
  32. gnuplot_cmd = 'gnuplot'
  33. if 'GNUPLOT' in os.environ:
  34. gnuplot_cmd = os.environ['GNUPLOT']
  35. conf = {
  36. 'max_domains': 10,
  37. 'max_ext_length': 10,
  38. 'style': 'gitstats.css',
  39. 'max_authors': 20,
  40. 'authors_top': 5,
  41. 'commit_begin': '',
  42. 'commit_end': 'HEAD',
  43. 'linear_linestats': 1,
  44. 'project_name': '',
  45. 'processes': multiprocessing.cpu_count(),
  46. 'start_date': '',
  47. 'end_date': '',
  48. 'excluded_authors': [],
  49. 'excluded_prefixes': []
  50. }
  51. def getpipeoutput(cmds, quiet = False):
  52. global exectime_external
  53. start = time.time()
  54. if not quiet and ON_LINUX and os.isatty(1):
  55. print('>> ' + ' | '.join(cmds))
  56. sys.stdout.flush()
  57. p = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
  58. processes=[p]
  59. for x in cmds[1:]:
  60. p = subprocess.Popen(x, stdin = p.stdout, stdout = subprocess.PIPE, shell = True)
  61. processes.append(p)
  62. output = (p.communicate()[0]).decode("utf-8")
  63. for p in processes:
  64. p.wait()
  65. end = time.time()
  66. if not quiet:
  67. if ON_LINUX and os.isatty(1):
  68. print('\r')
  69. print('[%.5f] >> %s' % (end - start, ' | '.join(cmds)))
  70. exectime_external += (end - start)
  71. return output.rstrip('\n')
  72. def getlogrange(defaultrange = 'HEAD', end_only = True):
  73. commit_range = getcommitrange(defaultrange, end_only)
  74. datesel = ''
  75. if len(conf['start_date']) > 0:
  76. datesel = '--since="%s" %s' % (conf['start_date'], datesel)
  77. if len(conf['end_date']) > 0:
  78. datesel = '--until="%s" %s' % (conf['end_date'], datesel)
  79. if (len(datesel) > 0):
  80. commit_range = '%s "%s"' % (datesel, commit_range)
  81. return commit_range
  82. def getcommitrange(defaultrange = 'HEAD', end_only = False):
  83. if len(conf['commit_end']) > 0:
  84. if end_only or len(conf['commit_begin']) == 0:
  85. return conf['commit_end']
  86. return '%s..%s' % (conf['commit_begin'], conf['commit_end'])
  87. return defaultrange
  88. def getkeyssortedbyvalues(dict):
  89. return [el[1] for el in sorted([(el[1], el[0]) for el in list(dict.items())])]
  90. # dict['author'] = { 'commits': 512 } - ...key(dict, 'commits')
  91. def getkeyssortedbyvaluekey(d, key):
  92. return [el[1] for el in sorted([(d[el][key], el) for el in list(d.keys())])]
  93. def getstatsummarycounts(line):
  94. numbers = re.findall('\d+', line)
  95. if len(numbers) == 1:
  96. # neither insertions nor deletions: may probably only happen for "0 files changed"
  97. numbers.append(0);
  98. numbers.append(0);
  99. elif len(numbers) == 2 and line.find('(+)') != -1:
  100. numbers.append(0); # only insertions were printed on line
  101. elif len(numbers) == 2 and line.find('(-)') != -1:
  102. numbers.insert(1, 0); # only deletions were printed on line
  103. return numbers
  104. VERSION = 0
  105. def getversion():
  106. global VERSION
  107. if VERSION == 0:
  108. gitstats_repo = os.path.dirname(os.path.abspath(__file__))
  109. VERSION = getpipeoutput(["git --git-dir=%s/.git --work-tree=%s rev-parse --short %s" %
  110. (gitstats_repo, gitstats_repo, getcommitrange('HEAD').split('\n')[0])])
  111. return VERSION
  112. def getgitversion():
  113. return getpipeoutput(['git --version']).split('\n')[0]
  114. def getgnuplotversion():
  115. return getpipeoutput(['%s --version' % gnuplot_cmd]).split('\n')[0]
  116. def getnumoffilesfromrev(time_rev):
  117. """
  118. Get number of files changed in commit
  119. """
  120. time, rev = time_rev
  121. return (int(time), rev, int(getpipeoutput(['git ls-tree -r --name-only "%s"' % rev, 'wc -l']).split('\n')[0]))
  122. def getnumoflinesinblob(ext_blob):
  123. """
  124. Get number of lines in blob
  125. """
  126. ext, blob_id = ext_blob
  127. return (ext, blob_id, int(getpipeoutput(['git cat-file blob %s' % blob_id, 'wc -l']).split()[0]))
  128. class DataCollector:
  129. """Manages data collection from a revision control repository."""
  130. def __init__(self):
  131. self.stamp_created = time.time()
  132. self.cache = {}
  133. self.total_branches = 0
  134. self.total_tags = 0
  135. self.total_authors = 0
  136. self.activity_by_hour_of_day = {} # hour -> commits
  137. self.activity_by_day_of_week = {} # day -> commits
  138. self.activity_by_month_of_year = {} # month [1-12] -> commits
  139. self.activity_by_hour_of_week = {} # weekday -> hour -> commits
  140. self.activity_by_hour_of_day_busiest = 0
  141. self.activity_by_hour_of_week_busiest = 0
  142. self.activity_by_year_week = {} # yy_wNN -> commits
  143. self.activity_by_year_week_peak = 0
  144. self.lineactivity_by_hour_of_day = {} # hour -> commits
  145. self.lineactivity_by_day_of_week = {} # day -> commits
  146. self.lineactivity_by_month_of_year = {} # month [1-12] -> commits
  147. self.lineactivity_by_hour_of_week = {} # weekday -> hour -> commits
  148. self.lineactivity_by_hour_of_day_busiest = 0
  149. self.lineactivity_by_hour_of_week_busiest = 0
  150. self.lineactivity_by_year_week = {} # yy_wNN -> commits
  151. self.lineactivity_by_year_week_peak = 0
  152. self.changes_by_date_by_author = {} # stamp -> author -> lines_added
  153. self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp, last_active_day, active_days, lines_added, lines_removed}
  154. self.total_commits = 0
  155. self.total_files = 0
  156. self.authors_by_commits = 0
  157. # domains
  158. self.domains = {} # domain -> commits
  159. # author of the month
  160. self.author_of_month = {} # month -> author -> commits
  161. self.author_of_year = {} # year -> author -> commits
  162. self.commits_by_month = {} # month -> commits
  163. self.commits_by_year = {} # year -> commits
  164. self.lines_added_by_month = {} # month -> lines added
  165. self.lines_added_by_year = {} # year -> lines added
  166. self.lines_removed_by_month = {} # month -> lines removed
  167. self.lines_removed_by_year = {} # year -> lines removed
  168. self.first_commit_stamp = 0
  169. self.last_commit_stamp = 0
  170. self.last_active_day = None
  171. self.active_days = set()
  172. # lines
  173. self.total_lines = 0
  174. self.total_lines_added = 0
  175. self.total_lines_removed = 0
  176. # size
  177. self.total_size = 0
  178. # timezone
  179. self.commits_by_timezone = {} # timezone -> commits
  180. # tags
  181. self.tags = {}
  182. self.files_by_stamp = {} # stamp -> files
  183. # extensions
  184. self.extensions = {} # extension -> files, lines
  185. # line statistics
  186. self.changes_by_date = {} # stamp -> { files, ins, del }
  187. ##
  188. # This should be the main function to extract data from the repository.
  189. def collect(self, dir):
  190. self.dir = dir
  191. if len(conf['project_name']) == 0:
  192. self.projectname = os.path.basename(os.path.abspath(dir))
  193. else:
  194. self.projectname = conf['project_name']
  195. ##
  196. # Load cacheable data
  197. def loadCache(self, cachefile):
  198. if not os.path.exists(cachefile):
  199. return
  200. print('Loading cache...')
  201. f = open(cachefile, 'rb')
  202. try:
  203. self.cache = pickle.loads(zlib.decompress(f.read()))
  204. except:
  205. # temporary hack to upgrade non-compressed caches
  206. f.seek(0)
  207. self.cache = pickle.load(f)
  208. f.close()
  209. ##
  210. # Produce any additional statistics from the extracted data.
  211. def refine(self):
  212. pass
  213. ##
  214. # : get a dictionary of author
  215. def getAuthorInfo(self, author):
  216. return None
  217. def getActivityByDayOfWeek(self):
  218. return {}
  219. def getActivityByHourOfDay(self):
  220. return {}
  221. def getLineActivityByDayOfWeek(self):
  222. return {}
  223. def getLineActivityByHourOfDay(self):
  224. return {}
  225. # : get a dictionary of domains
  226. def getDomainInfo(self, domain):
  227. return None
  228. ##
  229. # Get a list of authors
  230. def getAuthors(self):
  231. return []
  232. def getFirstCommitDate(self):
  233. return datetime.datetime.now()
  234. def getLastCommitDate(self):
  235. return datetime.datetime.now()
  236. def getStampCreated(self):
  237. return self.stamp_created
  238. def getTags(self):
  239. return []
  240. def getTotalAuthors(self):
  241. return -1
  242. def getTotalCommits(self):
  243. return -1
  244. def getTotalFiles(self):
  245. return -1
  246. def getTotalLines(self):
  247. return -1
  248. def getTotalLOC(self):
  249. return -1
  250. ##
  251. # Save cacheable data
  252. def saveCache(self, cachefile):
  253. print('Saving cache...')
  254. tempfile = cachefile + '.tmp'
  255. f = open(tempfile, 'wb')
  256. #pickle.dump(self.cache, f)
  257. data = zlib.compress(pickle.dumps(self.cache))
  258. f.write(data)
  259. f.close()
  260. try:
  261. os.remove(cachefile)
  262. except OSError:
  263. pass
  264. os.rename(tempfile, cachefile)
  265. class GitDataCollector(DataCollector):
  266. def collect(self, dir):
  267. DataCollector.collect(self, dir)
  268. self.total_authors += int(getpipeoutput(['git shortlog -s %s' % getlogrange(), 'wc -l']))
  269. self.total_branches += int(getpipeoutput(['git branch -r', 'wc -l']))
  270. self.total_tags += int(getpipeoutput(['git tag', 'wc -l']))
  271. #self.total_lines = int(getoutput('git-ls-files -z |xargs -0 cat |wc -l'))
  272. # tags
  273. lines = getpipeoutput(['git show-ref --tags']).split('\n')
  274. for line in lines:
  275. if len(line) == 0:
  276. continue
  277. (hash, tag) = line.split(' ')
  278. tag = tag.replace('refs/tags/', '')
  279. output = getpipeoutput(['git log "%s" --pretty=format:"%%at %%aN" -n 1' % hash])
  280. if len(output) > 0:
  281. parts = output.split(' ')
  282. stamp = 0
  283. try:
  284. stamp = int(parts[0])
  285. except ValueError:
  286. stamp = 0
  287. self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), 'commits': 0, 'authors': {} }
  288. # collect info on tags, starting from latest
  289. tags_sorted_by_date_desc = list(map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), self.tags.items())))))
  290. prev = None
  291. for tag in reversed(tags_sorted_by_date_desc):
  292. cmd = 'git shortlog -s "%s"' % tag
  293. if prev != None:
  294. cmd += ' "^%s"' % prev
  295. output = getpipeoutput([cmd])
  296. if len(output) == 0:
  297. continue
  298. prev = tag
  299. for line in output.split('\n'):
  300. parts = re.split('\s+', line, 2)
  301. commits = int(parts[1])
  302. author = parts[2]
  303. if author in conf["excluded_authors"]:
  304. continue
  305. self.tags[tag]['commits'] += commits
  306. self.tags[tag]['authors'][author] = commits
  307. # Collect revision statistics
  308. # Outputs "<stamp> <date> <time> <timezone> <author> '<' <mail> '>'"
  309. lines = getpipeoutput(['git rev-list --pretty=format:"%%at %%ai %%aN <%%aE>" %s' % getlogrange('HEAD'), 'grep -v ^commit']).split('\n')
  310. for line in lines:
  311. parts = line.split(' ', 4)
  312. author = ''
  313. try:
  314. stamp = int(parts[0])
  315. except ValueError:
  316. stamp = 0
  317. timezone = parts[3]
  318. author, mail = parts[4].split('<', 1)
  319. author = author.rstrip()
  320. if author in conf["excluded_authors"]:
  321. continue
  322. mail = mail.rstrip('>')
  323. domain = '?'
  324. if mail.find('@') != -1:
  325. domain = mail.rsplit('@', 1)[1]
  326. date = datetime.datetime.fromtimestamp(float(stamp))
  327. # First and last commit stamp (may be in any order because of cherry-picking and patches)
  328. if stamp > self.last_commit_stamp:
  329. self.last_commit_stamp = stamp
  330. if self.first_commit_stamp == 0 or stamp < self.first_commit_stamp:
  331. self.first_commit_stamp = stamp
  332. # activity
  333. # hour
  334. hour = date.hour
  335. self.activity_by_hour_of_day[hour] = self.activity_by_hour_of_day.get(hour, 0) + 1
  336. # most active hour?
  337. if self.activity_by_hour_of_day[hour] > self.activity_by_hour_of_day_busiest:
  338. self.activity_by_hour_of_day_busiest = self.activity_by_hour_of_day[hour]
  339. # day of week
  340. day = date.weekday()
  341. self.activity_by_day_of_week[day] = self.activity_by_day_of_week.get(day, 0) + 1
  342. # domain stats
  343. if domain not in self.domains:
  344. self.domains[domain] = {}
  345. # commits
  346. self.domains[domain]['commits'] = self.domains[domain].get('commits', 0) + 1
  347. # hour of week
  348. if day not in self.activity_by_hour_of_week:
  349. self.activity_by_hour_of_week[day] = {}
  350. self.activity_by_hour_of_week[day][hour] = self.activity_by_hour_of_week[day].get(hour, 0) + 1
  351. # most active hour?
  352. if self.activity_by_hour_of_week[day][hour] > self.activity_by_hour_of_week_busiest:
  353. self.activity_by_hour_of_week_busiest = self.activity_by_hour_of_week[day][hour]
  354. # month of year
  355. month = date.month
  356. self.activity_by_month_of_year[month] = self.activity_by_month_of_year.get(month, 0) + 1
  357. # yearly/weekly activity
  358. yyw = date.strftime('%Y-%W')
  359. self.activity_by_year_week[yyw] = self.activity_by_year_week.get(yyw, 0) + 1
  360. if self.activity_by_year_week_peak < self.activity_by_year_week[yyw]:
  361. self.activity_by_year_week_peak = self.activity_by_year_week[yyw]
  362. # author stats
  363. if author not in self.authors:
  364. self.authors[author] = {}
  365. # commits, note again that commits may be in any date order because of cherry-picking and patches
  366. if 'last_commit_stamp' not in self.authors[author]:
  367. self.authors[author]['last_commit_stamp'] = stamp
  368. if stamp > self.authors[author]['last_commit_stamp']:
  369. self.authors[author]['last_commit_stamp'] = stamp
  370. if 'first_commit_stamp' not in self.authors[author]:
  371. self.authors[author]['first_commit_stamp'] = stamp
  372. if stamp < self.authors[author]['first_commit_stamp']:
  373. self.authors[author]['first_commit_stamp'] = stamp
  374. # author of the month/year
  375. yymm = date.strftime('%Y-%m')
  376. if yymm in self.author_of_month:
  377. self.author_of_month[yymm][author] = self.author_of_month[yymm].get(author, 0) + 1
  378. else:
  379. self.author_of_month[yymm] = {}
  380. self.author_of_month[yymm][author] = 1
  381. self.commits_by_month[yymm] = self.commits_by_month.get(yymm, 0) + 1
  382. yy = date.year
  383. if yy in self.author_of_year:
  384. self.author_of_year[yy][author] = self.author_of_year[yy].get(author, 0) + 1
  385. else:
  386. self.author_of_year[yy] = {}
  387. self.author_of_year[yy][author] = 1
  388. self.commits_by_year[yy] = self.commits_by_year.get(yy, 0) + 1
  389. # authors: active days
  390. yymmdd = date.strftime('%Y-%m-%d')
  391. if 'last_active_day' not in self.authors[author]:
  392. self.authors[author]['last_active_day'] = yymmdd
  393. self.authors[author]['active_days'] = set([yymmdd])
  394. elif yymmdd != self.authors[author]['last_active_day']:
  395. self.authors[author]['last_active_day'] = yymmdd
  396. self.authors[author]['active_days'].add(yymmdd)
  397. # project: active days
  398. if yymmdd != self.last_active_day:
  399. self.last_active_day = yymmdd
  400. self.active_days.add(yymmdd)
  401. # timezone
  402. self.commits_by_timezone[timezone] = self.commits_by_timezone.get(timezone, 0) + 1
  403. # outputs "<stamp> <files>" for each revision
  404. revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T %%an" %s' % getlogrange('HEAD'), 'grep -v ^commit']).strip().split('\n')
  405. lines = []
  406. revs_to_read = []
  407. time_rev_count = []
  408. #Look up rev in cache and take info from cache if found
  409. #If not append rev to list of rev to read from repo
  410. for revline in revlines:
  411. _revline = revline.split(' ')
  412. time, rev = _revline[:2]
  413. author = ' '.join(_revline[2:])
  414. if author in conf["excluded_authors"]:
  415. continue
  416. #if cache empty then add time and rev to list of new rev's
  417. #otherwise try to read needed info from cache
  418. if 'files_in_tree' not in self.cache.keys():
  419. revs_to_read.append((time,rev))
  420. continue
  421. if rev in self.cache['files_in_tree'].keys():
  422. lines.append('%d %d' % (int(time), self.cache['files_in_tree'][rev]))
  423. else:
  424. revs_to_read.append((time,rev))
  425. #Read revisions from repo
  426. pool = Pool(processes=conf['processes'])
  427. time_rev_count = pool.map(getnumoffilesfromrev, revs_to_read)
  428. pool.terminate()
  429. pool.join()
  430. #Update cache with new revisions and append then to general list
  431. for (time, rev, count) in time_rev_count:
  432. if 'files_in_tree' not in self.cache:
  433. self.cache['files_in_tree'] = {}
  434. self.cache['files_in_tree'][rev] = count
  435. lines.append('%d %d' % (int(time), count))
  436. self.total_commits += len(lines)
  437. for line in lines:
  438. parts = line.split(' ')
  439. if len(parts) != 2:
  440. continue
  441. (stamp, files) = parts[0:2]
  442. try:
  443. self.files_by_stamp[int(stamp)] = int(files)
  444. except ValueError:
  445. print('Warning: failed to parse line "%s"' % line)
  446. # extensions and size of files
  447. lines = getpipeoutput(['git ls-tree -r -l -z %s' % getcommitrange('HEAD', end_only = True)]).split('\000')
  448. blobs_to_read = []
  449. for line in lines:
  450. if len(line) == 0:
  451. continue
  452. parts = re.split('\s+', line, 4)
  453. if parts[0] == '160000' and parts[3] == '-':
  454. # skip submodules
  455. continue
  456. blob_id = parts[2]
  457. size = int(parts[3])
  458. fullpath = parts[4]
  459. exclude = False
  460. for path in conf["excluded_prefixes"]:
  461. if fullpath.startswith(path):
  462. exclude = True
  463. break
  464. if exclude:
  465. continue
  466. self.total_size += size
  467. self.total_files += 1
  468. filename = fullpath.split('/')[-1] # strip directories
  469. if filename.find('.') == -1 or filename.rfind('.') == 0:
  470. ext = ''
  471. else:
  472. ext = filename[(filename.rfind('.') + 1):]
  473. if len(ext) > conf['max_ext_length']:
  474. ext = ''
  475. if ext not in self.extensions:
  476. self.extensions[ext] = {'files': 0, 'lines': 0}
  477. self.extensions[ext]['files'] += 1
  478. #if cache empty then add ext and blob id to list of new blob's
  479. #otherwise try to read needed info from cache
  480. if 'lines_in_blob' not in self.cache.keys():
  481. blobs_to_read.append((ext,blob_id))
  482. continue
  483. if blob_id in self.cache['lines_in_blob'].keys():
  484. self.extensions[ext]['lines'] += self.cache['lines_in_blob'][blob_id]
  485. else:
  486. blobs_to_read.append((ext,blob_id))
  487. #Get info abount line count for new blob's that wasn't found in cache
  488. pool = Pool(processes=conf['processes'])
  489. ext_blob_linecount = pool.map(getnumoflinesinblob, blobs_to_read)
  490. pool.terminate()
  491. pool.join()
  492. #Update cache and write down info about number of number of lines
  493. for (ext, blob_id, linecount) in ext_blob_linecount:
  494. if 'lines_in_blob' not in self.cache:
  495. self.cache['lines_in_blob'] = {}
  496. self.cache['lines_in_blob'][blob_id] = linecount
  497. self.extensions[ext]['lines'] += self.cache['lines_in_blob'][blob_id]
  498. # line statistics
  499. # outputs:
  500. # N files changed, N insertions (+), N deletions(-)
  501. # <stamp> <author>
  502. self.changes_by_date = {} # stamp -> { files, ins, del }
  503. # computation of lines of code by date is better done
  504. # on a linear history.
  505. extra = ''
  506. if conf['linear_linestats']:
  507. extra = '--first-parent -m'
  508. lines = getpipeoutput(['git log --shortstat %s --pretty=format:"%%at %%aN" %s' % (extra, getlogrange('HEAD'))]).split('\n')
  509. lines.reverse()
  510. files = 0; inserted = 0; deleted = 0; total_lines = 0
  511. author = None
  512. last_line = ""
  513. for line in lines:
  514. if len(line) == 0:
  515. continue
  516. # <stamp> <author>
  517. if re.search('files? changed', line) == None:
  518. pos = line.find(' ')
  519. if pos != -1:
  520. try:
  521. (stamp, author) = (int(line[:pos]), line[pos+1:])
  522. if author not in conf["excluded_authors"]:
  523. self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
  524. date = datetime.datetime.fromtimestamp(stamp)
  525. yymm = date.strftime('%Y-%m')
  526. self.lines_added_by_month[yymm] = self.lines_added_by_month.get(yymm, 0) + inserted
  527. self.lines_removed_by_month[yymm] = self.lines_removed_by_month.get(yymm, 0) + deleted
  528. yy = date.year
  529. self.lines_added_by_year[yy] = self.lines_added_by_year.get(yy,0) + inserted
  530. self.lines_removed_by_year[yy] = self.lines_removed_by_year.get(yy, 0) + deleted
  531. # lineactivity
  532. # hour
  533. hour = date.hour
  534. self.lineactivity_by_hour_of_day[hour] = self.lineactivity_by_hour_of_day.get(hour, 0) + inserted + deleted
  535. # most active hour?
  536. if self.lineactivity_by_hour_of_day[hour] > self.lineactivity_by_hour_of_day_busiest:
  537. self.lineactivity_by_hour_of_day_busiest = self.lineactivity_by_hour_of_day[hour]
  538. # day of week
  539. day = date.weekday()
  540. self.lineactivity_by_day_of_week[day] = self.lineactivity_by_day_of_week.get(day, 0) + inserted + deleted
  541. # domain stats
  542. #if domain not in self.domains:
  543. #self.domains[domain] = {}
  544. # lines
  545. #self.domains[domain]['lines'] = self.domains[domain].get('lines', 0) + 1
  546. # hour of week
  547. if day not in self.lineactivity_by_hour_of_week:
  548. self.lineactivity_by_hour_of_week[day] = {}
  549. self.lineactivity_by_hour_of_week[day][hour] = self.lineactivity_by_hour_of_week[day].get(hour, 0) + inserted + deleted
  550. # most active hour?
  551. if self.lineactivity_by_hour_of_week[day][hour] > self.lineactivity_by_hour_of_week_busiest:
  552. self.lineactivity_by_hour_of_week_busiest = self.lineactivity_by_hour_of_week[day][hour]
  553. # month of year
  554. month = date.month
  555. self.lineactivity_by_month_of_year[month] = self.lineactivity_by_month_of_year.get(month, 0) + inserted + deleted
  556. # yearly/weekly activity
  557. yyw = date.strftime('%Y-%W')
  558. self.lineactivity_by_year_week[yyw] = self.lineactivity_by_year_week.get(yyw, 0) + inserted + deleted
  559. if self.lineactivity_by_year_week_peak < self.lineactivity_by_year_week[yyw]:
  560. self.lineactivity_by_year_week_peak = self.lineactivity_by_year_week[yyw]
  561. files, inserted, deleted = 0, 0, 0
  562. numbers = getstatsummarycounts(last_line)
  563. if len(numbers) == 3:
  564. (files, inserted, deleted) = [int(el) for el in numbers]
  565. total_lines += inserted
  566. total_lines -= deleted
  567. self.total_lines_added += inserted
  568. self.total_lines_removed += deleted
  569. else:
  570. print('Warning: failed to handle line "%s"' % line)
  571. (files, inserted, deleted) = (0, 0, 0)
  572. except ValueError:
  573. print('Warning: unexpected line "%s"' % line)
  574. else:
  575. print('Warning: unexpected line "%s"' % line)
  576. else:
  577. last_line = line
  578. #self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
  579. self.total_lines += total_lines
  580. # Per-author statistics
  581. # defined for stamp, author only if author commited at this timestamp.
  582. # Similar to the above, but never use --first-parent
  583. # (we need to walk through every commit to know who
  584. # committed what, not just through mainline)
  585. lines = getpipeoutput(['git log --shortstat --date-order --pretty=format:"%%at %%aN" %s' % (getlogrange('HEAD'))]).split('\n')
  586. lines.reverse()
  587. files = 0; inserted = 0; deleted = 0
  588. author = None
  589. stamp = 0
  590. for line in lines:
  591. if len(line) == 0:
  592. continue
  593. # <stamp> <author>
  594. if re.search('files? changed', line) == None:
  595. pos = line.find(' ')
  596. if pos != -1:
  597. try:
  598. oldstamp = stamp
  599. (stamp, author) = (int(line[:pos]), line[pos+1:])
  600. if author not in conf["excluded_authors"]:
  601. if oldstamp > stamp:
  602. # clock skew, keep old timestamp to avoid having ugly graph
  603. stamp = oldstamp
  604. if author not in self.authors:
  605. self.authors[author] = { 'lines_added' : 0, 'lines_removed' : 0, 'commits' : 0}
  606. self.authors[author]['commits'] = self.authors[author].get('commits', 0) + 1
  607. self.authors[author]['lines_added'] = self.authors[author].get('lines_added', 0) + inserted
  608. self.authors[author]['lines_removed'] = self.authors[author].get('lines_removed', 0) + deleted
  609. if stamp not in self.changes_by_date_by_author:
  610. self.changes_by_date_by_author[stamp] = {}
  611. if author not in self.changes_by_date_by_author[stamp]:
  612. self.changes_by_date_by_author[stamp][author] = {}
  613. self.changes_by_date_by_author[stamp][author]['lines_added'] = self.authors[author]['lines_added']
  614. self.changes_by_date_by_author[stamp][author]['commits'] = self.authors[author]['commits']
  615. files, inserted, deleted = 0, 0, 0
  616. except ValueError:
  617. print('Warning: unexpected line "%s"' % line)
  618. else:
  619. print('Warning: unexpected line "%s"' % line)
  620. else:
  621. numbers = getstatsummarycounts(line);
  622. if len(numbers) == 3:
  623. (files, inserted, deleted) = [int(el) for el in numbers]
  624. else:
  625. print('Warning: failed to handle line "%s"' % line)
  626. (files, inserted, deleted) = (0, 0, 0)
  627. def refine(self):
  628. # authors
  629. # name -> {place_by_commits, commits_frac, date_first, date_last, timedelta}
  630. self.authors_by_commits = getkeyssortedbyvaluekey(self.authors, 'commits')
  631. self.authors_by_commits.reverse() # most first
  632. for i, name in enumerate(self.authors_by_commits):
  633. self.authors[name]['place_by_commits'] = i + 1
  634. for name in self.authors.keys():
  635. a = self.authors[name]
  636. a['commits_frac'] = (100 * float(a['commits'])) / self.getTotalCommits()
  637. date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp'])
  638. date_last = datetime.datetime.fromtimestamp(a['last_commit_stamp'])
  639. delta = date_last - date_first
  640. a['date_first'] = date_first.strftime('%Y-%m-%d')
  641. a['date_last'] = date_last.strftime('%Y-%m-%d')
  642. a['timedelta'] = delta
  643. if 'lines_added' not in a: a['lines_added'] = 0
  644. if 'lines_removed' not in a: a['lines_removed'] = 0
  645. def getActiveDays(self):
  646. return self.active_days
  647. def getActivityByDayOfWeek(self):
  648. return self.activity_by_day_of_week
  649. def getActivityByHourOfDay(self):
  650. return self.activity_by_hour_of_day
  651. def getLineActivityByDayOfWeek(self):
  652. return self.lineactivity_by_day_of_week
  653. def getLineActivityByHourOfDay(self):
  654. return self.lineactivity_by_hour_of_day
  655. def getAuthorInfo(self, author):
  656. return self.authors[author]
  657. def getAuthors(self, limit = None):
  658. res = getkeyssortedbyvaluekey(self.authors, 'commits')
  659. res.reverse()
  660. return res[:limit]
  661. def getCommitDeltaDays(self):
  662. return (self.last_commit_stamp / 86400 - self.first_commit_stamp / 86400) + 1
  663. def getDomainInfo(self, domain):
  664. return self.domains[domain]
  665. def getDomains(self):
  666. return self.domains.keys()
  667. def getFirstCommitDate(self):
  668. return datetime.datetime.fromtimestamp(self.first_commit_stamp)
  669. def getLastCommitDate(self):
  670. return datetime.datetime.fromtimestamp(self.last_commit_stamp)
  671. def getTags(self):
  672. lines = getpipeoutput(['git show-ref --tags', 'cut -d/ -f3'])
  673. return lines.split('\n')
  674. def getTagDate(self, tag):
  675. return self.revToDate('tags/' + tag)
  676. def getTotalAuthors(self):
  677. return self.total_authors
  678. def getTotalCommits(self):
  679. return self.total_commits
  680. def getTotalFiles(self):
  681. return self.total_files
  682. def getTotalLOC(self):
  683. return self.total_lines
  684. def getTotalLines(self):
  685. return self.total_lines_added + self.total_lines_removed
  686. def getTotalSize(self):
  687. return self.total_size
  688. def revToDate(self, rev):
  689. stamp = int(getpipeoutput(['git log --pretty=format:%%at "%s" -n 1' % rev]))
  690. return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d')
  691. class ReportCreator:
  692. """Creates the actual report based on given data."""
  693. def __init__(self):
  694. pass
  695. def create(self, data, path):
  696. self.data = data
  697. self.path = path
  698. def html_linkify(text):
  699. return text.lower().replace(' ', '_')
  700. def html_header(level, text):
  701. name = html_linkify(text)
  702. return '\n<h%d id="%s"><a href="#%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
  703. class GitDataCollectorJSONEncoder(json.JSONEncoder):
  704. def default(self, obj):
  705. if isinstance(obj, set):
  706. return list(obj)
  707. if isinstance(obj, datetime.timedelta):
  708. return str(obj)
  709. if isinstance(obj, GitDataCollector):
  710. return obj.__dict__
  711. # Let the base class default method raise the TypeError
  712. return json.JSONEncoder.default(self, obj)
  713. class JSONReportCreator(ReportCreator):
  714. def create(self, data, filename):
  715. f = open(filename, 'w')
  716. json.dump(data, f, indent=True,
  717. cls=GitDataCollectorJSONEncoder)
  718. f.close()
  719. class HTMLReportCreator(ReportCreator):
  720. def create(self, data, path):
  721. ReportCreator.create(self, data, path)
  722. self.title = data.projectname
  723. # copy static files. Looks in the binary directory, ../share/gitstats and /usr/share/gitstats
  724. binarypath = os.path.dirname(os.path.abspath(__file__))
  725. secondarypath = os.path.join(binarypath, '..', 'share', 'gitstats')
  726. basedirs = [binarypath, secondarypath, '/usr/share/gitstats']
  727. for file in (conf['style'], 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif'):
  728. for base in basedirs:
  729. src = base + '/' + file
  730. if os.path.exists(src):
  731. shutil.copyfile(src, path + '/' + file)
  732. break
  733. else:
  734. print('Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs))
  735. f = open(path + "/index.html", 'w')
  736. format = '%Y-%m-%d %H:%M:%S'
  737. self.printHeader(f)
  738. self.printNav(f)
  739. general_content = ['<h1 class="text-3xl font-bold underline">GitStats - %s</h1>' % data.projectname]
  740. general_content.append('<div><div class="card-body"><dl>')
  741. general_content.append('<dt>Project name</dt><dd>%s (%s branches, %s tags)</dd>' % (data.projectname, data.total_branches, data.total_tags))
  742. general_content.append('<dt>Generated</dt><dd>%s (in %d seconds)</dd>' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated()))
  743. general_content.append('<dt>Generator</dt><dd><a href="http://gitstats.sourceforge.net/">GitStats</a> (version %s), %s, %s</dd>' % (getversion(), getgitversion(), getgnuplotversion()))
  744. general_content.append('<dt>Report Period</dt><dd>%s to %s</dd>' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format)))
  745. general_content.append('<dt>Age</dt><dd>%d days, %d active days (%3.2f%%)</dd>' % (data.getCommitDeltaDays(), len(data.getActiveDays()), (100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays())))
  746. general_content.append('<dt>Total Files</dt><dd>%s</dd>' % data.getTotalFiles())
  747. general_content.append('<dt>Total Lines of Code</dt><dd>%s (%d added, %d removed)</dd>' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed))
  748. general_content.append('<dt>Total Commits</dt><dd>%s (average %.1f commits per active day, %.1f per all days)</dd>' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays()))
  749. general_content.append('<dt>Authors</dt><dd>%s (average %.1f commits per author)</dd>' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors()))
  750. general_content.append('</dl></div></div>')
  751. self.printContent(f, general_content)
  752. f.write('</div></body>\n</html>')
  753. f.close()
  754. ###
  755. # Activity
  756. f = open(path + '/activity.html', 'w')
  757. self.printHeader(f)
  758. self.printNav(f)
  759. activity_content = []
  760. #activity_content.append('<h2>Last 30 days</h2>')
  761. #activity_content.append('<h2>Last 12 months</h2>')
  762. # Weekly activity
  763. WEEKS = 32
  764. activity_content.append(html_header(2, 'Weekly activity'))
  765. activity_content.append('<p>Last %d weeks</p>' % WEEKS)
  766. # generate weeks to show (previous N weeks from now)
  767. now = datetime.datetime.now()
  768. deltaweek = datetime.timedelta(7)
  769. weeks = []
  770. stampcur = now
  771. for i in range(0, WEEKS):
  772. weeks.insert(0, stampcur.strftime('%Y-%W'))
  773. stampcur -= deltaweek
  774. # top row: commits & bar
  775. activity_content.append('<div><div class="card-body"><table class="noborders"><tr>')
  776. for i in range(0, WEEKS):
  777. commits = 0
  778. if weeks[i] in data.activity_by_year_week:
  779. commits = data.activity_by_year_week[weeks[i]]
  780. percentage = 0
  781. if weeks[i] in data.activity_by_year_week:
  782. percentage = float(data.activity_by_year_week[weeks[i]]) / data.activity_by_year_week_peak
  783. height = max(1, int(200 * percentage))
  784. activity_content.append('<td style="text-align: center; vertical-align: bottom">%d<div style="display: block; background-color: red; width: 20px; height: %dpx"></div></td>' % (commits, height))
  785. # bottom row: year/week
  786. activity_content.append('</tr><tr>')
  787. for i in range(0, WEEKS):
  788. activity_content.append('<td>%s</td>' % (WEEKS - i))
  789. activity_content.append('</tr></table></div></div>')
  790. # Hour of Day
  791. activity_content.append(html_header(2, 'Hour of Day'))
  792. hour_of_day = data.getActivityByHourOfDay()
  793. activity_content.append('<div><div class="card-body"><table><tr><th>Hour</th>')
  794. for i in range(0, 24):
  795. activity_content.append('<th>%d</th>' % i)
  796. activity_content.append('</tr>\n<tr><th>Commits</th>')
  797. fp = open(path + '/hour_of_day.dat', 'w')
  798. for i in range(0, 24):
  799. if i in hour_of_day:
  800. r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128)
  801. activity_content.append('<td style="background-color: rgb(%d, 0, 0)">%d</td>' % (r, hour_of_day[i]))
  802. fp.write('%d %d\n' % (i, hour_of_day[i]))
  803. else:
  804. activity_content.append('<td>0</td>')
  805. fp.write('%d 0\n' % i)
  806. fp.close()
  807. activity_content.append('</tr>\n<tr><th>%</th>')
  808. totalcommits = data.getTotalCommits()
  809. for i in range(0, 24):
  810. if i in hour_of_day:
  811. r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128)
  812. activity_content.append('<td style="background-color: rgb(%d, 0, 0)">%.2f</td>' % (r, (100.0 * hour_of_day[i]) / totalcommits))
  813. else:
  814. activity_content.append('<td>0.00</td>')
  815. activity_content.append('</tr></table>')
  816. activity_content.append('<img src="hour_of_day.png" alt="Hour of Day">')
  817. fg = open(path + '/hour_of_day.dat', 'w')
  818. for i in range(0, 24):
  819. if i in hour_of_day:
  820. fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
  821. else:
  822. fg.write('%d 0\n' % (i + 1))
  823. fg.close()
  824. # Day of Week
  825. activity_content.append(html_header(2, 'Day of Week'))
  826. day_of_week = data.getActivityByDayOfWeek()
  827. activity_content.append('<div class="vtable"><table>')
  828. activity_content.append('<tr><th>Day</th><th>Total (%)</th></tr>')
  829. fp = open(path + '/day_of_week.dat', 'w')
  830. for d in range(0, 7):
  831. commits = 0
  832. if d in day_of_week:
  833. commits = day_of_week[d]
  834. fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits))
  835. activity_content.append('<tr>')
  836. activity_content.append('<th>%s</th>' % (WEEKDAYS[d]))
  837. if d in day_of_week:
  838. activity_content.append('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits))
  839. else:
  840. activity_content.append('<td>0</td>')
  841. activity_content.append('</tr>')
  842. activity_content.append('</table></div>')
  843. activity_content.append('<img src="day_of_week.png" alt="Day of Week">')
  844. fp.close()
  845. # Hour of Week
  846. activity_content.append(html_header(2, 'Hour of Week'))
  847. activity_content.append('<table>')
  848. activity_content.append('<tr><th>Weekday</th>')
  849. for hour in range(0, 24):
  850. activity_content.append('<th>%d</th>' % (hour))
  851. activity_content.append('</tr>')
  852. for weekday in range(0, 7):
  853. activity_content.append('<tr><th>%s</th>' % (WEEKDAYS[weekday]))
  854. for hour in range(0, 24):
  855. try:
  856. commits = data.activity_by_hour_of_week[weekday][hour]
  857. except KeyError:
  858. commits = 0
  859. if commits != 0:
  860. activity_content.append('<td')
  861. r = 127 + int((float(commits) / data.activity_by_hour_of_week_busiest) * 128)
  862. activity_content.append(' style="background-color: rgb(%d, 0, 0)"' % r)
  863. activity_content.append('>%d</td>' % commits)
  864. else:
  865. activity_content.append('<td></td>')
  866. activity_content.append('</tr>')
  867. activity_content.append('</table></div></div>')
  868. # Month of Year
  869. activity_content.append(html_header(2, 'Month of Year'))
  870. activity_content.append('<div class="vtable"><table>')
  871. activity_content.append('<tr><th>Month</th><th>Commits (%)</th></tr>')
  872. fp = open (path + '/month_of_year.dat', 'w')
  873. for mm in range(1, 13):
  874. commits = 0
  875. if mm in data.activity_by_month_of_year:
  876. commits = data.activity_by_month_of_year[mm]
  877. activity_content.append('<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalCommits()))
  878. fp.write('%d %d\n' % (mm, commits))
  879. fp.close()
  880. activity_content.append('</table></div>')
  881. activity_content.append('<img src="month_of_year.png" alt="Month of Year">')
  882. # Commits by year/month
  883. activity_content.append(html_header(2, 'Commits by year/month'))
  884. activity_content.append('<div class="vtable"><table><tr><th>Month</th><th>Commits</th><th>Lines added</th><th>Lines removed</th></tr>')
  885. for yymm in reversed(sorted(data.commits_by_month.keys())):
  886. activity_content.append('<tr><td>%s</td><td>%d</td><td>%d</td><td>%d</td></tr>' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0)))
  887. activity_content.append('</table></div>')
  888. activity_content.append('<img src="commits_by_year_month.png" alt="Commits by year/month">')
  889. fg = open(path + '/commits_by_year_month.dat', 'w')
  890. for yymm in sorted(data.commits_by_month.keys()):
  891. fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
  892. fg.close()
  893. # Commits by year
  894. activity_content.append(html_header(2, 'Commits by Year'))
  895. activity_content.append('<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th><th>Lines added</th><th>Lines removed</th></tr>')
  896. for yy in reversed(sorted(data.commits_by_year.keys())):
  897. activity_content.append('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d</td><td>%d</td></tr>' % (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)))
  898. activity_content.append('</table></div>')
  899. activity_content.append('<img src="commits_by_year.png" alt="Commits by Year">')
  900. fg = open(path + '/commits_by_year.dat', 'w')
  901. for yy in sorted(data.commits_by_year.keys()):
  902. fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
  903. fg.close()
  904. # Commits by timezone
  905. activity_content.append(html_header(2, 'Commits by Timezone'))
  906. activity_content.append('<div><div class="card-body"><table><tr>')
  907. activity_content.append('<th>Timezone</th><th>Commits</th>')
  908. activity_content.append('</tr>')
  909. max_commits_on_tz = max(data.commits_by_timezone.values())
  910. for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)):
  911. commits = data.commits_by_timezone[i]
  912. r = 127 + int((float(commits) / max_commits_on_tz) * 128)
  913. activity_content.append('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
  914. activity_content.append('</table></div>')
  915. self.printContent(f, activity_content, 'Activity')
  916. f.write('</div></body></html>')
  917. f.close()
  918. ###
  919. # Authors
  920. f = open(path + '/authors.html', 'w')
  921. self.printHeader(f)
  922. self.printNav(f)
  923. authors_content = []
  924. # Authors :: List of authors
  925. authors_content.append(html_header(2, 'List of Authors'))
  926. authors_content.append('<div><div class="card-body"><table class="authors sortable" id="authors">')
  927. authors_content.append('<tr><th>Author</th><th>Commits (%)</th><th>+ lines</th><th>- lines</th><th>First commit</th><th>Last commit</th><th class="unsortable">Age</th><th>Active days</th><th># by commits</th></tr>')
  928. for author in data.getAuthors(conf['max_authors']):
  929. info = data.getAuthorInfo(author)
  930. authors_content.append('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d</td><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%d</td><td>%d</td></tr>' % (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']))
  931. authors_content.append('</table></div></div>')
  932. allauthors = data.getAuthors()
  933. if len(allauthors) > conf['max_authors']:
  934. rest = allauthors[conf['max_authors']:]
  935. authors_content.append('<p class="moreauthors">These didn\'t make it to the top: %s</p>' % ', '.join(rest))
  936. authors_content.append(html_header(2, 'Cumulated Added Lines of Code per Author'))
  937. authors_content.append('<img src="lines_of_code_by_author.png" alt="Lines of code per Author">')
  938. if len(allauthors) > conf['max_authors']:
  939. authors_content.append('<p class="moreauthors">Only top %d authors shown</p>' % conf['max_authors'])
  940. authors_content.append(html_header(2, 'Commits per Author'))
  941. authors_content.append('<img src="commits_by_author.png" alt="Commits per Author">')
  942. if len(allauthors) > conf['max_authors']:
  943. authors_content.append('<p class="moreauthors">Only top %d authors shown</p>' % conf['max_authors'])
  944. fgl = open(path + '/lines_of_code_by_author.dat', 'w')
  945. fgc = open(path + '/commits_by_author.dat', 'w')
  946. lines_by_authors = {} # cumulated added lines by
  947. # author. to save memory,
  948. # changes_by_date_by_author[stamp][author] is defined
  949. # only at points where author commits.
  950. # lines_by_authors allows us to generate all the
  951. # points in the .dat file.
  952. # Don't rely on getAuthors to give the same order each
  953. # time. Be robust and keep the list in a variable.
  954. commits_by_authors = {} # cumulated added lines by
  955. self.authors_to_plot = data.getAuthors(conf['max_authors'])
  956. for author in self.authors_to_plot:
  957. lines_by_authors[author] = 0
  958. commits_by_authors[author] = 0
  959. for stamp in sorted(data.changes_by_date_by_author.keys()):
  960. fgl.write('%d' % stamp)
  961. fgc.write('%d' % stamp)
  962. for author in self.authors_to_plot:
  963. if author in data.changes_by_date_by_author[stamp].keys():
  964. lines_by_authors[author] = data.changes_by_date_by_author[stamp][author]['lines_added']
  965. commits_by_authors[author] = data.changes_by_date_by_author[stamp][author]['commits']
  966. fgl.write(' %d' % lines_by_authors[author])
  967. fgc.write(' %d' % commits_by_authors[author])
  968. fgl.write('\n')
  969. fgc.write('\n')
  970. fgl.close()
  971. fgc.close()
  972. # Authors :: Author of Month
  973. authors_content.append(html_header(2, 'Author of Month'))
  974. authors_content.append('<table class="sortable" id="aom">')
  975. authors_content.append('<tr><th>Month</th><th>Author</th><th>Commits (%%)</th><th class="unsortable">Next top %d</th><th>Number of authors</th></tr>' % conf['authors_top'])
  976. for yymm in reversed(sorted(data.author_of_month.keys())):
  977. authordict = data.author_of_month[yymm]
  978. authors = getkeyssortedbyvalues(authordict)
  979. authors.reverse()
  980. commits = data.author_of_month[yymm][authors[0]]
  981. next = ', '.join(authors[1:conf['authors_top']+1])
  982. authors_content.append('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td><td>%d</td></tr>' % (yymm, authors[0], commits, (100.0 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm], next, len(authors)))
  983. authors_content.append('</table>')
  984. authors_content.append(html_header(2, 'Author of Year'))
  985. authors_content.append('<table class="sortable" id="aoy"><tr><th>Year</th><th>Author</th><th>Commits (%%)</th><th class="unsortable">Next top %d</th><th>Number of authors</th></tr>' % conf['authors_top'])
  986. for yy in reversed(sorted(data.author_of_year.keys())):
  987. authordict = data.author_of_year[yy]
  988. authors = getkeyssortedbyvalues(authordict)
  989. authors.reverse()
  990. commits = data.author_of_year[yy][authors[0]]
  991. next = ', '.join(authors[1:conf['authors_top']+1])
  992. authors_content.append('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td><td>%d</td></tr>' % (yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next, len(authors)))
  993. authors_content.append('</table>')
  994. # Domains
  995. authors_content.append(html_header(2, 'Commits by Domains'))
  996. domains_by_commits = getkeyssortedbyvaluekey(data.domains, 'commits')
  997. domains_by_commits.reverse() # most first
  998. authors_content.append('<div class="vtable"><table>')
  999. authors_content.append('<tr><th>Domains</th><th>Total (%)</th></tr>')
  1000. fp = open(path + '/domains.dat', 'w')
  1001. n = 0
  1002. for domain in domains_by_commits:
  1003. if n == conf['max_domains']:
  1004. break
  1005. commits = 0
  1006. n += 1
  1007. info = data.getDomainInfo(domain)
  1008. fp.write('%s %d %d\n' % (domain, n , info['commits']))
  1009. authors_content.append('<tr><th>%s</th><td>%d (%.2f%%)</td></tr>' % (domain, info['commits'], (100.0 * info['commits'] / totalcommits)))
  1010. authors_content.append('</table></div>')
  1011. authors_content.append('<img src="domains.png" alt="Commits by Domains">')
  1012. fp.close()
  1013. self.printContent(f, authors_content, 'Authors')
  1014. f.write('</div></body></html>')
  1015. f.close()
  1016. ###
  1017. # Files
  1018. f = open(path + '/files.html', 'w')
  1019. self.printHeader(f)
  1020. self.printNav(f)
  1021. files_content = []
  1022. files_content.append('<dl>\n')
  1023. files_content.append('<dt>Total files</dt><dd>%d</dd>' % data.getTotalFiles())
  1024. files_content.append('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
  1025. try:
  1026. files_content.append('<dt>Average file size</dt><dd>%.2f bytes</dd>' % (float(data.getTotalSize()) / data.getTotalFiles()))
  1027. except ZeroDivisionError:
  1028. pass
  1029. files_content.append('</dl>\n')
  1030. # Files :: File count by date
  1031. files_content.append(html_header(2, 'File count by date'))
  1032. # use set to get rid of duplicate/unnecessary entries
  1033. files_by_date = set()
  1034. for stamp in sorted(data.files_by_stamp.keys()):
  1035. files_by_date.add('%s %d' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
  1036. fg = open(path + '/files_by_date.dat', 'w')
  1037. for line in sorted(list(files_by_date)):
  1038. fg.write('%s\n' % line)
  1039. #for stamp in sorted(data.files_by_stamp.keys()):
  1040. # fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
  1041. fg.close()
  1042. files_content.append('<img src="files_by_date.png" alt="Files by Date">')
  1043. #files_content.append('<h2>Average file size by date</h2>')
  1044. # Files :: Extensions
  1045. files_content.append(html_header(2, 'Extensions'))
  1046. files_content.append('<table class="sortable" id="ext"><tr><th>Extension</th><th>Files (%)</th><th>Lines (%)</th><th>Lines/file</th></tr>')
  1047. for ext in sorted(data.extensions.keys()):
  1048. files = data.extensions[ext]['files']
  1049. lines = data.extensions[ext]['lines']
  1050. try:
  1051. loc_percentage = (100.0 * lines) / data.getTotalLOC()
  1052. except ZeroDivisionError:
  1053. loc_percentage = 0
  1054. files_content.append('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d (%.2f%%)</td><td>%d</td></tr>' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, loc_percentage, lines / files))
  1055. files_content.append('</table>')
  1056. self.printContent(f, files_content, 'Files')
  1057. f.write('</div></body></html>')
  1058. f.close()
  1059. ###
  1060. # Lines
  1061. f = open(path + '/lines.html', 'w')
  1062. self.printHeader(f)
  1063. self.printNav(f)
  1064. lines_content=[]
  1065. lines_content.append('<dl>\n')
  1066. lines_content.append('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
  1067. lines_content.append('</dl>\n')
  1068. lines_content.append(html_header(2, 'Lines of Code'))
  1069. lines_content.append('<img src="lines_of_code.png" alt="Lines of Code">')
  1070. fg = open(path + '/lines_of_code.dat', 'w')
  1071. for stamp in sorted(data.changes_by_date.keys()):
  1072. fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines']))
  1073. fg.close()
  1074. # Weekly activity
  1075. WEEKS = 32
  1076. lines_content.append(html_header(2, 'Weekly activity'))
  1077. lines_content.append('<p>Last %d weeks</p>' % WEEKS)
  1078. # generate weeks to show (previous N weeks from now)
  1079. now = datetime.datetime.now()
  1080. deltaweek = datetime.timedelta(7)
  1081. weeks = []
  1082. stampcur = now
  1083. for i in range(0, WEEKS):
  1084. weeks.insert(0, stampcur.strftime('%Y-%W'))
  1085. stampcur -= deltaweek
  1086. # top row: commits & bar
  1087. lines_content.append('<table class="noborders"><tr>')
  1088. for i in range(0, WEEKS):
  1089. commits = 0
  1090. if weeks[i] in data.lineactivity_by_year_week:
  1091. commits = data.lineactivity_by_year_week[weeks[i]]
  1092. percentage = 0
  1093. if weeks[i] in data.lineactivity_by_year_week:
  1094. percentage = float(data.lineactivity_by_year_week[weeks[i]]) / data.lineactivity_by_year_week_peak
  1095. height = max(1, int(200 * percentage))
  1096. lines_content.append('<td style="text-align: center; vertical-align: bottom">%d<div style="display: block; background-color: red; width: 20px; height: %dpx"></div></td>' % (commits, height))
  1097. # bottom row: year/week
  1098. lines_content.append('</tr><tr>')
  1099. for i in range(0, WEEKS):
  1100. lines_content.append('<td>%s</td>' % (WEEKS - i))
  1101. lines_content.append('</tr></table>')
  1102. # Hour of Day
  1103. lines_content.append(html_header(2, 'Hour of Day'))
  1104. hour_of_day = data.getLineActivityByHourOfDay()
  1105. lines_content.append('<table><tr><th>Hour</th>')
  1106. for i in range(0, 24):
  1107. lines_content.append('<th>%d</th>' % i)
  1108. lines_content.append('</tr>\n<tr><th>Lines</th>')
  1109. fp = open(path + '/line_hour_of_day.dat', 'w')
  1110. for i in range(0, 24):
  1111. if i in hour_of_day:
  1112. r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128)
  1113. lines_content.append('<td style="background-color: rgb(%d, 0, 0)">%d</td>' % (r, hour_of_day[i]))
  1114. fp.write('%d %d\n' % (i, hour_of_day[i]))
  1115. else:
  1116. lines_content.append('<td>0</td>')
  1117. fp.write('%d 0\n' % i)
  1118. fp.close()
  1119. lines_content.append('</tr>\n<tr><th>%</th>')
  1120. totallines = data.getTotalLines()
  1121. for i in range(0, 24):
  1122. if i in hour_of_day:
  1123. r = 127 + int((float(hour_of_day[i]) / data.lineactivity_by_hour_of_day_busiest) * 128)
  1124. lines_content.append('<td style="background-color: rgb(%d, 0, 0)">%.2f</td>' % (r, (100.0 * hour_of_day[i]) / totallines))
  1125. else:
  1126. lines_content.append('<td>0.00</td>')
  1127. lines_content.append('</tr></table>')
  1128. lines_content.append('<img src="line_hour_of_day.png" alt="Hour of Day" />')
  1129. fg = open(path + '/line_hour_of_day.dat', 'w')
  1130. for i in range(0, 24):
  1131. if i in hour_of_day:
  1132. fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
  1133. else:
  1134. fg.write('%d 0\n' % (i + 1))
  1135. fg.close()
  1136. # Day of Week
  1137. lines_content.append(html_header(2, 'Day of Week'))
  1138. day_of_week = data.getLineActivityByDayOfWeek()
  1139. lines_content.append('<div class="vtable"><table>')
  1140. lines_content.append('<tr><th>Day</th><th>Total (%)</th></tr>')
  1141. fp = open(path + '/line_day_of_week.dat', 'w')
  1142. for d in range(0, 7):
  1143. commits = 0
  1144. if d in day_of_week:
  1145. commits = day_of_week[d]
  1146. fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits))
  1147. lines_content.append('<tr>')
  1148. lines_content.append('<th>%s</th>' % (WEEKDAYS[d]))
  1149. if d in day_of_week:
  1150. lines_content.append('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totallines))
  1151. else:
  1152. lines_content.append('<td>0</td>')
  1153. lines_content.append('</tr>')
  1154. lines_content.append('</table></div>')
  1155. lines_content.append('<img src="line_day_of_week.png" alt="Day of Week" />')
  1156. fp.close()
  1157. # Hour of Week
  1158. lines_content.append(html_header(2, 'Hour of Week'))
  1159. lines_content.append('<table>')
  1160. lines_content.append('<tr><th>Weekday</th>')
  1161. for hour in range(0, 24):
  1162. lines_content.append('<th>%d</th>' % (hour))
  1163. lines_content.append('</tr>')
  1164. for weekday in range(0, 7):
  1165. lines_content.append('<tr><th>%s</th>' % (WEEKDAYS[weekday]))
  1166. for hour in range(0, 24):
  1167. try:
  1168. commits = data.lineactivity_by_hour_of_week[weekday][hour]
  1169. except KeyError:
  1170. commits = 0
  1171. if commits != 0:
  1172. lines_content.append('<td')
  1173. r = 127 + int((float(commits) / data.lineactivity_by_hour_of_week_busiest) * 128)
  1174. lines_content.append(' style="background-color: rgb(%d, 0, 0)"' % r)
  1175. lines_content.append('>%d</td>' % commits)
  1176. else:
  1177. lines_content.append('<td></td>')
  1178. lines_content.append('</tr>')
  1179. lines_content.append('</table>')
  1180. # Month of Year
  1181. lines_content.append(html_header(2, 'Month of Year'))
  1182. lines_content.append('<div class="vtable"><table>')
  1183. lines_content.append('<tr><th>Month</th><th>Lines (%)</th></tr>')
  1184. fp = open (path + '/line_month_of_year.dat', 'w')
  1185. for mm in range(1, 13):
  1186. commits = 0
  1187. if mm in data.lineactivity_by_month_of_year:
  1188. commits = data.lineactivity_by_month_of_year[mm]
  1189. lines_content.append('<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalLines()))
  1190. fp.write('%d %d\n' % (mm, commits))
  1191. fp.close()
  1192. lines_content.append('</table></div>')
  1193. lines_content.append('<img src="line_month_of_year.png" alt="Month of Year" />')
  1194. # Lines by year/month
  1195. lines_content.append(html_header(2, 'Lines by year/month'))
  1196. lines_content.append('<div class="vtable"><table><tr><th>Month</th><th>Commits</th><th>Lines added</th><th>Lines removed</th></tr>')
  1197. for yymm in reversed(sorted(data.commits_by_month.keys())):
  1198. lines_content.append('<tr><td>%s</td><td>%d</td><td>%d</td><td>%d</td></tr>' % (yymm, data.commits_by_month.get(yymm,0), data.lines_added_by_month.get(yymm,0), data.lines_removed_by_month.get(yymm,0)))
  1199. lines_content.append('</table></div>')
  1200. lines_content.append('<img src="line_commits_by_year_month.png" alt="Commits by year/month" />')
  1201. fg = open(path + '/line_commits_by_year_month.dat', 'w')
  1202. for yymm in sorted(data.commits_by_month.keys()):
  1203. fg.write('%s %s\n' % (yymm, data.lines_added_by_month.get(yymm, 0) + data.lines_removed_by_month.get(yymm, 0)))
  1204. fg.close()
  1205. # Lines by year
  1206. lines_content.append(html_header(2, 'Lines by Year'))
  1207. lines_content.append('<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th><th>Lines added</th><th>Lines removed</th></tr>')
  1208. for yy in reversed(sorted(data.commits_by_year.keys())):
  1209. lines_content.append('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d</td><td>%d</td></tr>' % (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)))
  1210. lines_content.append('</table></div>')
  1211. lines_content.append('<img src="line_commits_by_year.png" alt="Commits by Year" />')
  1212. fg = open(path + '/line_commits_by_year.dat', 'w')
  1213. for yy in sorted(data.commits_by_year.keys()):
  1214. fg.write('%d %d\n' % (yy, data.lines_added_by_year.get(yy,0) + data.lines_removed_by_year.get(yy,0)))
  1215. fg.close()
  1216. # Commits by timezone
  1217. lines_content.append(html_header(2, 'Commits by Timezone'))
  1218. lines_content.append('<table><tr>')
  1219. lines_content.append('<th>Timezone</th><th>Commits</th>')
  1220. max_commits_on_tz = max(data.commits_by_timezone.values())
  1221. for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)):
  1222. commits = data.commits_by_timezone[i]
  1223. r = 127 + int((float(commits) / max_commits_on_tz) * 128)
  1224. lines_content.append('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
  1225. lines_content.append('</tr></table>')
  1226. self.printContent(f, lines_content, 'Lines')
  1227. f.write('</div></body></html>')
  1228. f.close()
  1229. ###
  1230. # tags.html
  1231. f = open(path + '/tags.html', 'w')
  1232. self.printHeader(f)
  1233. self.printNav(f)
  1234. tags_content = []
  1235. tags_content.append('<dl>')
  1236. tags_content.append('<dt>Total tags</dt><dd>%d</dd>' % len(data.tags))
  1237. if len(data.tags) > 0:
  1238. tags_content.append('<dt>Average commits per tag</dt><dd>%.2f</dd>' % (1.0 * data.getTotalCommits() / len(data.tags)))
  1239. tags_content.append('</dl>')
  1240. tags_content.append('<table class="tags">')
  1241. tags_content.append('<tr><th>Name</th><th>Date</th><th>Commits</th><th>Authors</th></tr>')
  1242. # sort the tags by date desc
  1243. tags_sorted_by_date_desc = [el[1] for el in reversed(sorted([(el[1]['date'], el[0]) for el in list(data.tags.items())]))]
  1244. for tag in tags_sorted_by_date_desc:
  1245. authorinfo = []
  1246. self.authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors'])
  1247. for i in reversed(self.authors_by_commits):
  1248. authorinfo.append('%s (%d)' % (i, data.tags[tag]['authors'][i]))
  1249. tags_content.append('<tr><td>%s</td><td>%s</td><td>%d</td><td>%s</td></tr>' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo)))
  1250. tags_content.append('</table>')
  1251. self.printContent(f, tags_content, 'Tags')
  1252. f.write('</div></body></html>')
  1253. f.close()
  1254. self.createGraphs(path)
  1255. def createGraphs(self, path):
  1256. print('Generating graphs...')
  1257. # hour of day
  1258. f = open(path + '/hour_of_day.plot', 'w')
  1259. f.write(GNUPLOT_COMMON)
  1260. f.write(
  1261. """
  1262. set output 'hour_of_day.png'
  1263. unset key
  1264. set xrange [0.5:24.5]
  1265. set yrange [0:]
  1266. set xtics 4
  1267. set grid y
  1268. set ylabel "Commits"
  1269. plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
  1270. """)
  1271. f.close()
  1272. # day of week
  1273. f = open(path + '/day_of_week.plot', 'w')
  1274. f.write(GNUPLOT_COMMON)
  1275. f.write(
  1276. """
  1277. set output 'day_of_week.png'
  1278. unset key
  1279. set xrange [0.5:7.5]
  1280. set yrange [0:]
  1281. set xtics 1
  1282. set grid y
  1283. set ylabel "Commits"
  1284. plot 'day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid
  1285. """)
  1286. f.close()
  1287. # Domains
  1288. f = open(path + '/domains.plot', 'w')
  1289. f.write(GNUPLOT_COMMON)
  1290. f.write(
  1291. """
  1292. set output 'domains.png'
  1293. unset key
  1294. unset xtics
  1295. set yrange [0:]
  1296. set grid y
  1297. set ylabel "Commits"
  1298. plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1
  1299. """)
  1300. f.close()
  1301. # Month of Year
  1302. f = open(path + '/month_of_year.plot', 'w')
  1303. f.write(GNUPLOT_COMMON)
  1304. f.write(
  1305. """
  1306. set output 'month_of_year.png'
  1307. unset key
  1308. set xrange [0.5:12.5]
  1309. set yrange [0:]
  1310. set xtics 1
  1311. set grid y
  1312. set ylabel "Commits"
  1313. plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid
  1314. """)
  1315. f.close()
  1316. # commits_by_year_month
  1317. f = open(path + '/commits_by_year_month.plot', 'w')
  1318. f.write(GNUPLOT_COMMON)
  1319. f.write(
  1320. """
  1321. set output 'commits_by_year_month.png'
  1322. unset key
  1323. set yrange [0:]
  1324. set xdata time
  1325. set timefmt "%Y-%m"
  1326. set format x "%Y-%m"
  1327. set xtics rotate
  1328. set bmargin 5
  1329. set grid y
  1330. set ylabel "Commits"
  1331. plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
  1332. """)
  1333. f.close()
  1334. # commits_by_year
  1335. f = open(path + '/commits_by_year.plot', 'w')
  1336. f.write(GNUPLOT_COMMON)
  1337. f.write(
  1338. """
  1339. set output 'commits_by_year.png'
  1340. unset key
  1341. set yrange [0:]
  1342. set xtics 1 rotate
  1343. set grid y
  1344. set ylabel "Commits"
  1345. set yrange [0:]
  1346. plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
  1347. """)
  1348. f.close()
  1349. # Files by date
  1350. f = open(path + '/files_by_date.plot', 'w')
  1351. f.write(GNUPLOT_COMMON)
  1352. f.write(
  1353. """
  1354. set output 'files_by_date.png'
  1355. unset key
  1356. set yrange [0:]
  1357. set xdata time
  1358. set timefmt "%Y-%m-%d"
  1359. set format x "%Y-%m-%d"
  1360. set grid y
  1361. set ylabel "Files"
  1362. set xtics rotate
  1363. set ytics autofreq
  1364. set bmargin 6
  1365. plot 'files_by_date.dat' using 1:2 w steps
  1366. """)
  1367. f.close()
  1368. # Lines of Code
  1369. f = open(path + '/lines_of_code.plot', 'w')
  1370. f.write(GNUPLOT_COMMON)
  1371. f.write(
  1372. """
  1373. set output 'lines_of_code.png'
  1374. unset key
  1375. set yrange [0:]
  1376. set xdata time
  1377. set timefmt "%s"
  1378. set format x "%Y-%m-%d"
  1379. set grid y
  1380. set ylabel "Lines"
  1381. set xtics rotate
  1382. set bmargin 6
  1383. plot 'lines_of_code.dat' using 1:2 w lines
  1384. """)
  1385. f.close()
  1386. # Lines of Code Added per author
  1387. f = open(path + '/lines_of_code_by_author.plot', 'w')
  1388. f.write(GNUPLOT_COMMON)
  1389. f.write(
  1390. """
  1391. set terminal png transparent size 640,480
  1392. set output 'lines_of_code_by_author.png'
  1393. set key left top
  1394. set yrange [0:]
  1395. set xdata time
  1396. set timefmt "%s"
  1397. set format x "%Y-%m-%d"
  1398. set grid y
  1399. set ylabel "Lines"
  1400. set xtics rotate
  1401. set bmargin 6
  1402. plot """
  1403. )
  1404. i = 1
  1405. plots = []
  1406. for a in self.authors_to_plot:
  1407. i = i + 1
  1408. author = a.replace("\"", "\\\"").replace("`", "")
  1409. plots.append("""'lines_of_code_by_author.dat' using 1:%d title "%s" w lines""" % (i, author))
  1410. f.write(", ".join(plots))
  1411. f.write('\n')
  1412. f.close()
  1413. # hour of day
  1414. f = open(path + '/line_hour_of_day.plot', 'w')
  1415. f.write(GNUPLOT_COMMON)
  1416. f.write(
  1417. """
  1418. set output 'line_hour_of_day.png'
  1419. unset key
  1420. set xrange [0.5:24.5]
  1421. set xtics 4
  1422. set grid y
  1423. set ylabel "Lines"
  1424. plot 'line_hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
  1425. """)
  1426. f.close()
  1427. # day of week
  1428. f = open(path + '/line_day_of_week.plot', 'w')
  1429. f.write(GNUPLOT_COMMON)
  1430. f.write(
  1431. """
  1432. set output 'line_day_of_week.png'
  1433. unset key
  1434. set xrange [0.5:7.5]
  1435. set xtics 1
  1436. set grid y
  1437. set ylabel "Lines"
  1438. plot 'line_day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid
  1439. """)
  1440. f.close()
  1441. # Domains
  1442. # f = open(path + '/domains.plot', 'w')
  1443. # f.write(GNUPLOT_COMMON)
  1444. # f.write(
  1445. #"""
  1446. #set output 'domains.png'
  1447. #unset key
  1448. #unset xtics
  1449. #set yrange [0:]
  1450. #set grid y
  1451. #set ylabel "Commits"
  1452. #plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1
  1453. #""")
  1454. # f.close()
  1455. # Month of Year
  1456. f = open(path + '/line_month_of_year.plot', 'w')
  1457. f.write(GNUPLOT_COMMON)
  1458. f.write(
  1459. """
  1460. set output 'line_month_of_year.png'
  1461. unset key
  1462. set xrange [0.5:12.5]
  1463. set xtics 1
  1464. set grid y
  1465. set ylabel "Lines"
  1466. plot 'line_month_of_year.dat' using 1:2:(0.5) w boxes fs solid
  1467. """)
  1468. f.close()
  1469. # commits_by_year_month
  1470. f = open(path + '/line_commits_by_year_month.plot', 'w')
  1471. f.write(GNUPLOT_COMMON)
  1472. f.write(
  1473. """
  1474. set output 'line_commits_by_year_month.png'
  1475. unset key
  1476. set xdata time
  1477. set timefmt "%Y-%m"
  1478. set format x "%Y-%m"
  1479. set xtics rotate
  1480. set bmargin 5
  1481. set grid y
  1482. set ylabel "Lines"
  1483. plot 'line_commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
  1484. """)
  1485. f.close()
  1486. # commits_by_year
  1487. f = open(path + '/line_commits_by_year.plot', 'w')
  1488. f.write(GNUPLOT_COMMON)
  1489. f.write(
  1490. """
  1491. set output 'line_commits_by_year.png'
  1492. unset key
  1493. set xtics 1 rotate
  1494. set grid y
  1495. set ylabel "Lines"
  1496. set yrange [0:]
  1497. plot 'line_commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
  1498. """)
  1499. f.close()
  1500. # Commits per author
  1501. f = open(path + '/commits_by_author.plot', 'w')
  1502. f.write(GNUPLOT_COMMON)
  1503. f.write(
  1504. """
  1505. set terminal png transparent size 640,480
  1506. set output 'commits_by_author.png'
  1507. set key left top
  1508. set yrange [0:]
  1509. set xdata time
  1510. set timefmt "%s"
  1511. set format x "%Y-%m-%d"
  1512. set grid y
  1513. set ylabel "Commits"
  1514. set xtics rotate
  1515. set bmargin 6
  1516. plot """
  1517. )
  1518. i = 1
  1519. plots = []
  1520. for a in self.authors_to_plot:
  1521. i = i + 1
  1522. author = a.replace("\"", "\\\"").replace("`", "")
  1523. plots.append("""'commits_by_author.dat' using 1:%d title "%s" w lines""" % (i, author))
  1524. f.write(", ".join(plots))
  1525. f.write('\n')
  1526. f.close()
  1527. os.chdir(path)
  1528. files = glob.glob(path + '/*.plot')
  1529. for f in files:
  1530. out = getpipeoutput([gnuplot_cmd + ' "%s"' % f])
  1531. if len(out) > 0:
  1532. print(out)
  1533. def printHeader(self, f, title = ''):
  1534. f.write(
  1535. """<!DOCTYPE html>
  1536. <html>
  1537. <head>
  1538. <meta charset="UTF-8">
  1539. <title>GitStats - %s</title>
  1540. <link rel="stylesheet" href="%s" type="text/css" />
  1541. <meta name="generator" content="GitStats %s" />
  1542. <script type="text/javascript" src="sortable.js"></script>
  1543. <script src="https://cdn.tailwindcss.com"></script>
  1544. </head>
  1545. <body>
  1546. <div class="flex h-screen antialiased text-gray-900 bg-gray-100 dark:bg-dark dark:text-light">
  1547. """ % (self.title, conf['style'], getversion()))
  1548. def printContent(self, f, content, title="Front's Stats"):
  1549. content = '\n'.join(content)
  1550. f.write(f'''
  1551. <div class="flex-1 h-full overflow-x-hidden overflow-y-auto">
  1552. <!-- Navbar -->
  1553. <header class="relative flex-shrink-0 bg-white dark:bg-darker">
  1554. <div class="flex items-center justify-between p-2 border-b dark:border-primary-darker">
  1555. <a href="#" class="inline-block text-2xl font-bold tracking-wider uppercase text-primary-dark dark:text-light">
  1556. {title}
  1557. </a>
  1558. </div>
  1559. </header>
  1560. <!-- Main content -->
  1561. <main class="p-2">{content}</main>
  1562. </div>
  1563. ''')
  1564. def printNav(self, f):
  1565. menu = ''.join([f'''
  1566. <a
  1567. href="{href}"
  1568. class="flex items-center p-2 text-gray-500 transition-colors rounded-md dark:text-light hover:bg-primary-100 dark:hover:bg-primary"
  1569. role="button"
  1570. >
  1571. <span aria-hidden="true">
  1572. <svg
  1573. class="w-5 h-5"
  1574. xmlns="http://www.w3.org/2000/svg"
  1575. fill="none"
  1576. viewBox="0 0 24 24"
  1577. stroke="currentColor"
  1578. >
  1579. <path
  1580. stroke-linecap="round"
  1581. stroke-linejoin="round"
  1582. stroke-width="2"
  1583. d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
  1584. />
  1585. </svg>
  1586. </span>
  1587. <span class="ml-2 text-sm">{label}</span>
  1588. </a>
  1589. ''' for (label, href) in [
  1590. ("General", "index.html"),
  1591. ("Activity", "activity.html"),
  1592. ("Authors","authors.html"),
  1593. ("Files","files.html"),
  1594. ("Lines", "lines.html"),
  1595. ("Tags", "tags.html")
  1596. ]])
  1597. f.write(f'''
  1598. <!-- Sidebar -->
  1599. <aside class="flex-shrink-0 hidden w-64 bg-white border-r dark:border-primary-darker dark:bg-darker md:block">
  1600. <div class="flex flex-col h-full">
  1601. <!-- Sidebar links -->
  1602. <nav aria-label="Main" class="flex-1 px-2 py-4 space-y-2 overflow-y-hidden hover:overflow-y-auto">
  1603. {menu}
  1604. </nav>
  1605. <!-- Sidebar footer -->
  1606. <div class="flex-shrink-0 px-2 py-4 space-y-2">
  1607. <button type="button" class="flex items-center justify-center w-full px-4 py-2 text-sm text-white rounded-md bg-primary hover:bg-primary-dark focus:outline-none focus:ring focus:ring-primary-dark focus:ring-offset-1 focus:ring-offset-white dark:focus:ring-offset-dark">
  1608. <span>Customize</span>
  1609. </button>
  1610. </div>
  1611. </div>
  1612. </aside>
  1613. ''')
  1614. def printNav_old(self, f):
  1615. f.write("""
  1616. <div class="nav">
  1617. <ul>
  1618. <li><a href="index.html">General</a></li>
  1619. <li><a href="activity.html">Activity</a></li>
  1620. <li><a href="authors.html">Authors</a></li>
  1621. <li><a href="files.html">Files</a></li>
  1622. <li><a href="lines.html">Lines</a></li>
  1623. <li><a href="tags.html">Tags</a></li>
  1624. </ul>
  1625. </div>
  1626. """)
  1627. def usage():
  1628. text = """
  1629. Usage: gitstats [options] <gitpath..> <outputpath>
  1630. Options:
  1631. -c key=value Override configuration value
  1632. Default config values:
  1633. %s
  1634. Please see the manual page for more details.
  1635. """ % conf
  1636. print(text)
  1637. class GitStats:
  1638. def run(self, args_orig):
  1639. optlist, args = getopt.getopt(args_orig, 'hc:', ["help"])
  1640. for o,v in optlist:
  1641. if o == '-c':
  1642. key, value = v.split('=', 1)
  1643. if key not in conf:
  1644. raise KeyError('no such key "%s" in config' % key)
  1645. if isinstance(conf[key], int):
  1646. conf[key] = int(value)
  1647. elif isinstance(conf[key], list):
  1648. conf[key].append(value)
  1649. else:
  1650. conf[key] = value
  1651. elif o in ('-h', '--help'):
  1652. usage()
  1653. sys.exit()
  1654. if len(args) < 2:
  1655. usage()
  1656. sys.exit(0)
  1657. outputpath = os.path.abspath(args[-1])
  1658. rundir = os.getcwd()
  1659. try:
  1660. os.makedirs(outputpath)
  1661. except OSError:
  1662. pass
  1663. if not os.path.isdir(outputpath):
  1664. print('FATAL: Output path is not a directory or does not exist')
  1665. sys.exit(1)
  1666. if not getgnuplotversion():
  1667. print('gnuplot not found')
  1668. sys.exit(1)
  1669. print('Output path: %s' % outputpath)
  1670. cachefile = os.path.join(outputpath, 'gitstats.cache')
  1671. data = GitDataCollector()
  1672. data.loadCache(cachefile)
  1673. for gitpath in args[0:-1]:
  1674. print('Git path: %s' % gitpath)
  1675. prevdir = os.getcwd()
  1676. os.chdir(gitpath)
  1677. print('Collecting data...')
  1678. data.collect(gitpath)
  1679. os.chdir(prevdir)
  1680. print('Refining data...')
  1681. data.saveCache(cachefile)
  1682. data.refine()
  1683. os.chdir(rundir)
  1684. print('Generating HTML report...')
  1685. report = HTMLReportCreator()
  1686. report.create(data, outputpath)
  1687. print('Generating JSON report...')
  1688. report = JSONReportCreator()
  1689. report.create(data, os.path.join(outputpath, JSONFILE))
  1690. time_end = time.time()
  1691. exectime_internal = time_end - time_start
  1692. print('Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal))
  1693. if sys.stdin.isatty():
  1694. print('You may now run:')
  1695. print()
  1696. print(' sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''"))
  1697. print(' sensible-notepad \'%s\'' % os.path.join(outputpath, JSONFILE).replace("'", "'\\''"))
  1698. print()
  1699. if __name__=='__main__':
  1700. g = GitStats()
  1701. g.run(sys.argv[1:])