123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771
  1. #!/usr/bin/python
  2. # Copyright (c) 2007 Heikki Hokkanen <hoxu@users.sf.net>
  3. # GPLv2
  4. import commands
  5. import datetime
  6. import glob
  7. import os
  8. import re
  9. import shutil
  10. import sys
  11. import time
  12. GNUPLOT_COMMON = 'set terminal png transparent\nset size 0.5,0.5\n'
  13. def getoutput(cmd, quiet = False):
  14. if not quiet:
  15. print '>> %s' % cmd
  16. output = commands.getoutput(cmd)
  17. return output
  18. def getkeyssortedbyvalues(dict):
  19. return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
  20. # TODO getdictkeyssortedbyvaluekey(dict, key) - eg. dict['author'] = { 'commits' : 512 } - ...key(dict, 'commits')
  21. class DataCollector:
  22. """Manages data collection from a revision control repository."""
  23. def __init__(self):
  24. self.stamp_created = time.time()
  25. pass
  26. ##
  27. # This should be the main function to extract data from the repository.
  28. def collect(self, dir):
  29. self.dir = dir
  30. ##
  31. # : get a dictionary of author
  32. def getAuthorInfo(self, author):
  33. return None
  34. def getActivityByDayOfWeek(self):
  35. return {}
  36. def getActivityByHourOfDay(self):
  37. return {}
  38. ##
  39. # Get a list of authors
  40. def getAuthors(self):
  41. return []
  42. def getFirstCommitDate(self):
  43. return datetime.datetime.now()
  44. def getLastCommitDate(self):
  45. return datetime.datetime.now()
  46. def getStampCreated(self):
  47. return self.stamp_created
  48. def getTags(self):
  49. return []
  50. def getTotalAuthors(self):
  51. return -1
  52. def getTotalCommits(self):
  53. return -1
  54. def getTotalFiles(self):
  55. return -1
  56. def getTotalLOC(self):
  57. return -1
  58. class GitDataCollector(DataCollector):
  59. def collect(self, dir):
  60. DataCollector.collect(self, dir)
  61. self.total_authors = int(getoutput('git-log |git-shortlog -s |wc -l'))
  62. self.total_commits = int(getoutput('git-rev-list HEAD |wc -l'))
  63. self.total_files = int(getoutput('git-ls-files |wc -l'))
  64. self.total_lines = int(getoutput('git-ls-files -z |xargs -0 cat |wc -l'))
  65. self.activity_by_hour_of_day = {} # hour -> commits
  66. self.activity_by_day_of_week = {} # day -> commits
  67. self.activity_by_month_of_year = {} # month [1-12] -> commits
  68. self.activity_by_hour_of_week = {} # weekday -> hour -> commits
  69. self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp}
  70. # author of the month
  71. self.author_of_month = {} # month -> author -> commits
  72. self.author_of_year = {} # year -> author -> commits
  73. self.commits_by_month = {} # month -> commits
  74. self.commits_by_year = {} # year -> commits
  75. self.first_commit_stamp = 0
  76. self.last_commit_stamp = 0
  77. # tags
  78. self.tags = {}
  79. lines = getoutput('git-show-ref --tags').split('\n')
  80. for line in lines:
  81. if len(line) == 0:
  82. continue
  83. (hash, tag) = line.split(' ')
  84. tag = tag.replace('refs/tags/', '')
  85. output = getoutput('git-log "%s" --pretty=format:"%%at %%an" -n 1' % hash)
  86. if len(output) > 0:
  87. parts = output.split(' ')
  88. stamp = 0
  89. try:
  90. stamp = int(parts[0])
  91. except ValueError:
  92. stamp = 0
  93. self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d') }
  94. pass
  95. # Collect revision statistics
  96. # Outputs "<stamp> <author>"
  97. lines = getoutput('git-rev-list --pretty=format:"%at %an" HEAD |grep -v ^commit').split('\n')
  98. for line in lines:
  99. # linux-2.6 says "<unknown>" for one line O_o
  100. parts = line.split(' ')
  101. author = ''
  102. try:
  103. stamp = int(parts[0])
  104. except ValueError:
  105. stamp = 0
  106. if len(parts) > 1:
  107. author = ' '.join(parts[1:])
  108. date = datetime.datetime.fromtimestamp(float(stamp))
  109. # First and last commit stamp
  110. if self.last_commit_stamp == 0:
  111. self.last_commit_stamp = stamp
  112. self.first_commit_stamp = stamp
  113. # activity
  114. # hour
  115. hour = date.hour
  116. if hour in self.activity_by_hour_of_day:
  117. self.activity_by_hour_of_day[hour] += 1
  118. else:
  119. self.activity_by_hour_of_day[hour] = 1
  120. # day of week
  121. day = date.weekday()
  122. if day in self.activity_by_day_of_week:
  123. self.activity_by_day_of_week[day] += 1
  124. else:
  125. self.activity_by_day_of_week[day] = 1
  126. # hour of week
  127. if day not in self.activity_by_hour_of_week:
  128. self.activity_by_hour_of_week[day] = {}
  129. if hour not in self.activity_by_hour_of_week[day]:
  130. self.activity_by_hour_of_week[day][hour] = 1
  131. else:
  132. self.activity_by_hour_of_week[day][hour] += 1
  133. # month of year
  134. month = date.month
  135. if month in self.activity_by_month_of_year:
  136. self.activity_by_month_of_year[month] += 1
  137. else:
  138. self.activity_by_month_of_year[month] = 1
  139. # author stats
  140. if author not in self.authors:
  141. self.authors[author] = {}
  142. # TODO commits
  143. if 'last_commit_stamp' not in self.authors[author]:
  144. self.authors[author]['last_commit_stamp'] = stamp
  145. self.authors[author]['first_commit_stamp'] = stamp
  146. if 'commits' in self.authors[author]:
  147. self.authors[author]['commits'] += 1
  148. else:
  149. self.authors[author]['commits'] = 1
  150. # author of the month/year
  151. yymm = datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m')
  152. if yymm in self.author_of_month:
  153. if author in self.author_of_month[yymm]:
  154. self.author_of_month[yymm][author] += 1
  155. else:
  156. self.author_of_month[yymm][author] = 1
  157. else:
  158. self.author_of_month[yymm] = {}
  159. self.author_of_month[yymm][author] = 1
  160. if yymm in self.commits_by_month:
  161. self.commits_by_month[yymm] += 1
  162. else:
  163. self.commits_by_month[yymm] = 1
  164. yy = datetime.datetime.fromtimestamp(stamp).year
  165. if yy in self.author_of_year:
  166. if author in self.author_of_year[yy]:
  167. self.author_of_year[yy][author] += 1
  168. else:
  169. self.author_of_year[yy][author] = 1
  170. else:
  171. self.author_of_year[yy] = {}
  172. self.author_of_year[yy][author] = 1
  173. if yy in self.commits_by_year:
  174. self.commits_by_year[yy] += 1
  175. else:
  176. self.commits_by_year[yy] = 1
  177. # outputs "<stamp> <files>" for each revision
  178. self.files_by_stamp = {} # stamp -> files
  179. lines = getoutput('git-rev-list --pretty=format:"%at %H" HEAD |grep -v ^commit |while read line; do set $line; echo "$1 $(git-ls-tree -r "$2" |wc -l)"; done').split('\n')
  180. for line in lines:
  181. parts = line.split(' ')
  182. if len(parts) != 2:
  183. continue
  184. (stamp, files) = parts[0:2]
  185. self.files_by_stamp[int(stamp)] = int(files)
  186. # extensions
  187. self.extensions = {} # extension -> files, lines
  188. lines = getoutput('git-ls-files').split('\n')
  189. for line in lines:
  190. base = os.path.basename(line)
  191. if base.find('.') == -1:
  192. ext = ''
  193. else:
  194. ext = base[(base.rfind('.') + 1):]
  195. if ext not in self.extensions:
  196. self.extensions[ext] = {'files': 0, 'lines': 0}
  197. self.extensions[ext]['files'] += 1
  198. try:
  199. # FIXME filenames with spaces or special characters are broken
  200. self.extensions[ext]['lines'] += int(getoutput('wc -l < %s' % line, quiet = True))
  201. except:
  202. print 'Warning: Could not count lines for file "%s"' % line
  203. # line statistics
  204. # outputs:
  205. # N files changed, N insertions (+), N deletions(-)
  206. # <stamp> <author>
  207. self.changes_by_date = {} # stamp -> { files, ins, del }
  208. lines = getoutput('git-log --shortstat --pretty=format:"%at %an" |tac').split('\n')
  209. files = 0; inserted = 0; deleted = 0; total_lines = 0
  210. for line in lines:
  211. # <stamp> <author>
  212. if line.find(',') == -1:
  213. pos = line.find(' ')
  214. (stamp, author) = (line[:pos], line[pos+1:])
  215. self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
  216. else:
  217. numbers = re.findall('\d+', line)
  218. if len(numbers) == 3:
  219. (files, inserted, deleted) = map(lambda el : int(el), numbers)
  220. total_lines += inserted
  221. total_lines -= deleted
  222. else:
  223. print 'Warning: failed to handle line "%s"' % line
  224. (files, inserted, deleted) = (0, 0, 0)
  225. #self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
  226. def getActivityByDayOfWeek(self):
  227. return self.activity_by_day_of_week
  228. def getActivityByHourOfDay(self):
  229. return self.activity_by_hour_of_day
  230. def getAuthorInfo(self, author):
  231. a = self.authors[author]
  232. commits = a['commits']
  233. commits_frac = (100 * float(commits)) / self.getTotalCommits()
  234. date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp'])
  235. date_last = datetime.datetime.fromtimestamp(a['last_commit_stamp'])
  236. delta = date_last - date_first
  237. res = { 'commits': commits, 'commits_frac': commits_frac, 'date_first': date_first.strftime('%Y-%m-%d'), 'date_last': date_last.strftime('%Y-%m-%d'), 'timedelta' : delta }
  238. return res
  239. def getAuthors(self):
  240. return self.authors.keys()
  241. def getFirstCommitDate(self):
  242. return datetime.datetime.fromtimestamp(self.first_commit_stamp)
  243. def getLastCommitDate(self):
  244. return datetime.datetime.fromtimestamp(self.last_commit_stamp)
  245. def getTags(self):
  246. lines = getoutput('git-show-ref --tags |cut -d/ -f3')
  247. return lines.split('\n')
  248. def getTagDate(self, tag):
  249. return self.revToDate('tags/' + tag)
  250. def getTotalAuthors(self):
  251. return self.total_authors
  252. def getTotalCommits(self):
  253. return self.total_commits
  254. def getTotalFiles(self):
  255. return self.total_files
  256. def getTotalLOC(self):
  257. return self.total_lines
  258. def revToDate(self, rev):
  259. stamp = int(getoutput('git-log --pretty=format:%%at "%s" -n 1' % rev))
  260. return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d')
  261. class ReportCreator:
  262. """Creates the actual report based on given data."""
  263. def __init__(self):
  264. pass
  265. def create(self, data, path):
  266. self.data = data
  267. self.path = path
  268. def html_linkify(text):
  269. return text.lower().replace(' ', '_')
  270. def html_header(level, text):
  271. name = html_linkify(text)
  272. return '\n<h%d><a href="#%s" name="%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
  273. class HTMLReportCreator(ReportCreator):
  274. def create(self, data, path):
  275. ReportCreator.create(self, data, path)
  276. # TODO copy the CSS if it does not exist
  277. if not os.path.exists(path + '/gitstats.css'):
  278. #shutil.copyfile('')
  279. pass
  280. f = open(path + "/index.html", 'w')
  281. format = '%Y-%m-%d %H:%m:%S'
  282. self.printHeader(f)
  283. f.write('<h1>GitStats</h1>')
  284. self.printNav(f)
  285. f.write('<dl>');
  286. f.write('<dt>Generated</dt><dd>%s (in %d seconds)</dd>' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated()));
  287. f.write('<dt>Report Period</dt><dd>%s to %s</dd>' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format)))
  288. f.write('<dt>Total Files</dt><dd>%s</dd>' % data.getTotalFiles())
  289. f.write('<dt>Total Lines of Code</dt><dd>%s</dd>' % data.getTotalLOC())
  290. f.write('<dt>Total Commits</dt><dd>%s</dd>' % data.getTotalCommits())
  291. f.write('<dt>Authors</dt><dd>%s</dd>' % data.getTotalAuthors())
  292. f.write('</dl>');
  293. f.write('</body>\n</html>');
  294. f.close()
  295. ###
  296. # Activity
  297. f = open(path + '/activity.html', 'w')
  298. self.printHeader(f)
  299. f.write('<h1>Activity</h1>')
  300. self.printNav(f)
  301. #f.write('<h2>Last 30 days</h2>')
  302. #f.write('<h2>Last 12 months</h2>')
  303. # Hour of Day
  304. f.write(html_header(2, 'Hour of Day'))
  305. hour_of_day = data.getActivityByHourOfDay()
  306. f.write('<table><tr><th>Hour</th>')
  307. for i in range(1, 25):
  308. f.write('<th>%d</th>' % i)
  309. f.write('</tr>\n<tr><th>Commits</th>')
  310. fp = open(path + '/hour_of_day.dat', 'w')
  311. for i in range(0, 24):
  312. if i in hour_of_day:
  313. f.write('<td>%d</td>' % hour_of_day[i])
  314. fp.write('%d %d\n' % (i, hour_of_day[i]))
  315. else:
  316. f.write('<td>0</td>')
  317. fp.write('%d 0\n' % i)
  318. fp.close()
  319. f.write('</tr>\n<tr><th>%</th>')
  320. totalcommits = data.getTotalCommits()
  321. for i in range(0, 24):
  322. if i in hour_of_day:
  323. f.write('<td>%.2f</td>' % ((100.0 * hour_of_day[i]) / totalcommits))
  324. else:
  325. f.write('<td>0.00</td>')
  326. f.write('</tr></table>')
  327. f.write('<img src="hour_of_day.png" alt="Hour of Day" />')
  328. fg = open(path + '/hour_of_day.dat', 'w')
  329. for i in range(0, 24):
  330. if i in hour_of_day:
  331. fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
  332. else:
  333. fg.write('%d 0\n' % (i + 1))
  334. fg.close()
  335. # Day of Week
  336. f.write(html_header(2, 'Day of Week'))
  337. day_of_week = data.getActivityByDayOfWeek()
  338. f.write('<div class="vtable"><table>')
  339. f.write('<tr><th>Day</th><th>Total (%)</th></tr>')
  340. fp = open(path + '/day_of_week.dat', 'w')
  341. for d in range(0, 7):
  342. commits = 0
  343. if d in day_of_week:
  344. commits = day_of_week[d]
  345. fp.write('%d %d\n' % (d + 1, commits))
  346. f.write('<tr>')
  347. f.write('<th>%d</th>' % (d + 1))
  348. if d in day_of_week:
  349. f.write('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits))
  350. else:
  351. f.write('<td>0</td>')
  352. f.write('</tr>')
  353. f.write('</table></div>')
  354. f.write('<img src="day_of_week.png" alt="Day of Week" />')
  355. fp.close()
  356. # Hour of Week
  357. f.write(html_header(2, 'Hour of Week'))
  358. f.write('<table>')
  359. f.write('<tr><th>Weekday</th>')
  360. for hour in range(0, 24):
  361. f.write('<th>%d</th>' % (hour + 1))
  362. f.write('</tr>')
  363. for weekday in range(0, 7):
  364. f.write('<tr><th>%d</th>' % (weekday + 1))
  365. for hour in range(0, 24):
  366. try:
  367. commits = data.activity_by_hour_of_week[weekday][hour]
  368. except KeyError:
  369. commits = 0
  370. if commits != 0:
  371. f.write('<td>%d</td>' % commits)
  372. else:
  373. f.write('<td></td>')
  374. f.write('</tr>')
  375. f.write('</table>')
  376. # Month of Year
  377. f.write(html_header(2, 'Month of Year'))
  378. f.write('<div class="vtable"><table>')
  379. f.write('<tr><th>Month</th><th>Commits (%)</th></tr>')
  380. fp = open (path + '/month_of_year.dat', 'w')
  381. for mm in range(1, 13):
  382. commits = 0
  383. if mm in data.activity_by_month_of_year:
  384. commits = data.activity_by_month_of_year[mm]
  385. f.write('<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalCommits()))
  386. fp.write('%d %d\n' % (mm, commits))
  387. fp.close()
  388. f.write('</table></div>')
  389. f.write('<img src="month_of_year.png" alt="Month of Year" />')
  390. # Commits by year/month
  391. f.write(html_header(2, 'Commits by year/month'))
  392. f.write('<div class="vtable"><table><tr><th>Month</th><th>Commits</th></tr>')
  393. for yymm in reversed(sorted(data.commits_by_month.keys())):
  394. f.write('<tr><td>%s</td><td>%d</td></tr>' % (yymm, data.commits_by_month[yymm]))
  395. f.write('</table></div>')
  396. f.write('<img src="commits_by_year_month.png" alt="Commits by year/month" />')
  397. fg = open(path + '/commits_by_year_month.dat', 'w')
  398. for yymm in sorted(data.commits_by_month.keys()):
  399. fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
  400. fg.close()
  401. # Commits by year
  402. f.write(html_header(2, 'Commits by Year'))
  403. f.write('<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th></tr>')
  404. for yy in reversed(sorted(data.commits_by_year.keys())):
  405. f.write('<tr><td>%s</td><td>%d (%.2f%%)</td></tr>' % (yy, data.commits_by_year[yy], (100.0 * data.commits_by_year[yy]) / data.getTotalCommits()))
  406. f.write('</table></div>')
  407. f.write('<img src="commits_by_year.png" alt="Commits by Year" />')
  408. fg = open(path + '/commits_by_year.dat', 'w')
  409. for yy in sorted(data.commits_by_year.keys()):
  410. fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
  411. fg.close()
  412. f.write('</body></html>')
  413. f.close()
  414. ###
  415. # Authors
  416. f = open(path + '/authors.html', 'w')
  417. self.printHeader(f)
  418. f.write('<h1>Authors</h1>')
  419. self.printNav(f)
  420. # Authors :: List of authors
  421. f.write(html_header(2, 'List of Authors'))
  422. f.write('<table class="authors">')
  423. f.write('<tr><th>Author</th><th>Commits (%)</th><th>First commit</th><th>Last commit</th><th>Age</th></tr>')
  424. for author in sorted(data.getAuthors()):
  425. info = data.getAuthorInfo(author)
  426. f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%s</td><td>%s</td><td>%s</td></tr>' % (author, info['commits'], info['commits_frac'], info['date_first'], info['date_last'], info['timedelta']))
  427. f.write('</table>')
  428. # Authors :: Author of Month
  429. f.write(html_header(2, 'Author of Month'))
  430. f.write('<table>')
  431. f.write('<tr><th>Month</th><th>Author</th><th>Commits (%)</th></tr>')
  432. for yymm in reversed(sorted(data.author_of_month.keys())):
  433. authordict = data.author_of_month[yymm]
  434. authors = getkeyssortedbyvalues(authordict)
  435. authors.reverse()
  436. commits = data.author_of_month[yymm][authors[0]]
  437. f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td></tr>' % (yymm, authors[0], commits, (100 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm]))
  438. f.write('</table>')
  439. f.write(html_header(2, 'Author of Year'))
  440. f.write('<table><tr><th>Year</th><th>Author</th><th>Commits (%)</th></tr>')
  441. for yy in reversed(sorted(data.author_of_year.keys())):
  442. authordict = data.author_of_year[yy]
  443. authors = getkeyssortedbyvalues(authordict)
  444. authors.reverse()
  445. commits = data.author_of_year[yy][authors[0]]
  446. f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td></tr>' % (yy, authors[0], commits, (100 * commits) / data.commits_by_year[yy], data.commits_by_year[yy]))
  447. f.write('</table>')
  448. f.write('</body></html>')
  449. f.close()
  450. ###
  451. # Files
  452. f = open(path + '/files.html', 'w')
  453. self.printHeader(f)
  454. f.write('<h1>Files</h1>')
  455. self.printNav(f)
  456. f.write('<dl>\n')
  457. f.write('<dt>Total files</dt><dd>%d</dd>' % data.getTotalFiles())
  458. f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
  459. f.write('<dt>Average file size</dt><dd>%.2f bytes</dd>' % ((100.0 * data.getTotalLOC()) / data.getTotalFiles()))
  460. f.write('</dl>\n')
  461. # Files :: File count by date
  462. f.write(html_header(2, 'File count by date'))
  463. fg = open(path + '/files_by_date.dat', 'w')
  464. for stamp in sorted(data.files_by_stamp.keys()):
  465. fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
  466. fg.close()
  467. f.write('<img src="files_by_date.png" alt="Files by Date" />')
  468. #f.write('<h2>Average file size by date</h2>')
  469. # Files :: Extensions
  470. f.write(html_header(2, 'Extensions'))
  471. f.write('<table><tr><th>Extension</th><th>Files (%)</th><th>Lines (%)</th><th>Lines/file</th></tr>')
  472. for ext in sorted(data.extensions.keys()):
  473. files = data.extensions[ext]['files']
  474. lines = data.extensions[ext]['lines']
  475. f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d (%.2f%%)</td><td>%d</td></tr>' % (ext, files, (100.0 * files) / data.getTotalFiles(), lines, (100.0 * lines) / data.getTotalLOC(), lines / files))
  476. f.write('</table>')
  477. f.write('</body></html>')
  478. f.close()
  479. ###
  480. # Lines
  481. f = open(path + '/lines.html', 'w')
  482. self.printHeader(f)
  483. f.write('<h1>Lines</h1>')
  484. self.printNav(f)
  485. f.write('<dl>\n')
  486. f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
  487. f.write('</dl>\n')
  488. f.write('</body></html>')
  489. f.close()
  490. ###
  491. # tags.html
  492. f = open(path + '/tags.html', 'w')
  493. self.printHeader(f)
  494. f.write('<h1>Tags</h1>')
  495. self.printNav(f)
  496. f.write('<dl>')
  497. f.write('<dt>Total tags</dt><dd>%d</dd>' % len(data.tags))
  498. if len(data.tags) > 0:
  499. f.write('<dt>Average commits per tag</dt><dd>%.2f</dd>' % (data.getTotalCommits() / len(data.tags)))
  500. f.write('</dl>')
  501. f.write('<table>')
  502. f.write('<tr><th>Name</th><th>Date</th></tr>')
  503. # sort the tags by date desc
  504. tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
  505. for tag in tags_sorted_by_date_desc:
  506. f.write('<tr><td>%s</td><td>%s</td></tr>' % (tag, data.tags[tag]['date']))
  507. f.write('</table>')
  508. f.write('</body></html>')
  509. f.close()
  510. self.createGraphs(path)
  511. pass
  512. def createGraphs(self, path):
  513. print 'Generating graphs...'
  514. # hour of day
  515. f = open(path + '/hour_of_day.plot', 'w')
  516. f.write(GNUPLOT_COMMON)
  517. f.write(
  518. """
  519. set output 'hour_of_day.png'
  520. unset key
  521. set xrange [0.5:24.5]
  522. set xtics 4
  523. set ylabel "Commits"
  524. plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
  525. """)
  526. f.close()
  527. # day of week
  528. f = open(path + '/day_of_week.plot', 'w')
  529. f.write(GNUPLOT_COMMON)
  530. f.write(
  531. """
  532. set output 'day_of_week.png'
  533. unset key
  534. set xrange [0.5:7.5]
  535. set xtics 1
  536. set ylabel "Commits"
  537. plot 'day_of_week.dat' using 1:2:(0.5) w boxes fs solid
  538. """)
  539. f.close()
  540. # Month of Year
  541. f = open(path + '/month_of_year.plot', 'w')
  542. f.write(GNUPLOT_COMMON)
  543. f.write(
  544. """
  545. set output 'month_of_year.png'
  546. unset key
  547. set xrange [0.5:12.5]
  548. set xtics 1
  549. set ylabel "Commits"
  550. plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid
  551. """)
  552. f.close()
  553. # commits_by_year_month
  554. f = open(path + '/commits_by_year_month.plot', 'w')
  555. f.write(GNUPLOT_COMMON)
  556. f.write(
  557. """
  558. set output 'commits_by_year_month.png'
  559. unset key
  560. set xdata time
  561. set timefmt "%Y-%m"
  562. set format x "%Y-%m"
  563. set xtics rotate by 90 15768000
  564. set ylabel "Commits"
  565. plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
  566. """)
  567. f.close()
  568. # commits_by_year
  569. f = open(path + '/commits_by_year.plot', 'w')
  570. f.write(GNUPLOT_COMMON)
  571. f.write(
  572. """
  573. set output 'commits_by_year.png'
  574. unset key
  575. set xtics 1
  576. set ylabel "Commits"
  577. plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
  578. """)
  579. f.close()
  580. # Files by date
  581. f = open(path + '/files_by_date.plot', 'w')
  582. f.write(GNUPLOT_COMMON)
  583. f.write(
  584. """
  585. set output 'files_by_date.png'
  586. unset key
  587. set xdata time
  588. set timefmt "%Y-%m-%d"
  589. set format x "%Y-%m-%d"
  590. set ylabel "Files"
  591. set xtics rotate by 90
  592. plot 'files_by_date.dat' using 1:2 smooth csplines
  593. """)
  594. f.close()
  595. os.chdir(path)
  596. files = glob.glob(path + '/*.plot')
  597. for f in files:
  598. print '>> gnuplot %s' % os.path.basename(f)
  599. os.system('gnuplot %s' % f)
  600. def printHeader(self, f):
  601. f.write(
  602. """<?xml version="1.0" encoding="UTF-8"?>
  603. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  604. <html xmlns="http://www.w3.org/1999/xhtml">
  605. <head>
  606. <title>GitStats</title>
  607. <link rel="stylesheet" href="gitstats.css" type="text/css" />
  608. <meta name="generator" content="GitStats" />
  609. </head>
  610. <body>
  611. """)
  612. def printNav(self, f):
  613. f.write("""
  614. <div class="nav">
  615. <ul>
  616. <li><a href="index.html">General</a></li>
  617. <li><a href="activity.html">Activity</a></li>
  618. <li><a href="authors.html">Authors</a></li>
  619. <li><a href="files.html">Files</a></li>
  620. <li><a href="lines.html">Lines</a></li>
  621. <li><a href="tags.html">Tags</a></li>
  622. </ul>
  623. </div>
  624. """)
  625. usage = """
  626. Usage: gitstats [options] <gitpath> <outputpath>
  627. Options:
  628. """
  629. if len(sys.argv) < 3:
  630. print usage
  631. sys.exit(0)
  632. gitpath = sys.argv[1]
  633. outputpath = os.path.abspath(sys.argv[2])
  634. try:
  635. os.makedirs(outputpath)
  636. except OSError:
  637. pass
  638. if not os.path.isdir(outputpath):
  639. print 'FATAL: Output path is not a directory or does not exist'
  640. sys.exit(1)
  641. print 'Git path: %s' % gitpath
  642. print 'Output path: %s' % outputpath
  643. os.chdir(gitpath)
  644. print 'Collecting data...'
  645. data = GitDataCollector()
  646. data.collect(gitpath)
  647. print 'Generating report...'
  648. report = HTMLReportCreator()
  649. report.create(data, outputpath)