summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorKimmo Brunfeldt <kimmobrunfeldt@gmail.com>2015-09-22 23:22:43 +0300
committerKimmo Brunfeldt <kimmobrunfeldt@gmail.com>2015-09-22 23:22:43 +0300
commitb57bcd3f4c79ed4bcdccb7b6f9b0c7636183b916 (patch)
tree726ba24a648d6eeb23f42433e5a05c5c208b70a2 /src
parent471cdea43e44d79f3024ff849c85776238144b8f (diff)
downloadgit-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-xsrc/index.js275
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();