gitstats 24KB

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