gitstats 25KB

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