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