12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184
  1. #!/usr/bin/env python
  2. # Copyright (c) 2007-2010 Heikki Hokkanen <hoxu@users.sf.net> & others (see doc/author.txt)
  3. # GPLv2 / GPLv3
  4. import datetime
  5. import getopt
  6. import glob
  7. import os
  8. import pickle
  9. import platform
  10. import re
  11. import shutil
  12. import subprocess
  13. import sys
  14. import time
  15. import zlib
  16. GNUPLOT_COMMON = 'set terminal png transparent size 640,240\nset size 1.0,1.0\n'
  17. ON_LINUX = (platform.system() == 'Linux')
  18. WEEKDAYS = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun')
  19. exectime_internal = 0.0
  20. exectime_external = 0.0
  21. time_start = time.time()
  22. # By default, gnuplot is searched from path, but can be overridden with the
  23. # environment variable "GNUPLOT"
  24. gnuplot_cmd = 'gnuplot'
  25. if 'GNUPLOT' in os.environ:
  26. gnuplot_cmd = os.environ['GNUPLOT']
  27. conf = {
  28. 'max_domains': 10,
  29. 'max_ext_length': 10,
  30. 'style': 'gitstats.css',
  31. 'max_authors': 20,
  32. 'authors_top': 5,
  33. 'commit_begin': '',
  34. 'commit_end': '',
  35. 'linear_linestats': 1,
  36. 'project_name': '',
  37. }
  38. def getpipeoutput(cmds, quiet = False):
  39. global exectime_external
  40. start = time.time()
  41. if not quiet and ON_LINUX and os.isatty(1):
  42. print '>> ' + ' | '.join(cmds),
  43. sys.stdout.flush()
  44. p0 = subprocess.Popen(cmds[0], stdout = subprocess.PIPE, shell = True)
  45. p = p0
  46. for x in cmds[1:]:
  47. p = subprocess.Popen(x, stdin = p0.stdout, stdout = subprocess.PIPE, shell = True)
  48. p0 = p
  49. output = p.communicate()[0]
  50. end = time.time()
  51. if not quiet:
  52. if ON_LINUX and os.isatty(1):
  53. print '\r',
  54. print '[%.5f] >> %s' % (end - start, ' | '.join(cmds))
  55. exectime_external += (end - start)
  56. return output.rstrip('\n')
  57. def getcommitrange(defaultrange = 'HEAD', end_only = False):
  58. if len(conf['commit_end']) > 0:
  59. if end_only or len(conf['commit_begin']) == 0:
  60. return conf['commit_end']
  61. return '%s..%s' % (conf['commit_begin'], conf['commit_end'])
  62. return defaultrange
  63. def getkeyssortedbyvalues(dict):
  64. return map(lambda el : el[1], sorted(map(lambda el : (el[1], el[0]), dict.items())))
  65. # dict['author'] = { 'commits': 512 } - ...key(dict, 'commits')
  66. def getkeyssortedbyvaluekey(d, key):
  67. return map(lambda el : el[1], sorted(map(lambda el : (d[el][key], el), d.keys())))
  68. VERSION = 0
  69. def getversion():
  70. global VERSION
  71. if VERSION == 0:
  72. VERSION = getpipeoutput(["git rev-parse --short %s" % getcommitrange('HEAD')]).split('\n')[0]
  73. return VERSION
  74. class DataCollector:
  75. """Manages data collection from a revision control repository."""
  76. def __init__(self):
  77. self.stamp_created = time.time()
  78. self.cache = {}
  79. ##
  80. # This should be the main function to extract data from the repository.
  81. def collect(self, dir):
  82. self.dir = dir
  83. if len(conf['project_name']) == 0:
  84. self.projectname = os.path.basename(os.path.abspath(dir))
  85. else:
  86. self.projectname = conf['project_name']
  87. ##
  88. # Load cacheable data
  89. def loadCache(self, cachefile):
  90. if not os.path.exists(cachefile):
  91. return
  92. print 'Loading cache...'
  93. f = open(cachefile, 'rb')
  94. try:
  95. self.cache = pickle.loads(zlib.decompress(f.read()))
  96. except:
  97. # temporary hack to upgrade non-compressed caches
  98. f.seek(0)
  99. self.cache = pickle.load(f)
  100. f.close()
  101. ##
  102. # Produce any additional statistics from the extracted data.
  103. def refine(self):
  104. pass
  105. ##
  106. # : get a dictionary of author
  107. def getAuthorInfo(self, author):
  108. return None
  109. def getActivityByDayOfWeek(self):
  110. return {}
  111. def getActivityByHourOfDay(self):
  112. return {}
  113. # : get a dictionary of domains
  114. def getDomainInfo(self, domain):
  115. return None
  116. ##
  117. # Get a list of authors
  118. def getAuthors(self):
  119. return []
  120. def getFirstCommitDate(self):
  121. return datetime.datetime.now()
  122. def getLastCommitDate(self):
  123. return datetime.datetime.now()
  124. def getStampCreated(self):
  125. return self.stamp_created
  126. def getTags(self):
  127. return []
  128. def getTotalAuthors(self):
  129. return -1
  130. def getTotalCommits(self):
  131. return -1
  132. def getTotalFiles(self):
  133. return -1
  134. def getTotalLOC(self):
  135. return -1
  136. ##
  137. # Save cacheable data
  138. def saveCache(self, cachefile):
  139. print 'Saving cache...'
  140. f = open(cachefile, 'wb')
  141. #pickle.dump(self.cache, f)
  142. data = zlib.compress(pickle.dumps(self.cache))
  143. f.write(data)
  144. f.close()
  145. class GitDataCollector(DataCollector):
  146. def collect(self, dir):
  147. DataCollector.collect(self, dir)
  148. try:
  149. self.total_authors = int(getpipeoutput(['git shortlog -s %s' % getcommitrange(), 'wc -l']))
  150. except:
  151. self.total_authors = 0
  152. #self.total_lines = int(getoutput('git-ls-files -z |xargs -0 cat |wc -l'))
  153. self.activity_by_hour_of_day = {} # hour -> commits
  154. self.activity_by_day_of_week = {} # day -> commits
  155. self.activity_by_month_of_year = {} # month [1-12] -> commits
  156. self.activity_by_hour_of_week = {} # weekday -> hour -> commits
  157. self.activity_by_hour_of_day_busiest = 0
  158. self.activity_by_hour_of_week_busiest = 0
  159. self.activity_by_year_week = {} # yy_wNN -> commits
  160. self.activity_by_year_week_peak = 0
  161. self.authors = {} # name -> {commits, first_commit_stamp, last_commit_stamp, last_active_day, active_days, lines_added, lines_removed}
  162. # domains
  163. self.domains = {} # domain -> commits
  164. # author of the month
  165. self.author_of_month = {} # month -> author -> commits
  166. self.author_of_year = {} # year -> author -> commits
  167. self.commits_by_month = {} # month -> commits
  168. self.commits_by_year = {} # year -> commits
  169. self.first_commit_stamp = 0
  170. self.last_commit_stamp = 0
  171. self.last_active_day = None
  172. self.active_days = set()
  173. # lines
  174. self.total_lines = 0
  175. self.total_lines_added = 0
  176. self.total_lines_removed = 0
  177. # timezone
  178. self.commits_by_timezone = {} # timezone -> commits
  179. # tags
  180. self.tags = {}
  181. lines = getpipeoutput(['git show-ref --tags']).split('\n')
  182. for line in lines:
  183. if len(line) == 0:
  184. continue
  185. (hash, tag) = line.split(' ')
  186. tag = tag.replace('refs/tags/', '')
  187. output = getpipeoutput(['git log "%s" --pretty=format:"%%at %%aN" -n 1' % hash])
  188. if len(output) > 0:
  189. parts = output.split(' ')
  190. stamp = 0
  191. try:
  192. stamp = int(parts[0])
  193. except ValueError:
  194. stamp = 0
  195. self.tags[tag] = { 'stamp': stamp, 'hash' : hash, 'date' : datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), 'commits': 0, 'authors': {} }
  196. # collect info on tags, starting from latest
  197. tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), self.tags.items()))))
  198. prev = None
  199. for tag in reversed(tags_sorted_by_date_desc):
  200. cmd = 'git shortlog -s "%s"' % tag
  201. if prev != None:
  202. cmd += ' "^%s"' % prev
  203. output = getpipeoutput([cmd])
  204. if len(output) == 0:
  205. continue
  206. prev = tag
  207. for line in output.split('\n'):
  208. parts = re.split('\s+', line, 2)
  209. commits = int(parts[1])
  210. author = parts[2]
  211. self.tags[tag]['commits'] += commits
  212. self.tags[tag]['authors'][author] = commits
  213. # Collect revision statistics
  214. # Outputs "<stamp> <date> <time> <timezone> <author> '<' <mail> '>'"
  215. lines = getpipeoutput(['git rev-list --pretty=format:"%%at %%ai %%aN <%%aE>" %s' % getcommitrange('HEAD'), 'grep -v ^commit']).split('\n')
  216. for line in lines:
  217. parts = line.split(' ', 4)
  218. author = ''
  219. try:
  220. stamp = int(parts[0])
  221. except ValueError:
  222. stamp = 0
  223. timezone = parts[3]
  224. author, mail = parts[4].split('<', 1)
  225. author = author.rstrip()
  226. mail = mail.rstrip('>')
  227. domain = '?'
  228. if mail.find('@') != -1:
  229. domain = mail.rsplit('@', 1)[1]
  230. date = datetime.datetime.fromtimestamp(float(stamp))
  231. # First and last commit stamp
  232. if self.last_commit_stamp == 0:
  233. self.last_commit_stamp = stamp
  234. self.first_commit_stamp = stamp
  235. # activity
  236. # hour
  237. hour = date.hour
  238. self.activity_by_hour_of_day[hour] = self.activity_by_hour_of_day.get(hour, 0) + 1
  239. # most active hour?
  240. if self.activity_by_hour_of_day[hour] > self.activity_by_hour_of_day_busiest:
  241. self.activity_by_hour_of_day_busiest = self.activity_by_hour_of_day[hour]
  242. # day of week
  243. day = date.weekday()
  244. self.activity_by_day_of_week[day] = self.activity_by_day_of_week.get(day, 0) + 1
  245. # domain stats
  246. if domain not in self.domains:
  247. self.domains[domain] = {}
  248. # commits
  249. self.domains[domain]['commits'] = self.domains[domain].get('commits', 0) + 1
  250. # hour of week
  251. if day not in self.activity_by_hour_of_week:
  252. self.activity_by_hour_of_week[day] = {}
  253. self.activity_by_hour_of_week[day][hour] = self.activity_by_hour_of_week[day].get(hour, 0) + 1
  254. # most active hour?
  255. if self.activity_by_hour_of_week[day][hour] > self.activity_by_hour_of_week_busiest:
  256. self.activity_by_hour_of_week_busiest = self.activity_by_hour_of_week[day][hour]
  257. # month of year
  258. month = date.month
  259. self.activity_by_month_of_year[month] = self.activity_by_month_of_year.get(month, 0) + 1
  260. # yearly/weekly activity
  261. yyw = date.strftime('%Y-%W')
  262. self.activity_by_year_week[yyw] = self.activity_by_year_week.get(yyw, 0) + 1
  263. if self.activity_by_year_week_peak < self.activity_by_year_week[yyw]:
  264. self.activity_by_year_week_peak = self.activity_by_year_week[yyw]
  265. # author stats
  266. if author not in self.authors:
  267. self.authors[author] = {}
  268. # commits
  269. if 'last_commit_stamp' not in self.authors[author]:
  270. self.authors[author]['last_commit_stamp'] = stamp
  271. self.authors[author]['first_commit_stamp'] = stamp
  272. self.authors[author]['commits'] = self.authors[author].get('commits', 0) + 1
  273. # author of the month/year
  274. yymm = date.strftime('%Y-%m')
  275. if yymm in self.author_of_month:
  276. self.author_of_month[yymm][author] = self.author_of_month[yymm].get(author, 0) + 1
  277. else:
  278. self.author_of_month[yymm] = {}
  279. self.author_of_month[yymm][author] = 1
  280. self.commits_by_month[yymm] = self.commits_by_month.get(yymm, 0) + 1
  281. yy = date.year
  282. if yy in self.author_of_year:
  283. self.author_of_year[yy][author] = self.author_of_year[yy].get(author, 0) + 1
  284. else:
  285. self.author_of_year[yy] = {}
  286. self.author_of_year[yy][author] = 1
  287. self.commits_by_year[yy] = self.commits_by_year.get(yy, 0) + 1
  288. # authors: active days
  289. yymmdd = date.strftime('%Y-%m-%d')
  290. if 'last_active_day' not in self.authors[author]:
  291. self.authors[author]['last_active_day'] = yymmdd
  292. self.authors[author]['active_days'] = 1
  293. elif yymmdd != self.authors[author]['last_active_day']:
  294. self.authors[author]['last_active_day'] = yymmdd
  295. self.authors[author]['active_days'] += 1
  296. # project: active days
  297. if yymmdd != self.last_active_day:
  298. self.last_active_day = yymmdd
  299. self.active_days.add(yymmdd)
  300. # timezone
  301. self.commits_by_timezone[timezone] = self.commits_by_timezone.get(timezone, 0) + 1
  302. # TODO Optimize this, it's the worst bottleneck
  303. # outputs "<stamp> <files>" for each revision
  304. self.files_by_stamp = {} # stamp -> files
  305. revlines = getpipeoutput(['git rev-list --pretty=format:"%%at %%T" %s' % getcommitrange('HEAD'), 'grep -v ^commit']).strip().split('\n')
  306. lines = []
  307. for revline in revlines:
  308. time, rev = revline.split(' ')
  309. linecount = self.getFilesInCommit(rev)
  310. lines.append('%d %d' % (int(time), linecount))
  311. self.total_commits = len(lines)
  312. for line in lines:
  313. parts = line.split(' ')
  314. if len(parts) != 2:
  315. continue
  316. (stamp, files) = parts[0:2]
  317. try:
  318. self.files_by_stamp[int(stamp)] = int(files)
  319. except ValueError:
  320. print 'Warning: failed to parse line "%s"' % line
  321. # extensions
  322. self.extensions = {} # extension -> files, lines
  323. lines = getpipeoutput(['git ls-tree -r -z %s' % getcommitrange('HEAD', end_only = True)]).split('\000')
  324. self.total_files = len(lines)
  325. for line in lines:
  326. if len(line) == 0:
  327. continue
  328. parts = re.split('\s+', line, 4)
  329. sha1 = parts[2]
  330. filename = parts[3]
  331. if filename.find('.') == -1 or filename.rfind('.') == 0:
  332. ext = ''
  333. else:
  334. ext = filename[(filename.rfind('.') + 1):]
  335. if len(ext) > conf['max_ext_length']:
  336. ext = ''
  337. if ext not in self.extensions:
  338. self.extensions[ext] = {'files': 0, 'lines': 0}
  339. self.extensions[ext]['files'] += 1
  340. try:
  341. self.extensions[ext]['lines'] += self.getLinesInBlob(sha1)
  342. except:
  343. print 'Warning: Could not count lines for file "%s"' % line
  344. # line statistics
  345. # outputs:
  346. # N files changed, N insertions (+), N deletions(-)
  347. # <stamp> <author>
  348. self.changes_by_date = {} # stamp -> { files, ins, del }
  349. extra = ''
  350. if conf['linear_linestats']:
  351. extra = '--first-parent -m'
  352. lines = getpipeoutput(['git log --shortstat %s --pretty=format:"%%at %%aN" %s' % (extra, getcommitrange('HEAD'))]).split('\n')
  353. lines.reverse()
  354. files = 0; inserted = 0; deleted = 0; total_lines = 0
  355. author = None
  356. for line in lines:
  357. if len(line) == 0:
  358. continue
  359. # <stamp> <author>
  360. if line.find('files changed,') == -1:
  361. pos = line.find(' ')
  362. if pos != -1:
  363. try:
  364. (stamp, author) = (int(line[:pos]), line[pos+1:])
  365. self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted, 'lines': total_lines }
  366. if author not in self.authors:
  367. self.authors[author] = { 'lines_added' : 0, 'lines_removed' : 0 }
  368. self.authors[author]['lines_added'] = self.authors[author].get('lines_added', 0) + inserted
  369. self.authors[author]['lines_removed'] = self.authors[author].get('lines_removed', 0) + deleted
  370. files, inserted, deleted = 0, 0, 0
  371. except ValueError:
  372. print 'Warning: unexpected line "%s"' % line
  373. else:
  374. print 'Warning: unexpected line "%s"' % line
  375. else:
  376. numbers = re.findall('\d+', line)
  377. if len(numbers) == 3:
  378. (files, inserted, deleted) = map(lambda el : int(el), numbers)
  379. total_lines += inserted
  380. total_lines -= deleted
  381. self.total_lines_added += inserted
  382. self.total_lines_removed += deleted
  383. else:
  384. print 'Warning: failed to handle line "%s"' % line
  385. (files, inserted, deleted) = (0, 0, 0)
  386. #self.changes_by_date[stamp] = { 'files': files, 'ins': inserted, 'del': deleted }
  387. self.total_lines = total_lines
  388. def refine(self):
  389. # authors
  390. # name -> {place_by_commits, commits_frac, date_first, date_last, timedelta}
  391. authors_by_commits = getkeyssortedbyvaluekey(self.authors, 'commits')
  392. authors_by_commits.reverse() # most first
  393. for i, name in enumerate(authors_by_commits):
  394. self.authors[name]['place_by_commits'] = i + 1
  395. for name in self.authors.keys():
  396. a = self.authors[name]
  397. a['commits_frac'] = (100 * float(a['commits'])) / self.getTotalCommits()
  398. date_first = datetime.datetime.fromtimestamp(a['first_commit_stamp'])
  399. date_last = datetime.datetime.fromtimestamp(a['last_commit_stamp'])
  400. delta = date_last - date_first
  401. a['date_first'] = date_first.strftime('%Y-%m-%d')
  402. a['date_last'] = date_last.strftime('%Y-%m-%d')
  403. a['timedelta'] = delta
  404. if 'lines_added' not in a: a['lines_added'] = 0
  405. if 'lines_removed' not in a: a['lines_removed'] = 0
  406. def getActiveDays(self):
  407. return self.active_days
  408. def getActivityByDayOfWeek(self):
  409. return self.activity_by_day_of_week
  410. def getActivityByHourOfDay(self):
  411. return self.activity_by_hour_of_day
  412. def getAuthorInfo(self, author):
  413. return self.authors[author]
  414. def getAuthors(self, limit = None):
  415. res = getkeyssortedbyvaluekey(self.authors, 'commits')
  416. res.reverse()
  417. return res[:limit]
  418. def getCommitDeltaDays(self):
  419. return (self.last_commit_stamp - self.first_commit_stamp) / 86400 + 1
  420. def getDomainInfo(self, domain):
  421. return self.domains[domain]
  422. def getDomains(self):
  423. return self.domains.keys()
  424. def getFilesInCommit(self, rev):
  425. try:
  426. res = self.cache['files_in_tree'][rev]
  427. except:
  428. res = int(getpipeoutput(['git ls-tree -r --name-only "%s"' % rev, 'wc -l']).split('\n')[0])
  429. if 'files_in_tree' not in self.cache:
  430. self.cache['files_in_tree'] = {}
  431. self.cache['files_in_tree'][rev] = res
  432. return res
  433. def getFirstCommitDate(self):
  434. return datetime.datetime.fromtimestamp(self.first_commit_stamp)
  435. def getLastCommitDate(self):
  436. return datetime.datetime.fromtimestamp(self.last_commit_stamp)
  437. def getLinesInBlob(self, sha1):
  438. try:
  439. res = self.cache['lines_in_blob'][sha1]
  440. except:
  441. res = int(getpipeoutput(['git cat-file blob %s' % sha1, 'wc -l']).split()[0])
  442. if 'lines_in_blob' not in self.cache:
  443. self.cache['lines_in_blob'] = {}
  444. self.cache['lines_in_blob'][sha1] = res
  445. return res
  446. def getTags(self):
  447. lines = getpipeoutput(['git show-ref --tags', 'cut -d/ -f3'])
  448. return lines.split('\n')
  449. def getTagDate(self, tag):
  450. return self.revToDate('tags/' + tag)
  451. def getTotalAuthors(self):
  452. return self.total_authors
  453. def getTotalCommits(self):
  454. return self.total_commits
  455. def getTotalFiles(self):
  456. return self.total_files
  457. def getTotalLOC(self):
  458. return self.total_lines
  459. def revToDate(self, rev):
  460. stamp = int(getpipeoutput(['git log --pretty=format:%%at "%s" -n 1' % rev]))
  461. return datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d')
  462. class ReportCreator:
  463. """Creates the actual report based on given data."""
  464. def __init__(self):
  465. pass
  466. def create(self, data, path):
  467. self.data = data
  468. self.path = path
  469. def html_linkify(text):
  470. return text.lower().replace(' ', '_')
  471. def html_header(level, text):
  472. name = html_linkify(text)
  473. return '\n<h%d><a href="#%s" name="%s">%s</a></h%d>\n\n' % (level, name, name, text, level)
  474. class HTMLReportCreator(ReportCreator):
  475. def create(self, data, path):
  476. ReportCreator.create(self, data, path)
  477. self.title = data.projectname
  478. # copy static files. Looks in the binary directory, ../share/gitstats and /usr/share/gitstats
  479. binarypath = os.path.dirname(os.path.abspath(__file__))
  480. secondarypath = os.path.join(binarypath, '..', 'share', 'gitstats')
  481. basedirs = [binarypath, secondarypath, '/usr/share/gitstats']
  482. for file in ('gitstats.css', 'sortable.js', 'arrow-up.gif', 'arrow-down.gif', 'arrow-none.gif'):
  483. for base in basedirs:
  484. src = base + '/' + file
  485. if os.path.exists(src):
  486. shutil.copyfile(src, path + '/' + file)
  487. break
  488. else:
  489. print 'Warning: "%s" not found, so not copied (searched: %s)' % (file, basedirs)
  490. f = open(path + "/index.html", 'w')
  491. format = '%Y-%m-%d %H:%M:%S'
  492. self.printHeader(f)
  493. f.write('<h1>GitStats - %s</h1>' % data.projectname)
  494. self.printNav(f)
  495. f.write('<dl>')
  496. f.write('<dt>Project name</dt><dd>%s</dd>' % (data.projectname))
  497. f.write('<dt>Generated</dt><dd>%s (in %d seconds)</dd>' % (datetime.datetime.now().strftime(format), time.time() - data.getStampCreated()))
  498. f.write('<dt>Generator</dt><dd><a href="http://gitstats.sourceforge.net/">GitStats</a> (version %s)</dd>' % getversion())
  499. f.write('<dt>Report Period</dt><dd>%s to %s</dd>' % (data.getFirstCommitDate().strftime(format), data.getLastCommitDate().strftime(format)))
  500. f.write('<dt>Age</dt><dd>%d days, %d active days (%3.2f%%)</dd>' % (data.getCommitDeltaDays(), len(data.getActiveDays()), (100.0 * len(data.getActiveDays()) / data.getCommitDeltaDays())))
  501. f.write('<dt>Total Files</dt><dd>%s</dd>' % data.getTotalFiles())
  502. f.write('<dt>Total Lines of Code</dt><dd>%s (%d added, %d removed)</dd>' % (data.getTotalLOC(), data.total_lines_added, data.total_lines_removed))
  503. f.write('<dt>Total Commits</dt><dd>%s (average %.1f commits per active day, %.1f per all days)</dd>' % (data.getTotalCommits(), float(data.getTotalCommits()) / len(data.getActiveDays()), float(data.getTotalCommits()) / data.getCommitDeltaDays()))
  504. f.write('<dt>Authors</dt><dd>%s (average %.1f commits per author)</dd>' % (data.getTotalAuthors(), (1.0 * data.getTotalCommits()) / data.getTotalAuthors()))
  505. f.write('</dl>')
  506. f.write('</body>\n</html>')
  507. f.close()
  508. ###
  509. # Activity
  510. f = open(path + '/activity.html', 'w')
  511. self.printHeader(f)
  512. f.write('<h1>Activity</h1>')
  513. self.printNav(f)
  514. #f.write('<h2>Last 30 days</h2>')
  515. #f.write('<h2>Last 12 months</h2>')
  516. # Weekly activity
  517. WEEKS = 32
  518. f.write(html_header(2, 'Weekly activity'))
  519. f.write('<p>Last %d weeks</p>' % WEEKS)
  520. # generate weeks to show (previous N weeks from now)
  521. now = datetime.datetime.now()
  522. deltaweek = datetime.timedelta(7)
  523. weeks = []
  524. stampcur = now
  525. for i in range(0, WEEKS):
  526. weeks.insert(0, stampcur.strftime('%Y-%W'))
  527. stampcur -= deltaweek
  528. # top row: commits & bar
  529. f.write('<table class="noborders"><tr>')
  530. for i in range(0, WEEKS):
  531. commits = 0
  532. if weeks[i] in data.activity_by_year_week:
  533. commits = data.activity_by_year_week[weeks[i]]
  534. percentage = 0
  535. if weeks[i] in data.activity_by_year_week:
  536. percentage = float(data.activity_by_year_week[weeks[i]]) / data.activity_by_year_week_peak
  537. height = max(1, int(200 * percentage))
  538. f.write('<td style="text-align: center; vertical-align: bottom">%d<div style="display: block; background-color: red; width: 20px; height: %dpx"></div></td>' % (commits, height))
  539. # bottom row: year/week
  540. f.write('</tr><tr>')
  541. for i in range(0, WEEKS):
  542. f.write('<td>%s</td>' % (WEEKS - i))
  543. f.write('</tr></table>')
  544. # Hour of Day
  545. f.write(html_header(2, 'Hour of Day'))
  546. hour_of_day = data.getActivityByHourOfDay()
  547. f.write('<table><tr><th>Hour</th>')
  548. for i in range(0, 24):
  549. f.write('<th>%d</th>' % i)
  550. f.write('</tr>\n<tr><th>Commits</th>')
  551. fp = open(path + '/hour_of_day.dat', 'w')
  552. for i in range(0, 24):
  553. if i in hour_of_day:
  554. r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128)
  555. f.write('<td style="background-color: rgb(%d, 0, 0)">%d</td>' % (r, hour_of_day[i]))
  556. fp.write('%d %d\n' % (i, hour_of_day[i]))
  557. else:
  558. f.write('<td>0</td>')
  559. fp.write('%d 0\n' % i)
  560. fp.close()
  561. f.write('</tr>\n<tr><th>%</th>')
  562. totalcommits = data.getTotalCommits()
  563. for i in range(0, 24):
  564. if i in hour_of_day:
  565. r = 127 + int((float(hour_of_day[i]) / data.activity_by_hour_of_day_busiest) * 128)
  566. f.write('<td style="background-color: rgb(%d, 0, 0)">%.2f</td>' % (r, (100.0 * hour_of_day[i]) / totalcommits))
  567. else:
  568. f.write('<td>0.00</td>')
  569. f.write('</tr></table>')
  570. f.write('<img src="hour_of_day.png" alt="Hour of Day" />')
  571. fg = open(path + '/hour_of_day.dat', 'w')
  572. for i in range(0, 24):
  573. if i in hour_of_day:
  574. fg.write('%d %d\n' % (i + 1, hour_of_day[i]))
  575. else:
  576. fg.write('%d 0\n' % (i + 1))
  577. fg.close()
  578. # Day of Week
  579. f.write(html_header(2, 'Day of Week'))
  580. day_of_week = data.getActivityByDayOfWeek()
  581. f.write('<div class="vtable"><table>')
  582. f.write('<tr><th>Day</th><th>Total (%)</th></tr>')
  583. fp = open(path + '/day_of_week.dat', 'w')
  584. for d in range(0, 7):
  585. commits = 0
  586. if d in day_of_week:
  587. commits = day_of_week[d]
  588. fp.write('%d %s %d\n' % (d + 1, WEEKDAYS[d], commits))
  589. f.write('<tr>')
  590. f.write('<th>%s</th>' % (WEEKDAYS[d]))
  591. if d in day_of_week:
  592. f.write('<td>%d (%.2f%%)</td>' % (day_of_week[d], (100.0 * day_of_week[d]) / totalcommits))
  593. else:
  594. f.write('<td>0</td>')
  595. f.write('</tr>')
  596. f.write('</table></div>')
  597. f.write('<img src="day_of_week.png" alt="Day of Week" />')
  598. fp.close()
  599. # Hour of Week
  600. f.write(html_header(2, 'Hour of Week'))
  601. f.write('<table>')
  602. f.write('<tr><th>Weekday</th>')
  603. for hour in range(0, 24):
  604. f.write('<th>%d</th>' % (hour))
  605. f.write('</tr>')
  606. for weekday in range(0, 7):
  607. f.write('<tr><th>%s</th>' % (WEEKDAYS[weekday]))
  608. for hour in range(0, 24):
  609. try:
  610. commits = data.activity_by_hour_of_week[weekday][hour]
  611. except KeyError:
  612. commits = 0
  613. if commits != 0:
  614. f.write('<td')
  615. r = 127 + int((float(commits) / data.activity_by_hour_of_week_busiest) * 128)
  616. f.write(' style="background-color: rgb(%d, 0, 0)"' % r)
  617. f.write('>%d</td>' % commits)
  618. else:
  619. f.write('<td></td>')
  620. f.write('</tr>')
  621. f.write('</table>')
  622. # Month of Year
  623. f.write(html_header(2, 'Month of Year'))
  624. f.write('<div class="vtable"><table>')
  625. f.write('<tr><th>Month</th><th>Commits (%)</th></tr>')
  626. fp = open (path + '/month_of_year.dat', 'w')
  627. for mm in range(1, 13):
  628. commits = 0
  629. if mm in data.activity_by_month_of_year:
  630. commits = data.activity_by_month_of_year[mm]
  631. f.write('<tr><td>%d</td><td>%d (%.2f %%)</td></tr>' % (mm, commits, (100.0 * commits) / data.getTotalCommits()))
  632. fp.write('%d %d\n' % (mm, commits))
  633. fp.close()
  634. f.write('</table></div>')
  635. f.write('<img src="month_of_year.png" alt="Month of Year" />')
  636. # Commits by year/month
  637. f.write(html_header(2, 'Commits by year/month'))
  638. f.write('<div class="vtable"><table><tr><th>Month</th><th>Commits</th></tr>')
  639. for yymm in reversed(sorted(data.commits_by_month.keys())):
  640. f.write('<tr><td>%s</td><td>%d</td></tr>' % (yymm, data.commits_by_month[yymm]))
  641. f.write('</table></div>')
  642. f.write('<img src="commits_by_year_month.png" alt="Commits by year/month" />')
  643. fg = open(path + '/commits_by_year_month.dat', 'w')
  644. for yymm in sorted(data.commits_by_month.keys()):
  645. fg.write('%s %s\n' % (yymm, data.commits_by_month[yymm]))
  646. fg.close()
  647. # Commits by year
  648. f.write(html_header(2, 'Commits by Year'))
  649. f.write('<div class="vtable"><table><tr><th>Year</th><th>Commits (% of all)</th></tr>')
  650. for yy in reversed(sorted(data.commits_by_year.keys())):
  651. 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()))
  652. f.write('</table></div>')
  653. f.write('<img src="commits_by_year.png" alt="Commits by Year" />')
  654. fg = open(path + '/commits_by_year.dat', 'w')
  655. for yy in sorted(data.commits_by_year.keys()):
  656. fg.write('%d %d\n' % (yy, data.commits_by_year[yy]))
  657. fg.close()
  658. # Commits by timezone
  659. f.write(html_header(2, 'Commits by Timezone'))
  660. f.write('<table><tr>')
  661. f.write('<th>Timezone</th><th>Commits</th>')
  662. max_commits_on_tz = max(data.commits_by_timezone.values())
  663. for i in sorted(data.commits_by_timezone.keys(), key = lambda n : int(n)):
  664. commits = data.commits_by_timezone[i]
  665. r = 127 + int((float(commits) / max_commits_on_tz) * 128)
  666. f.write('<tr><th>%s</th><td style="background-color: rgb(%d, 0, 0)">%d</td></tr>' % (i, r, commits))
  667. f.write('</tr></table>')
  668. f.write('</body></html>')
  669. f.close()
  670. ###
  671. # Authors
  672. f = open(path + '/authors.html', 'w')
  673. self.printHeader(f)
  674. f.write('<h1>Authors</h1>')
  675. self.printNav(f)
  676. # Authors :: List of authors
  677. f.write(html_header(2, 'List of Authors'))
  678. f.write('<table class="authors sortable" id="authors">')
  679. f.write('<tr><th>Author</th><th>Commits (%)</th><th>+ lines</th><th>- lines</th><th>First commit</th><th>Last commit</th><th class="unsortable">Age</th><th>Active days</th><th># by commits</th></tr>')
  680. for author in data.getAuthors(conf['max_authors']):
  681. info = data.getAuthorInfo(author)
  682. f.write('<tr><td>%s</td><td>%d (%.2f%%)</td><td>%d</td><td>%d</td><td>%s</td><td>%s</td><td>%s</td><td>%d</td><td>%d</td></tr>' % (author, info['commits'], info['commits_frac'], info['lines_added'], info['lines_removed'], info['date_first'], info['date_last'], info['timedelta'], info['active_days'], info['place_by_commits']))
  683. f.write('</table>')
  684. allauthors = data.getAuthors()
  685. if len(allauthors) > conf['max_authors']:
  686. rest = allauthors[conf['max_authors']:]
  687. f.write('<p class="moreauthors">These didn\'t make it to the top: %s</p>' % ', '.join(rest))
  688. # Authors :: Author of Month
  689. f.write(html_header(2, 'Author of Month'))
  690. f.write('<table class="sortable" id="aom">')
  691. f.write('<tr><th>Month</th><th>Author</th><th>Commits (%%)</th><th class="unsortable">Next top %d</th><th>Number of authors</th></tr>' % conf['authors_top'])
  692. for yymm in reversed(sorted(data.author_of_month.keys())):
  693. authordict = data.author_of_month[yymm]
  694. authors = getkeyssortedbyvalues(authordict)
  695. authors.reverse()
  696. commits = data.author_of_month[yymm][authors[0]]
  697. next = ', '.join(authors[1:conf['authors_top']+1])
  698. f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td><td>%d</td></tr>' % (yymm, authors[0], commits, (100.0 * commits) / data.commits_by_month[yymm], data.commits_by_month[yymm], next, len(authors)))
  699. f.write('</table>')
  700. f.write(html_header(2, 'Author of Year'))
  701. f.write('<table class="sortable" id="aoy"><tr><th>Year</th><th>Author</th><th>Commits (%%)</th><th class="unsortable">Next top %d</th><th>Number of authors</th></tr>' % conf['authors_top'])
  702. for yy in reversed(sorted(data.author_of_year.keys())):
  703. authordict = data.author_of_year[yy]
  704. authors = getkeyssortedbyvalues(authordict)
  705. authors.reverse()
  706. commits = data.author_of_year[yy][authors[0]]
  707. next = ', '.join(authors[1:conf['authors_top']+1])
  708. f.write('<tr><td>%s</td><td>%s</td><td>%d (%.2f%% of %d)</td><td>%s</td><td>%d</td></tr>' % (yy, authors[0], commits, (100.0 * commits) / data.commits_by_year[yy], data.commits_by_year[yy], next, len(authors)))
  709. f.write('</table>')
  710. # Domains
  711. f.write(html_header(2, 'Commits by Domains'))
  712. domains_by_commits = getkeyssortedbyvaluekey(data.domains, 'commits')
  713. domains_by_commits.reverse() # most first
  714. f.write('<div class="vtable"><table>')
  715. f.write('<tr><th>Domains</th><th>Total (%)</th></tr>')
  716. fp = open(path + '/domains.dat', 'w')
  717. n = 0
  718. for domain in domains_by_commits:
  719. if n == conf['max_domains']:
  720. break
  721. commits = 0
  722. n += 1
  723. info = data.getDomainInfo(domain)
  724. fp.write('%s %d %d\n' % (domain, n , info['commits']))
  725. f.write('<tr><th>%s</th><td>%d (%.2f%%)</td></tr>' % (domain, info['commits'], (100.0 * info['commits'] / totalcommits)))
  726. f.write('</table></div>')
  727. f.write('<img src="domains.png" alt="Commits by Domains" />')
  728. fp.close()
  729. f.write('</body></html>')
  730. f.close()
  731. ###
  732. # Files
  733. f = open(path + '/files.html', 'w')
  734. self.printHeader(f)
  735. f.write('<h1>Files</h1>')
  736. self.printNav(f)
  737. f.write('<dl>\n')
  738. f.write('<dt>Total files</dt><dd>%d</dd>' % data.getTotalFiles())
  739. f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
  740. f.write('<dt>Average file size</dt><dd>%.2f bytes</dd>' % ((100.0 * data.getTotalLOC()) / data.getTotalFiles()))
  741. f.write('</dl>\n')
  742. # Files :: File count by date
  743. f.write(html_header(2, 'File count by date'))
  744. # use set to get rid of duplicate/unnecessary entries
  745. files_by_date = set()
  746. for stamp in sorted(data.files_by_stamp.keys()):
  747. files_by_date.add('%s %d' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
  748. fg = open(path + '/files_by_date.dat', 'w')
  749. for line in sorted(list(files_by_date)):
  750. fg.write('%s\n' % line)
  751. #for stamp in sorted(data.files_by_stamp.keys()):
  752. # fg.write('%s %d\n' % (datetime.datetime.fromtimestamp(stamp).strftime('%Y-%m-%d'), data.files_by_stamp[stamp]))
  753. fg.close()
  754. f.write('<img src="files_by_date.png" alt="Files by Date" />')
  755. #f.write('<h2>Average file size by date</h2>')
  756. # Files :: Extensions
  757. f.write(html_header(2, 'Extensions'))
  758. f.write('<table class="sortable" id="ext"><tr><th>Extension</th><th>Files (%)</th><th>Lines (%)</th><th>Lines/file</th></tr>')
  759. for ext in sorted(data.extensions.keys()):
  760. files = data.extensions[ext]['files']
  761. lines = data.extensions[ext]['lines']
  762. 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))
  763. f.write('</table>')
  764. f.write('</body></html>')
  765. f.close()
  766. ###
  767. # Lines
  768. f = open(path + '/lines.html', 'w')
  769. self.printHeader(f)
  770. f.write('<h1>Lines</h1>')
  771. self.printNav(f)
  772. f.write('<dl>\n')
  773. f.write('<dt>Total lines</dt><dd>%d</dd>' % data.getTotalLOC())
  774. f.write('</dl>\n')
  775. f.write(html_header(2, 'Lines of Code'))
  776. f.write('<img src="lines_of_code.png" />')
  777. fg = open(path + '/lines_of_code.dat', 'w')
  778. for stamp in sorted(data.changes_by_date.keys()):
  779. fg.write('%d %d\n' % (stamp, data.changes_by_date[stamp]['lines']))
  780. fg.close()
  781. f.write('</body></html>')
  782. f.close()
  783. ###
  784. # tags.html
  785. f = open(path + '/tags.html', 'w')
  786. self.printHeader(f)
  787. f.write('<h1>Tags</h1>')
  788. self.printNav(f)
  789. f.write('<dl>')
  790. f.write('<dt>Total tags</dt><dd>%d</dd>' % len(data.tags))
  791. if len(data.tags) > 0:
  792. f.write('<dt>Average commits per tag</dt><dd>%.2f</dd>' % (1.0 * data.getTotalCommits() / len(data.tags)))
  793. f.write('</dl>')
  794. f.write('<table class="tags">')
  795. f.write('<tr><th>Name</th><th>Date</th><th>Commits</th><th>Authors</th></tr>')
  796. # sort the tags by date desc
  797. tags_sorted_by_date_desc = map(lambda el : el[1], reversed(sorted(map(lambda el : (el[1]['date'], el[0]), data.tags.items()))))
  798. for tag in tags_sorted_by_date_desc:
  799. authorinfo = []
  800. authors_by_commits = getkeyssortedbyvalues(data.tags[tag]['authors'])
  801. for i in reversed(authors_by_commits):
  802. authorinfo.append('%s (%d)' % (i, data.tags[tag]['authors'][i]))
  803. f.write('<tr><td>%s</td><td>%s</td><td>%d</td><td>%s</td></tr>' % (tag, data.tags[tag]['date'], data.tags[tag]['commits'], ', '.join(authorinfo)))
  804. f.write('</table>')
  805. f.write('</body></html>')
  806. f.close()
  807. self.createGraphs(path)
  808. def createGraphs(self, path):
  809. print 'Generating graphs...'
  810. # hour of day
  811. f = open(path + '/hour_of_day.plot', 'w')
  812. f.write(GNUPLOT_COMMON)
  813. f.write(
  814. """
  815. set output 'hour_of_day.png'
  816. unset key
  817. set xrange [0.5:24.5]
  818. set xtics 4
  819. set grid y
  820. set ylabel "Commits"
  821. plot 'hour_of_day.dat' using 1:2:(0.5) w boxes fs solid
  822. """)
  823. f.close()
  824. # day of week
  825. f = open(path + '/day_of_week.plot', 'w')
  826. f.write(GNUPLOT_COMMON)
  827. f.write(
  828. """
  829. set output 'day_of_week.png'
  830. unset key
  831. set xrange [0.5:7.5]
  832. set xtics 1
  833. set grid y
  834. set ylabel "Commits"
  835. plot 'day_of_week.dat' using 1:3:(0.5):xtic(2) w boxes fs solid
  836. """)
  837. f.close()
  838. # Domains
  839. f = open(path + '/domains.plot', 'w')
  840. f.write(GNUPLOT_COMMON)
  841. f.write(
  842. """
  843. set output 'domains.png'
  844. unset key
  845. unset xtics
  846. set yrange [0:]
  847. set grid y
  848. set ylabel "Commits"
  849. plot 'domains.dat' using 2:3:(0.5) with boxes fs solid, '' using 2:3:1 with labels rotate by 45 offset 0,1
  850. """)
  851. f.close()
  852. # Month of Year
  853. f = open(path + '/month_of_year.plot', 'w')
  854. f.write(GNUPLOT_COMMON)
  855. f.write(
  856. """
  857. set output 'month_of_year.png'
  858. unset key
  859. set xrange [0.5:12.5]
  860. set xtics 1
  861. set grid y
  862. set ylabel "Commits"
  863. plot 'month_of_year.dat' using 1:2:(0.5) w boxes fs solid
  864. """)
  865. f.close()
  866. # commits_by_year_month
  867. f = open(path + '/commits_by_year_month.plot', 'w')
  868. f.write(GNUPLOT_COMMON)
  869. f.write(
  870. """
  871. set output 'commits_by_year_month.png'
  872. unset key
  873. set xdata time
  874. set timefmt "%Y-%m"
  875. set format x "%Y-%m"
  876. set xtics rotate
  877. set bmargin 5
  878. set grid y
  879. set ylabel "Commits"
  880. plot 'commits_by_year_month.dat' using 1:2:(0.5) w boxes fs solid
  881. """)
  882. f.close()
  883. # commits_by_year
  884. f = open(path + '/commits_by_year.plot', 'w')
  885. f.write(GNUPLOT_COMMON)
  886. f.write(
  887. """
  888. set output 'commits_by_year.png'
  889. unset key
  890. set xtics 1 rotate
  891. set grid y
  892. set ylabel "Commits"
  893. set yrange [0:]
  894. plot 'commits_by_year.dat' using 1:2:(0.5) w boxes fs solid
  895. """)
  896. f.close()
  897. # Files by date
  898. f = open(path + '/files_by_date.plot', 'w')
  899. f.write(GNUPLOT_COMMON)
  900. f.write(
  901. """
  902. set output 'files_by_date.png'
  903. unset key
  904. set xdata time
  905. set timefmt "%Y-%m-%d"
  906. set format x "%Y-%m-%d"
  907. set grid y
  908. set ylabel "Files"
  909. set xtics rotate
  910. set ytics autofreq
  911. set bmargin 6
  912. plot 'files_by_date.dat' using 1:2 w steps
  913. """)
  914. f.close()
  915. # Lines of Code
  916. f = open(path + '/lines_of_code.plot', 'w')
  917. f.write(GNUPLOT_COMMON)
  918. f.write(
  919. """
  920. set output 'lines_of_code.png'
  921. unset key
  922. set xdata time
  923. set timefmt "%s"
  924. set format x "%Y-%m-%d"
  925. set grid y
  926. set ylabel "Lines"
  927. set xtics rotate
  928. set bmargin 6
  929. plot 'lines_of_code.dat' using 1:2 w lines
  930. """)
  931. f.close()
  932. os.chdir(path)
  933. files = glob.glob(path + '/*.plot')
  934. for f in files:
  935. out = getpipeoutput([gnuplot_cmd + ' "%s"' % f])
  936. if len(out) > 0:
  937. print out
  938. def printHeader(self, f, title = ''):
  939. f.write(
  940. """<?xml version="1.0" encoding="UTF-8"?>
  941. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
  942. <html xmlns="http://www.w3.org/1999/xhtml">
  943. <head>
  944. <title>GitStats - %s</title>
  945. <link rel="stylesheet" href="%s" type="text/css" />
  946. <meta name="generator" content="GitStats %s" />
  947. <script type="text/javascript" src="sortable.js"></script>
  948. </head>
  949. <body>
  950. """ % (self.title, conf['style'], getversion()))
  951. def printNav(self, f):
  952. f.write("""
  953. <div class="nav">
  954. <ul>
  955. <li><a href="index.html">General</a></li>
  956. <li><a href="activity.html">Activity</a></li>
  957. <li><a href="authors.html">Authors</a></li>
  958. <li><a href="files.html">Files</a></li>
  959. <li><a href="lines.html">Lines</a></li>
  960. <li><a href="tags.html">Tags</a></li>
  961. </ul>
  962. </div>
  963. """)
  964. class GitStats:
  965. def run(self, args_orig):
  966. optlist, args = getopt.getopt(args_orig, 'c:')
  967. for o,v in optlist:
  968. if o == '-c':
  969. key, value = v.split('=', 1)
  970. if key not in conf:
  971. raise KeyError('no such key "%s" in config' % key)
  972. if isinstance(conf[key], int):
  973. conf[key] = int(value)
  974. else:
  975. conf[key] = value
  976. if len(args) < 2:
  977. print """
  978. Usage: gitstats [options] <gitpath> <outputpath>
  979. Options:
  980. -c key=value Override configuration value
  981. Default config values:
  982. %s
  983. """ % conf
  984. sys.exit(0)
  985. gitpath = args[0]
  986. outputpath = os.path.abspath(args[1])
  987. rundir = os.getcwd()
  988. try:
  989. os.makedirs(outputpath)
  990. except OSError:
  991. pass
  992. if not os.path.isdir(outputpath):
  993. print 'FATAL: Output path is not a directory or does not exist'
  994. sys.exit(1)
  995. print 'Git path: %s' % gitpath
  996. print 'Output path: %s' % outputpath
  997. os.chdir(gitpath)
  998. cachefile = os.path.join(outputpath, 'gitstats.cache')
  999. print 'Collecting data...'
  1000. data = GitDataCollector()
  1001. data.loadCache(cachefile)
  1002. data.collect(gitpath)
  1003. print 'Refining data...'
  1004. data.saveCache(cachefile)
  1005. data.refine()
  1006. os.chdir(rundir)
  1007. print 'Generating report...'
  1008. report = HTMLReportCreator()
  1009. report.create(data, outputpath)
  1010. time_end = time.time()
  1011. exectime_internal = time_end - time_start
  1012. print 'Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal)
  1013. if __name__=='__main__':
  1014. g = GitStats()
  1015. g.run(sys.argv[1:])