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