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