text: #!/usr/bin/env node
  
  var Promise = require('bluebird');
  var git = require('nodegit');
  var program = require('commander');
  var _ = require('lodash');
  var moment = require('moment');
  var fs = require('fs');
  
  var DATE_FORMAT = 'YYYY-MM-DD';
  
  var config = {
      // Maximum time diff between 2 subsequent commits in minutes which are
      // counted to be in the same coding "session"
      maxCommitDiffInMinutes: 2 * 60,
  
      // How many minutes should be added for the first commit of coding session
      firstCommitAdditionInMinutes: 2 * 60,
  
      // Include commits since time x
      since: 'always',
      until: 'always',
  
      // Include merge requests
      mergeRequest: true,
  
      // Git repo
      gitPath: '.',
  
      // Aliases of emails for grouping the same activity as one person
      emailAliases: {
          "linus@torvalds.com": "linus@linux.com"
      },
      
      branch: null
  };
  
  function main() {
      exitIfShallow();
  
      parseArgs();
      config = mergeDefaultsWithArgs(config);
      config.since = parseSinceDate(config.since);
      config.until = parseUntilDate(config.until);
  
      // Poor man`s multiple args support
      // https://github.com/tj/commander.js/issues/531
      for (var i = 0; i < process.argv.length; i++) {
          var k = process.argv[i];
          var n = i <= process.argv.length - 1 ? process.argv[i + 1] : undefined;
          if (k == "-e" || k == "--email") {
              parseEmailAlias(n);
          } else
          if (k.startsWith("--email=")) {
              n = k.substring(k.indexOf("=") + 1)
              parseEmailAlias(n);
          }
      }
  
      getCommits(config.gitPath, config.branch).then(function(commits) {
          var commitsByEmail = _.groupBy(commits, function(commit) {
              var email = commit.author.email || 'unknown'
              if (config.emailAliases !== undefined && config.emailAliases[email] !== undefined) {
                  email = config.emailAliases[email]
              }
              return email;
          });
  
          var authorWorks = _.map(commitsByEmail, function(authorCommits, authorEmail) {
              return {
                  email: authorEmail,
                  name: authorCommits[0].author.name,
                  hours: estimateHours(_.pluck(authorCommits, 'date')),
                  commits: authorCommits.length
              };
          });
  
          // XXX: This relies on the implementation detail that json is printed
          // in the same order as the keys were added. This is anyway just for
          // making the output easier to read, so it doesn't matter if it
          // isn't sorted in some cases.
          var sortedWork = {};
  
          _.each(_.sortBy(authorWorks, 'hours'), function(authorWork) {
              sortedWork[authorWork.email] = _.omit(authorWork, 'email');
          });
  
          var totalHours = _.reduce(sortedWork, function(sum, authorWork) {
              return sum + authorWork.hours;
          }, 0);
  
          sortedWork.total = {
              hours: totalHours,
              commits: commits.length
          };
  
          console.log(JSON.stringify(sortedWork, undefined, 2));
      }).catch(function(e) {
          console.error(e.stack);
      });
  }
  
  function exitIfShallow() {
      if (fs.existsSync(".git/shallow")) {
          console.log("Cannot analyze shallow copies!");
          console.log("Please run git fetch --unshallow before continuing!");
          process.exit(1);
      }
  }
  
  function parseArgs() {
      function int(val) {
          return parseInt(val, 10);
      }
  
      program
          .version(require('../package.json').version)
          .usage('[options]')
          .option(
              '-d, --max-commit-diff [max-commit-diff]',
              'maximum difference in minutes between commits counted to one' +
              ' session. Default: ' + config.maxCommitDiffInMinutes,
              int
          )
          .option(
              '-a, --first-commit-add [first-commit-add]',
              'how many minutes first commit of session should add to total.' +
              ' Default: ' + config.firstCommitAdditionInMinutes,
              int
          )
          .option(
              '-s, --since [since-certain-date]',
              'Analyze data since certain date.' +
              ' [always|yesterday|today|lastweek|thisweek|yyyy-mm-dd] Default: ' + config.since,
              String
          )
          .option(
              '-e, --email [emailOther=emailMain]',
              'Group person by email address.' +
              ' Default: none',
              String
          )
          .option(
              '-u, --until [until-certain-date]',
              'Analyze data until certain date.' +
              ' [always|yesterday|today|lastweek|thisweek|yyyy-mm-dd] Default: ' + config.until,
              String
          )
          .option(
              '-m, --merge-request [false|true]',
              'Include merge requests into calculation. ' + 
              ' Default: ' + config.mergeRequest,
              String
          )
          .option(
              '-p, --path [git-repo]',
              'Git repository to analyze.' +
              ' Default: ' + config.gitPath,
              String
          )
          .option(
              '-b, --branch [branch-name]',
              'Analyze only data on the specified branch. Default: ' + config.branch,
              String
          );
  
      program.on('--help', function() {
          console.log('  Examples:');
          console.log('');
          console.log('   - Estimate hours of project');
          console.log('');
          console.log('       $ git hours');
          console.log('');
          console.log('   - Estimate hours in repository where developers commit' +
                      ' more seldom: they might have 4h(240min) pause between commits');
          console.log('');
          console.log('       $ git hours --max-commit-diff 240');
          console.log('');
          console.log('   - Estimate hours in repository where developer works 5' +
                      ' hours before first commit in day');
          console.log('');
          console.log('       $ git hours --first-commit-add 300');
          console.log('');
          console.log('   - Estimate hours work in repository since yesterday');
          console.log('');
          console.log('       $ git hours --since yesterday');
          console.log('');
          console.log('   - Estimate hours work in repository since 2015-01-31');
          console.log('');
          console.log('       $ git hours --since 2015-01-31');
          console.log('');
          console.log('   - Estimate hours work in repository on the "master" branch');
          console.log('');
          console.log('       $ git hours --branch master');
          console.log('');
          console.log('  For more details, visit https://github.com/kimmobrunfeldt/git-hours');
          console.log('');
      });
  
      program.parse(process.argv);
  }
  
  function parseInputDate(inputDate) {
      switch (inputDate) {
          case 'today':
              return moment().startOf('day');
          case 'yesterday':
              return moment().startOf('day').subtract(1, 'day');
          case 'thisweek':
              return moment().startOf('week');
          case 'lastweek':
              return moment().startOf('week').subtract(1, 'week');
          case 'always':
              return 'always';
          default:
              // XXX: Moment tries to parse anything, results might be weird
              return moment(inputDate, DATE_FORMAT);
      }
  }
  
  function parseSinceDate(since) {
      return parseInputDate(since);
  }
  
  function parseUntilDate(until) {
      return parseInputDate(until);
  }
  
  function parseEmailAlias(alias) {
      if (alias.indexOf("=") > 0) {
          var email = alias.substring(0, alias.indexOf("=")).trim();
          var alias = alias.substring(alias.indexOf("=") + 1).trim();
          // console.warn("Adding alias " + email + " -> " + alias);
          if (config.emailAliases === undefined) {
              config.emailAliases = {}
          }
          config.emailAliases[email] = alias
      } else {
          console.error("ERROR: Invalid alias: " + alias);
      }
  }
  
  function mergeDefaultsWithArgs(conf) {
      return {
          range: program.range,
          maxCommitDiffInMinutes: program.maxCommitDiff || conf.maxCommitDiffInMinutes,
          firstCommitAdditionInMinutes: program.firstCommitAdd || conf.firstCommitAdditionInMinutes,
          since: program.since || conf.since,
          until: program.until || conf.until,
          gitPath: program.path || conf.gitPath,
          mergeRequest: program.mergeRequest !== undefined ? (program.mergeRequest == "true") : conf.mergeRequest,
          branch: program.branch || conf.branch
      };
  }
  
  // Estimates spent working hours based on commit dates
  function estimateHours(dates) {
      if (dates.length < 2) {
          return 0;
      }
  
      // Oldest commit first, newest last
      var sortedDates = dates.sort(function(a, b) {
          return a - b;
      });
      var allButLast = _.take(sortedDates, sortedDates.length - 1);
  
      var totalHours = _.reduce(allButLast, function(hours, date, index) {
          var nextDate = sortedDates[index + 1];
          var diffInMinutes = (nextDate - date) / 1000 / 60;
  
          // Check if commits are counted to be in same coding session
          if (diffInMinutes < config.maxCommitDiffInMinutes) {
              return hours + diffInMinutes / 60;
          }
  
          // The date difference is too big to be inside single coding session
          // The work of first commit of a session cannot be seen in git history,
          // so we make a blunt estimate of it
          return hours + config.firstCommitAdditionInMinutes / 60;
  
      }, 0);
  
      return Math.round(totalHours);
  }
  
  // Promisify nodegit's API of getting all commits in repository
  function getCommits(gitPath, branch) {
      return git.Repository.open(gitPath)
      .then(function(repo) {
          var allReferences = getAllReferences(repo);
  
          if (branch) {
              filterPromise = Promise.filter(allReferences, function(reference) {
                  return (reference == ('refs/heads/' + branch));
              });
          }
          else {
              filterPromise = Promise.filter(allReferences, function(reference) {
                  return reference.match(/refs\/heads\/.*/);
              });
          }
  
          return filterPromise.map(function(branchName) {
              return getBranchLatestCommit(repo, branchName);
          })
          .map(function(branchLatestCommit) {
              return getBranchCommits(branchLatestCommit);
          })
          .reduce(function(allCommits, branchCommits) {
              _.each(branchCommits, function(commit) {
                  allCommits.push(commit);
              });
  
              return allCommits;
          }, [])
          .then(function(commits) {
              // Multiple branches might share commits, so take unique
              var uniqueCommits = _.uniq(commits, function(item, key, a) {
                  return item.sha;
              });
  
              return uniqueCommits.filter(function (commit) {
                  // Exclude all commits starting with "Merge ..."
                  if (!config.mergeRequest && commit.message.startsWith("Merge ")) {
                      return false;
                  } else {
                      return true;
                  }
              });
          });
      });
  }
  
  function getAllReferences(repo) {
      return repo.getReferenceNames(git.Reference.TYPE.LISTALL);
  }
  
  function getBranchLatestCommit(repo, branchName) {
      return repo.getBranch(branchName).then(function(reference) {
          return repo.getBranchCommit(reference.name());
      });
  }
  
  function getBranchCommits(branchLatestCommit) {
      return new Promise(function(resolve, reject) {
          var history = branchLatestCommit.history();
          var commits = [];
  
          history.on('commit', function(commit) {
              var author = null;
              if (!_.isNull(commit.author())) {
                  author = {
                      name: commit.author().name(),
                      email: commit.author().email()
                  };
              }
  
              var commitData = {
                  sha: commit.sha(),
                  date: commit.date(),
                  message: commit.message(),
                  author: author
              };
  
              var isValidSince = true;
              var sinceAlways = config.since === 'always' || !config.since;
              if (sinceAlways || moment(commitData.date.toISOString()).isAfter(config.since)) {
                  isValidSince = true;
              } else {
                  isValidSince = false;
              }
  
              var isValidUntil = true;
              var untilAlways = config.until === 'always' || !config.until;
              if (untilAlways || moment(commitData.date.toISOString()).isBefore(config.until)) {
                  isValidUntil = true;
              } else {
                  isValidUntil = false;
              }
  
              if (isValidSince && isValidUntil) {
                  commits.push(commitData);
              }
          });
  
          history.on('end', function() {
              resolve(commits);
          });
  
          history.on('error', function(err) {
              reject(err);
          });
  
          // Start emitting events.
          history.start();
      });
  }
  
  main();
  