diff options
author | Kimmo Brunfeldt <kimmobrunfeldt@gmail.com> | 2015-09-22 23:22:43 +0300 |
---|---|---|
committer | Kimmo Brunfeldt <kimmobrunfeldt@gmail.com> | 2015-09-22 23:22:43 +0300 |
commit | b57bcd3f4c79ed4bcdccb7b6f9b0c7636183b916 (patch) | |
tree | 726ba24a648d6eeb23f42433e5a05c5c208b70a2 /src | |
parent | 471cdea43e44d79f3024ff849c85776238144b8f (diff) | |
download | git-hours-b57bcd3f4c79ed4bcdccb7b6f9b0c7636183b916.zip git-hours-b57bcd3f4c79ed4bcdccb7b6f9b0c7636183b916.tar.gz git-hours-b57bcd3f4c79ed4bcdccb7b6f9b0c7636183b916.tar.bz2 |
Normalize project structure. Fix linter errors. Conver since comparison to use moment
Diffstat (limited to 'src')
-rwxr-xr-x | src/index.js | 275 |
1 files changed, 275 insertions, 0 deletions
diff --git a/src/index.js b/src/index.js new file mode 100755 index 0000000..54e26a5 --- /dev/null +++ b/src/index.js @@ -0,0 +1,275 @@ +#!/usr/bin/env node + +var Promise = require('bluebird'); +var git = require('nodegit'); +var program = require('commander'); +var _ = require('lodash'); +var moment = require('moment'); +var exec = Promise.promisify(require('child_process').exec); + +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' +}; + +function main() { + parseArgs(); + config = mergeDefaultsWithArgs(config); + config.since = parseSinceDate(config.since); + + getCommits('.').then(function(commits) { + var commitsByEmail = _.groupBy(commits, function(commit) { + return commit.author.email || 'unknown'; + }); + 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 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 + ); + + 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(' For more details, visit https://github.com/kimmobrunfeldt/git-hours'); + console.log(''); + }); + + program.parse(process.argv); +} + +function parseSinceDate(since) { + switch (since) { + 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(since, DATE_FORMAT); + } +} + +function mergeDefaultsWithArgs(conf) { + return { + range: program.range, + maxCommitDiffInMinutes: program.maxCommitDiff || conf.maxCommitDiffInMinutes, + firstCommitAdditionInMinutes: program.firstCommitAdd || conf.firstCommitAdditionInMinutes, + since: program.since || conf.since + }; +} + +// 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) { + return git.Repository.open(gitPath) + .then(function(repo) { + var branchNames = getBranchNames(gitPath); + + return Promise.map(branchNames, 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; + }); + }); +} + +function getBranchNames(gitPath) { + var cmd = "git branch --no-color | awk -F ' +' '! /\\(no branch\\)/ {print $2}'"; + return new Promise(function(resolve, reject) { + exec(cmd, {cwd: gitPath}, function(err, stdout, stderr) { + if (err) { + reject(err); + } + + resolve(stdout + .split('\n') + .filter(function(e) { return e; }) // Remove empty + .map(function(str) { return str.trim(); }) // Trim whitespace + ); + }); + }); +} + +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 sinceAlways = config.since === 'always' || !config.since; + if (sinceAlways || moment(commitData.date.toISOString()).isAfter(config.since)) { + commits.push(commitData); + } + }); + + history.on('end', function() { + resolve(commits); + }); + + history.on('error', function(err) { + reject(err); + }); + + // Start emitting events. + history.start(); + }); +} + +main(); |