statgit 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. #!/usr/bin/python
  2. # Copyright (c) 2007 Heikki Hokkanen <hoxu@users.sf.net>
  3. # GPLv2
  4. import commands
  5. import datetime
  6. import os
  7. import re
  8. import sys
  9. GNUPLOT_COMMON = 'set terminal png\nset size 0.5,0.5\n'
  10. def getoutput(cmd):
  11. print '>> %s' % cmd
  12. output = commands.getoutput(cmd)
  13. return output
  14. def getkeyssortedbyvalues(dict):
  15. return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
  16. # TODO getdictkeyssortedbyvaluekey(dict, key) - eg. dict['author'] = { 'commits' : 512 } - ...key(dict, 'commits')
  17. class DataCollector:
  18. def __init__(self):
  19. pass
  20. ##
  21. # This should be the main function to extract data from the repository.
  22. def collect(self, dir):
  23. self.dir = dir
  24. ##
  25. # : get a dictionary of author
  26. def getAuthorInfo(self, author):
  27. return None
  28. def getActivityByDayOfWeek(self):
  29. return {}
  30. def getActivityByHourOfDay(self):
  31. return {}
  32. ##
  33. # Get a list of authors
  34. def getAuthors(self):
  35. return []
  36. def getFirstCommitDate(self):
  37. return datetime.datetime.now()
  38. def getLastCommitDate(self):
  39. return datetime.datetime.now()
  40. def getTags(self):
  41. return []
  42. def getTotalAuthors(self):
  43. return -1
  44. def getTotalCommits(self):
  45. return -1
  46. def getTotalFiles(self):
  47. return -1
  48. def getTotalLOC(self):
  49. return -1
  50. class GitDataCollector(DataCollector):
  51. def collect(self, dir):
  52. DataCollector.collect(self, dir)
  53. self.total_authors = int(getoutput('git-log |git-shortlog -s |wc -l'))
  54. self.total_commits = int(getoutput('git-rev-list HEAD |wc -l'))
  55. self.total_files = int(getoutput('git-ls-files |wc -l'))
  56. self.total_lines = int(getoutput('git-ls-files |xargs cat |wc -l'))
  57. self.activity_by_hour_of_day = {} # hour -> commits
  58. self.activity_by_day_of_week = {} # day -> commits
  59. self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp}
  60. # author of the month
  61. self.author_of_month = {} # month -> author -> commits
  62. self.author_of_year = {} # year -> author -> commits
  63. self.commits_by_month = {} # month -> commits
  64. self.commits_by_year = {} # year -> commits
  65. self.first_commit_stamp = 0
  66. self.last_commit_stamp = 0
  67. # tags
  68. self.tags = {}
  69. lines = getoutput('git-show-ref --tags').split('\n')
  70. for line in lines:
  71. if len(line) == 0:
  72. continue
  73. (hash, tag) = line.split(' ')
  74. tag = tag.replace('refs/tags/', '')
  75. output = getoutput('git-log "%s" --pretty=format:"%%at %%an" -n 1' % hash)
  76. if len(output) > 0:
  77. parts = output.split(' ')
  78. stamp = 0
  79. try:
  80. stamp = int(parts[0])
  81. except ValueError:
  82. stamp = 0
  83. self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d') }
  84. pass
  85. # TODO also collect statistics for "last 30 days"/"last 12 months"
  86. lines = getoutput('git-rev-list --pretty=format:"%at %an" HEAD |grep -v ^commit').split('\n')
  87. for line in lines:
  88. # linux-2.6 says "<unknown>" for one line O_o
  89. parts = line.split(' ')
  90. author = ''
  91. try:
  92. stamp = int(parts[0])
  93. except ValueError:
  94. stamp = 0
  95. if len(parts) > 1:
  96. author = ' '.join(parts[1:])
  97. date = datetime.datetime.fromtimestamp(float(stamp))
  98. # First and last commit stamp
  99. if self.last_commit_stamp == 0:
  100. self.last_commit_stamp = stamp
  101. self.first_commit_stamp = stamp
  102. # activity
  103. # hour
  104. hour = date.hour
  105. if hour in self.activity_by_hour_of_day:
  106. self.activity_by_hour_of_day[hour] += 1
  107. else:
  108. self.activity_by_hour_of_day[hour] = 1
  109. # day
  110. day = date.weekday()
  111. if day in self.activity_by_day_of_week:
  112. self.activity_by_day_of_week[day] += 1
  113. else:
  114. self.activity_by_day_of_week[day] = 1
  115. # author stats
  116. if author not in self.authors:
  117. self.authors[author] = {}
  118. # TODO commits
  119. if 'last_commit_stamp' not in self.authors[author]:
  120. self.authors[author]['last_commit_stamp'] = stamp
  121. self.authors[author]['first_commit_stamp'] = stamp
  122. if 'commits' in self.authors[author]:
  123. self.authors[author]['commits'] += 1
  124. else:
  125. self.authors[author]['commits'] = 1
  126. # author of the month/year
  127. yymm = datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m')
  128. if yymm in self.author_of_month:
  129. if author in self.author_of_month[yymm]:
  130. self.author_of_month[yymm][author] += 1
  131. else:
  132. self.author_of_month[yymm][author] = 1
  133. else:
  134. self.author_of_month[yymm] = {}
  135. self.author_of_month[yymm][author] = 1
  136. if yymm in self.commits_by_month:
  137. self.commits_by_month[yymm] += 1
  138. else:
  139. self.commits_by_month[yymm] = 1
  140. yy = datetime.datetime.fromtimestamp(stamp).year
  141. if yy in self.author_of_year:
  142. if author in self.author_of_year[yy]:
  143. self.author_of_year[yy][author] += 1
  144. else:
  145. self.author_of_year[yy][author] = 1
  146. else:
  147. self.author_of_year[yy] = {}
  148. self.author_of_year[yy][author] = 1
  149. if yy in self.commits_by_year:
  150. self.commits_by_year[yy] += 1
  151. else:
  152. self.commits_by_year[yy] = 1
  153. def getActivityByDayOfWeek(self):
  154. return self.activity_by_day_of_week
  155. def getActivityByHourOfDay(self):
  156. return self.activity_by_hour_of_day
  157. def getAuthorInfo(self, author):
  158. a = self.authors[author]
  159. commits = a['commits']
  160. commits_frac = (100 * float(commits)) / self.getTotalCommits()
  161. date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp']).strftime('%Y-%m-%d')
  162. date_last = datetime.datetime.fromtimestamp(a['last_commit_stamp']).strftime('%Y-%m-%d')
  163. res = { 'commits': commits, 'commits_frac': commits_frac, 'date_first': date_first, 'date_last': date_last }
  164. return res
  165. def getAuthors(self):
  166. return self.authors.keys()
  167. def getFirstCommitDate(self):
  168. return datetime.datetime.fromtimestamp(self.first_commit_stamp)
  169. def getLastCommitDate(self):
  170. return datetime.datetime.fromtimestamp(self.last_commit_stamp)
  171. def getTags(self):
  172. lines = getoutput('git-show-ref --tags |cut -d/ -f3')
  173. return lines.split('\n')
  174. def getTagDate(self, tag):
  175. return self.revToDate('tags/' + tag)
  176. def getTotalAuthors(self):
  177. return self.total_authors
  178. def getTotalCommits(self):
  179. return self.total_commits
  180. def getTotalFiles(self):
  181. return self.total_files
  182. def getTotalLOC(self):
  183. return self.total_lines
  184. def revToDate(self, rev):
  185. stamp = int(getoutput('git-log --pretty=format:%%at "%s" -n 1' % rev))
  186. return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d')
  187. class ReportCreator:
  188. def __init__(self):
  189. pass
  190. def create(self, data, path):
  191. self.data = data
  192. self.path = path
  193. class HTMLReportCreator(ReportCreator):
  194. def create(self, data, path):
  195. ReportCreator.create(self, data, path)
  196. f = open(path + "/index.html", 'w')
  197. format = '%Y-%m-%d %H:%m:%S'
  198. self.printHeader(f)
  199. f.write('<h1>StatGit</h1>')
  200. self.printNav(f)
  201. f.write('<dl>');
  202. f.write('<dt>Generated</dt><dd>%s</dd>' % datetime.datetime.now().strftime(format));
  203. f.write('<dt>Report Period</dt><dd>%s to %s</dd>' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format)))
  204. f.write('<dt>Total Files</dt><dd>%s</dd>' % data.getTotalFiles())
  205. f.write('<dt>Total Lines of Code</dt><dd>%s</dd>' % data.getTotalLOC())
  206. f.write('<dt>Total Commits</dt><dd>%s</dd>' % data.getTotalCommits())
  207. f.write('<dt>Authors</dt><dd>%s</dd>' % data.getTotalAuthors())
  208. f.write('</dl>');
  209. f.write('</body>\n</html>');
  210. f.close()
  211. ###
  212. # Activity
  213. f = open(path + '/activity.html', 'w')
  214. self.printHeader(f)
  215. f.write('<h1>Activity</h1>')
  216. self.printNav(f)
  217. f.write('<h2>Last 30 days</h2>')
  218. f.write('<h2>Last 12 months</h2>')
  219. # Hour of Day
  220. f.write('\n<h2>Hour of Day</h2>\n\n')
  221. hour_of_day = data.getActivityByHourOfDay()
  222. f.write('<table><tr><th>Hour</th>')
  223. for i in range(1, 25):
  224. f.write('<th>%d</th>' % i)
  225. f.write('</tr>\n<tr><th>Commits</th>')
  226. fp = open(path + '/hour_of_day.dat', 'w')
  227. for i in range(0, 24):
  228. if i in hour_of_day:
  229. f.write('<td>%d</td>' % hour_of_day[i])
  230. fp.write('%d %d\n' % (i, hour_of_day[i]))
  231. else:
  232. f.write('<td>0</td>')
  233. fp.write('%d 0\n' % i)
  234. fp.close()
  235. f.write('</tr>\n<tr><th>%</th>')
  236. totalcommits = data.getTotalCommits()
  237. for i in range(0, 24):
  238. if i in hour_of_day:
  239. f.write('<td>%.2f</td>' % ((100.0 * hour_of_day[i]) / totalcommits))
  240. else:
  241. f.write('<td>0.00</td>')
  242. f.write('</tr></table>')
  243. # Day of Week
  244. # TODO show also by hour of weekday?
  245. f.write('\n<h2>Day of Week</h2>\n\n')
  246. day_of_week = data.getActivityByDayOfWeek()
  247. f.write('<table>')
  248. f.write('<tr><th>Day</th><th>Total (%)</th></tr>')
  249. fp = open(path + '/day_of_week.dat', 'w')
  250. for d in range(0, 7):
  251. fp.write('%d %d\n' % (d + 1, day_of_week[d]))
  252. f.write('<tr>')
  253. f.write('<th>%d</th>' % (d + 1))
  254. if d in day_of_week:
  255. f.write('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits))
  256. else:
  257. f.write('<td>0</td>')
  258. f.write('</tr>')
  259. f.write('</table>')
  260. fp.close()
  261. # Commits by year/month
  262. f.write('<h2>Commits by year/month</h2>')
  263. f.write('<table><tr><th>Month</th><th>Commits</th></tr>')
  264. for yymm in reversed(sorted(data.commits_by_month.keys())):
  265. f.write('<tr><td>%s</td><td>%d</td></tr>' % (yymm, data.commits_by_month[yymm]))
  266. f.write('</table>')
  267. f.write('<img src="commits_by_year_month.png" />')
  268. fg = open(path + '/commits_by_year_month.dat', 'w')
  269. for yymm in sorted(data.commits_by_month.keys()):
  270. fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
  271. fg.close()
  272. # Commits by year
  273. f.write('<h2>Commits by year</h2>')
  274. f.write('<table><tr><th>Year</th><th>Commits (% of all)</th></tr>')
  275. for yy in reversed(sorted(data.commits_by_year.keys())):
  276. 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()))
  277. f.write('</table>')
  278. f.write('<img src="commits_by_year.png" />')
  279. fg = open(path + '/commits_by_year.dat', 'w')
  280. for yy in sorted(data.commits_by_year.keys()):
  281. fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
  282. fg.close()
  283. f.write('</body></html>')
  284. f.close()
  285. ###
  286. # Authors
  287. f = open(path + '/authors.html', 'w')
  288. self.printHeader(f)
  289. f.write('<h1>Authors</h1>')
  290. self.printNav(f)
  291. f.write('\n<h2>List of authors</h2>\n\n')
  292. f.write('<table class="authors">')
  293. f.write('<tr><th>Author</th><th>Commits (%)</th><th>First commit</th><th>Last commit</th></tr>')
  294. for author in sorted(data.getAuthors()):
  295. info = data.getAuthorInfo(author)
  296. f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%s</td><td>%s</td></tr>' % (author, info['commits'], info['commits_frac'], info['date_first'], info['date_last']))
  297. f.write('</table>')
  298. f.write('\n<h2>Author of Month</h2>\n\n')
  299. f.write('<table>')
  300. f.write('<tr><th>Month</th><th>Author</th><th>Commits (%)</th></tr>')
  301. for yymm in reversed(sorted(data.author_of_month.keys())):
  302. authordict = data.author_of_month[yymm]
  303. authors = getkeyssortedbyvalues(authordict)
  304. authors.reverse()
  305. commits = data.author_of_month[yymm][authors[0]]
  306. 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]))
  307. f.write('</table>')
  308. f.write('\n<h2>Author of Year</h2>\n\n')
  309. f.write('<table><tr><th>Year</th><th>Author</th><th>Commits (%)</th></tr>')
  310. for yy in reversed(sorted(data.author_of_year.keys())):
  311. authordict = data.author_of_year[yy]
  312. authors = getkeyssortedbyvalues(authordict)
  313. authors.reverse()
  314. commits = data.author_of_year[yy][authors[0]]
  315. 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]))
  316. f.write('</table>')
  317. f.write('</body></html>')
  318. f.close()
  319. ###
  320. # Files
  321. f = open(path + '/files.html', 'w')
  322. self.printHeader(f)
  323. f.write('<h1>Files</h1>')
  324. self.printNav(f)
  325. f.write('<h2>File count by date</h2>')
  326. f.write('</body></html>')
  327. f.close()
  328. ###
  329. # tags.html
  330. f = open(path + '/tags.html', 'w')
  331. self.printHeader(f)
  332. f.write('<h1>Tags</h1>')
  333. self.printNav(f)
  334. f.write('<dl>')
  335. f.write('<dt>Total tags</dt><dd>%d</dd>' % len(data.tags))
  336. if len(data.tags) > 0:
  337. f.write('<dt>Average commits per tag</dt><dd>%.2f</dd>' % (data.getTotalCommits() / len(data.tags)))
  338. f.write('</dl>')
  339. f.write('<table>')
  340. f.write('<tr><th>Name</th><th>Date</th></tr>')
  341. # sort the tags by date desc
  342. tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
  343. for tag in tags_sorted_by_date_desc:
  344. f.write('<tr><td>%s</td><td>%s</td></tr>' % (tag, data.tags[tag]['date']))
  345. f.write('</table>')
  346. f.write('</body></html>')
  347. f.close()
  348. self.createGraphs(path)
  349. pass
  350. def createGraphs(self, path):
  351. print 'Generating graphs...'
  352. # commits_by_year_month
  353. f = open(path + '/commits_by_year_month.plot', 'w')
  354. f.write(GNUPLOT_COMMON)
  355. f.write(
  356. # TODO rotate xtic labels by 90 degrees
  357. """
  358. set output 'commits_by_year_month.png'
  359. unset key
  360. set xdata time
  361. set timefmt "%Y-%m"
  362. set format x "%Y-%m"
  363. plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
  364. """)
  365. f.close()
  366. # commits_by_year
  367. f = open(path + '/commits_by_year.plot', 'w')
  368. f.write(GNUPLOT_COMMON)
  369. f.write(
  370. """
  371. set output 'commits_by_year.png'
  372. unset key
  373. set xtics 1
  374. plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
  375. """)
  376. f.close()
  377. os.chdir(path)
  378. for i in ('commits_by_year_month', 'commits_by_year'):
  379. os.system('gnuplot %s.plot' % i)
  380. pass
  381. def printHeader(self, f):
  382. f.write("""<html>
  383. <head>
  384. <title>StatGit</title>
  385. <link rel="stylesheet" href="statgit.css" type="text/css" />
  386. </head>
  387. <body>
  388. """)
  389. def printNav(self, f):
  390. f.write("""
  391. <div class="nav">
  392. <li><a href="index.html">General</a></li>
  393. <li><a href="activity.html">Activity</a></li>
  394. <li><a href="authors.html">Authors</a></li>
  395. <li><a href="files.html">Files</a></li>
  396. <li><a href="lines.html">Lines</a></li>
  397. <li><a href="tags.html">Tags</a></li>
  398. </ul>
  399. </div>
  400. """)
  401. usage = """
  402. Usage: statgit [options] <gitpath> <outputpath>
  403. Options:
  404. -o html
  405. """
  406. if len(sys.argv) < 3:
  407. print usage
  408. sys.exit(0)
  409. gitpath = sys.argv[1]
  410. outputpath = os.path.abspath(sys.argv[2])
  411. print 'Git path: %s' % gitpath
  412. print 'Output path: %s' % outputpath
  413. os.chdir(gitpath)
  414. print 'Collecting data...'
  415. data = GitDataCollector()
  416. data.collect(gitpath)
  417. print 'Generating report...'
  418. report = HTMLReportCreator()
  419. report.create(data, outputpath)