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