diff options
-rw-r--r-- | .eslintrc | 59 | ||||
-rw-r--r-- | .jscsrc | 73 | ||||
-rw-r--r-- | .travis.yml | 6 | ||||
-rw-r--r-- | CONTRIBUTING.md | 55 | ||||
-rw-r--r-- | Gruntfile.js | 40 | ||||
-rw-r--r-- | LICENSE | 4 | ||||
-rw-r--r-- | README.md | 31 | ||||
-rw-r--r-- | package.json | 23 | ||||
-rwxr-xr-x | src/index.js (renamed from index.js) | 99 | ||||
-rw-r--r-- | test/test-functional.js | 101 | ||||
-rwxr-xr-x | tools/ci.sh | 16 |
11 files changed, 309 insertions, 198 deletions
diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..6c07347 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,59 @@ +{ + "env": { + "browser": true, + "amd": true, + "node": true + }, + + "globals": { + "define": false, + "require": true, + + "alert": true, + "console": true, + "module": true, + + "describe": true, + "it": true, + "before": true, + "beforeEach": true, + "after": true, + "afterEach": true, + "expect": true, + "should": true, + + "Promise": true, // allow possibly overwriting Promise + }, + + "rules": { + "dot-notation": 0, // allow obj['somePropertyName'] + "new-cap": 0, // do not require 'new' keyword, allows i.e. "var promise = $.Deferred()" + "no-alert": 0, // disencourage alert() and confirm() + "no-console": 0, + "no-mixed-spaces-and-tabs": 1, + "semi-spacing": [2, {"before": false, "after": true}], + "no-spaced-func": 1, + "no-undef": 1, + "no-shadow": 1, + "no-trailing-spaces": 1, + "no-extra-parens": 1, + "no-underscore-dangle": 0, // allow _variableName + "no-new": 0, + "no-nested-ternary": 1, + "no-process-exit": 0, + + // requires local variable names to be used, but allows unused arguments + "no-unused-vars": [2, { "vars": "all", "args": "none" }], + "no-use-before-define": 0, + + // disable requirements for strict spacing rules in object properties or assignments + "key-spacing": 0, + "no-multi-spaces": 0, + + "quotes": 0, // allow both single and double quotes + "space-after-keywords": 1, + "space-infix-ops": 1, + "space-return-throw-case": 1, + "strict": 0 + } +} @@ -0,0 +1,73 @@ +{ + "disallowImplicitTypeConversion": ["string"], + "disallowMixedSpacesAndTabs": true, + "disallowMultipleLineBreaks": true, + "disallowMultipleVarDecl": true, + "disallowNewlineBeforeBlockStatements": true, + "disallowMultipleLineStrings": true, + "disallowMultipleVarDecl": null, + "disallowSpaceAfterPrefixUnaryOperators": true, + "disallowSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInsideObjectBrackets": null, + "disallowSpacesInsideArrayBrackets": "all", + "disallowSpacesInsideParentheses": true, + "disallowTrailingWhitespace": true, + + "maximumLineLength": { + "value": 100, + "allowComments": true, + "allowRegex": true + }, + + "fileExtensions": [".js", ".jsx"], + + "requireCamelCaseOrUpperCaseIdentifiers": true, + "requireCommaBeforeLineBreak": true, + "requireCurlyBraces": [ + "if", + "else", + "for", + "while", + "do", + "try", + "catch" + ], + "requireSpaceAfterBinaryOperators": true, + "requireSpaceAfterKeywords": [ + "do", + "for", + "if", + "else", + "switch", + "case", + "try", + "catch", + "void", + "while", + "with", + "return", + "typeof" + ], + "requireSpaceBeforeBinaryOperators": [ + "=", "+=", "-=", "*=", "/=", "%=", "<<=", ">>=", ">>>=", + "&=", "|=", "^=", "+=", + + "+", "-", "*", "/", "%", "<<", ">>", ">>>", "&", + "|", "^", "&&", "||", "===", "==", ">=", + "<=", "<", ">", "!=", "!==" + ], + "requireSpacesInConditionalExpression": true, + "requireSpaceBeforeBlockStatements": true, + "requireSpacesInForStatement": true, + "requireLineFeedAtFileEnd": true, + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + + "validateLineBreaks": "LF", + "validateIndentation": 4, + + "excludeFiles": ["node_modules/**"] +} diff --git a/.travis.yml b/.travis.yml index f6f876a..e80033d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,11 @@ language: node_js node_js: -- '0.12' + - '0.12' sudo: false before_script: -- npm install -g mocha grunt-cli nodegit + - npm install -g nodegit +script: + - npm run ci notifications: email: - kimmobrunfeldt+git-hours@gmail.com diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..bfd0d1f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,55 @@ +# Contribution documentation + +Pull requests and contributions are warmly welcome. +Please follow existing code style and commit message conventions. Also remember to keep documentation +updated. + +**Pull requests:** You don't need to bump version numbers or modify anything related to releasing. That stuff is fully automated, just write the functionality. + +## Get started with development + +* [Install local environment](#install-environment). + +## General project stuff + +This package uses npm/node tools just in the developer environment. NPM scripts +are used to run tasks. + +#### Versioning + +Versioning follows [Semantic Versioning 2.0.0](http://semver.org/). +All versions are pushed as git tags. + +## Install environment + +Install tools needed for development: + + npm install + + +## Test + +Tests are written with [Mocha](http://mochajs.org/). + +Running tests: + + npm test + +## Release + +**Before releasing, make sure there are no uncommitted files, +and CI passes.** + +Creating a new release of the package is simple: + +1. Commit and push all changes +2. Run local tests and linters with `npm run ci` +3. Make sure Travis passes tests too. +4. Run `releasor`, which will create new tag and publish code to GitHub and NPM. +5. Edit GitHub release notes + +By default, patch release is done. You can specify the version bump as a parameter: + + releasor --bump minor + +Valid version bump values: `major`, `minor`, `patch`. diff --git a/Gruntfile.js b/Gruntfile.js deleted file mode 100644 index ce4e96b..0000000 --- a/Gruntfile.js +++ /dev/null @@ -1,40 +0,0 @@ -module.exports = function(grunt) { - - grunt.initConfig({ - pkg: grunt.file.readJSON('package.json'), - jshint: { - files: ['*.js', 'test/**/*.js'], - options: { - globals: { - jQuery: true, - console: true, - module: true, - Promise: true - } - } - }, - shell: { - mocha: { - options: { - stdout: true - }, - command: 'mocha' - } - }, - release: { - options: { - // Don't release to NPM since travis does this - npm: false, - npmtag: false, - // default: 'release <%= version %>' - commitMessage: 'Release <%= version %>' - } - } - }); - - grunt.loadNpmTasks('grunt-contrib-jshint'); - grunt.loadNpmTasks('grunt-shell'); - grunt.loadNpmTasks('grunt-release'); - - grunt.registerTask('test', ['jshint', 'shell:mocha']); -}; @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2014 Kimmo Brunfeldt +Copyright (c) 2015 Kimmo Brunfeldt Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE.
\ No newline at end of file +SOFTWARE. @@ -27,6 +27,8 @@ From a person working 8 hours per day, it would take more than 3 years to build $ npm install -g git-hours +**WARNING: Does not work with node 4.0! See [this issue](https://github.com/nodegit/nodegit/issues/723)** + **NOTE: Use node version >=0.12.x.** *You can try to get nodegit working with older versions too but 0.12.x was the easiest. With older node versions, you might need to `npm install -g nodegit` too.* @@ -164,32 +166,3 @@ system by running: $ exit $ vagrant destroy -f ``` - -# For contributors - -Documentation for git-hours developers. - -## Release - -* Commit all changes -* Run `grunt release`, which will create new tag and publish code to GitHub -* Edit GitHub release notes -* Release to NPM - - $ git checkout x.x.x - $ npm publish - - -To see an example how to release minor/major, check https://github.com/geddski/grunt-release - -## Test - -Tests can be run with command - - $ grunt test - -or - - $ npm test - -You need to have *mocha* installed globally with `npm install -g mocha`. diff --git a/package.json b/package.json index 3b8cb59..98479c6 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,9 @@ "name": "git-hours", "version": "1.0.0", "description": "Estimate time spent on a git repository", - "main": "index.js", - "scripts": { - "test": "grunt test" - }, + "main": "./src/index.js", "bin": { - "git-hours": "./index.js" + "git-hours": "./src/index.js" }, "repository": { "type": "git", @@ -31,13 +28,19 @@ "bluebird": "^2.1.3", "commander": "^2.2.0", "lodash": "^2.4.1", + "moment": "^2.10.6", "nodegit": "^0.4.1" }, "devDependencies": { - "grunt": "^0.4.5", - "grunt-contrib-jshint": "^0.10.0", - "grunt-release": "^0.7.0", - "grunt-shell": "^0.7.0", - "mocha": "^2.2.5" + "eslint": "^1.5.1", + "jscs": "^2.1.1", + "mocha": "^2.3.3", + "releasor": "^0.1.3" + }, + "scripts": { + "ci": "./tools/ci.sh", + "test": "mocha", + "eslint": "eslint --ext .js ./src ./test", + "jscs": "jscs ./src ./test" } } @@ -1,14 +1,14 @@ #!/usr/bin/env node -var fs = require('fs'); - 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" @@ -17,16 +17,16 @@ var config = { // How many minutes should be added for the first commit of coding session firstCommitAdditionInMinutes: 2 * 60, - //Since data + // Include commits since time x since: 'always' }; function main() { parseArgs(); config = mergeDefaultsWithArgs(config); - parseSinceDate(config); + config.since = parseSinceDate(config.since); - commits('.').then(function(commits) { + getCommits('.').then(function(commits) { var commitsByEmail = _.groupBy(commits, function(commit) { return commit.author.email || 'unknown'; }); @@ -63,30 +63,29 @@ function main() { } function parseArgs() { - function list(val) { - return val.split(','); - } - function int(val) { return parseInt(val, 10); } program - .version(require('./package.json').version) + .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, + '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, + '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|tonight|lastweek|yyyy-mm-dd] Default: ' + config.since, + 'Analyze data since certain date.' + + ' [always|yesterday|today|lastweek|thisweek|yyyy-mm-dd] Default: ' + config.since, String ); @@ -97,11 +96,13 @@ function parseArgs() { 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(' - 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(' - 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(''); @@ -120,42 +121,30 @@ function parseArgs() { program.parse(process.argv); } -function parseSinceDate(options){ - var justNow, thisPeriod, paramDate; - switch(options.since){ - case 'tonight': - justNow = new Date(); - thisPeriod = new Date(justNow.getFullYear(), justNow.getMonth(), justNow.getUTCDate()); - config.since = thisPeriod; - break; +function parseSinceDate(since) { + switch (since) { + case 'today': + return moment().startOf('day'); case 'yesterday': - justNow = new Date(); - thisPeriod = new Date(justNow.getFullYear(), justNow.getMonth(), justNow.getUTCDate()-1); - config.since = thisPeriod; - break; + return moment().startOf('day').subtract(1, 'day'); + case 'thisweek': + return moment().startOf('week'); case 'lastweek': - justNow = new Date(); - thisPeriod = new Date(justNow.getFullYear(), justNow.getMonth(), justNow.getUTCDate()-7); - config.since = thisPeriod; - break; + return moment().startOf('week').subtract(1, 'week'); case 'always': - break; + return 'always'; default: - paramDate = new Date(String(config.since)); - if(paramDate === undefined){ - config.since = 'always'; - }else{ - config.since = paramDate; - } + // XXX: Moment tries to parse anything, results might be weird + return moment(since, DATE_FORMAT); } } -function mergeDefaultsWithArgs(config) { +function mergeDefaultsWithArgs(conf) { return { range: program.range, - maxCommitDiffInMinutes: program.maxCommitDiff || config.maxCommitDiffInMinutes, - firstCommitAdditionInMinutes: program.firstCommitAdd || config.firstCommitAdditionInMinutes, - since: program.since || config.since + maxCommitDiffInMinutes: program.maxCommitDiff || conf.maxCommitDiffInMinutes, + firstCommitAdditionInMinutes: program.firstCommitAdd || conf.firstCommitAdditionInMinutes, + since: program.since || conf.since }; } @@ -171,27 +160,27 @@ function estimateHours(dates) { }); var allButLast = _.take(sortedDates, sortedDates.length - 1); - var hours = _.reduce(allButLast, function(hours, date, index) { + 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); + 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); + return hours + config.firstCommitAdditionInMinutes / 60; }, 0); - return Math.round(hours); + return Math.round(totalHours); } // Promisify nodegit's API of getting all commits in repository -function commits(gitPath) { +function getCommits(gitPath) { return git.Repository.open(gitPath) .then(function(repo) { var branchNames = getBranchNames(gitPath); @@ -237,18 +226,7 @@ function getBranchNames(gitPath) { }); } -function getBranches(repo, names) { - var branches = []; - for (var i = 0; i < names.length; ++i) { - branches.push(getBranch(repo, names[i])); - } - - return Promise.all(branches); -} - function getBranchLatestCommit(repo, branchName) { - var type = git.Reference.TYPE.SYMBOLIC; - return repo.getBranch(branchName).then(function(reference) { return repo.getBranchCommit(reference.name()); }); @@ -275,7 +253,8 @@ function getBranchCommits(branchLatestCommit) { author: author }; - if(commitData.date > config.since || config.since === 'always'){ + var sinceAlways = config.since === 'always' || !config.since; + if (sinceAlways || moment(commitData.date.toISOString()).isAfter(config.since)) { commits.push(commitData); } }); diff --git a/test/test-functional.js b/test/test-functional.js index d716e07..a852597 100644 --- a/test/test-functional.js +++ b/test/test-functional.js @@ -1,75 +1,66 @@ -var _ = require('lodash'); -var assert = require("assert"); +var assert = require('assert'); var exec = require('child_process').exec; var totalHoursCount; describe('git-hours', function() { + it('should output json', function(done) { + exec('node ./src/index.js', function(err, stdout, stderr) { + if (err !== null) { + throw new Error(stderr); + } - describe('cli', function() { - - it('should output json', function(done) { - exec('node index.js', function(err, stdout, stderr) { - if (err !== null) { - throw new Error(stderr); - } - - console.log('stdout:', stdout); - var work = JSON.parse(stdout); - assert.notEqual(work.total.hours.length, 0); - assert.notEqual(work.total.commits.length, 0); - totalHoursCount = work.total.hours; - done(); - }); + console.log('stdout:', stdout); + var work = JSON.parse(stdout); + assert.notEqual(work.total.hours.length, 0); + assert.notEqual(work.total.commits.length, 0); + totalHoursCount = work.total.hours; + done(); }); }); - describe('Since option', function(){ - it('Should analyse since today', function(done) { - exec('node index.js --since today', function(err, stdout, stderr) { - assert.ifError(err); - var work = JSON.parse(stdout); - assert.strictEqual(typeof work.total.hours, 'number'); - done(); - }); + it('Should analyse since today', function(done) { + exec('node ./src/index.js --since today', function(err, stdout, stderr) { + assert.ifError(err); + var work = JSON.parse(stdout); + assert.strictEqual(typeof work.total.hours, 'number'); + done(); }); + }); - it('Should analyse since yesterday', function(done) { - exec('node index.js --since yesterday', function(err, stdout, stderr) { - assert.ifError(err); - var work = JSON.parse(stdout); - assert.strictEqual(typeof work.total.hours, 'number'); - done(); - }); + it('Should analyse since yesterday', function(done) { + exec('node ./src/index.js --since yesterday', function(err, stdout, stderr) { + assert.ifError(err); + var work = JSON.parse(stdout); + assert.strictEqual(typeof work.total.hours, 'number'); + done(); }); + }); - it('Should analyse since last week', function(done){ - exec('node index.js --since lastweek', function(err, stdout, stderr) { - assert.ifError(err); - var work = JSON.parse(stdout); - assert.strictEqual(typeof work.total.hours, 'number'); - done(); - }); + it('Should analyse since last week', function(done) { + exec('node ./src/index.js --since lastweek', function(err, stdout, stderr) { + assert.ifError(err); + var work = JSON.parse(stdout); + assert.strictEqual(typeof work.total.hours, 'number'); + done(); }); + }); - it('Should analyse since a specific date', function(done){ - exec('node index.js --since 2015-01-01', function(err, stdout, stderr) { - assert.ifError(err); - var work = JSON.parse(stdout); - assert.notEqual(work.total.hours, 0); - done(); - }); + it('Should analyse since a specific date', function(done) { + exec('node ./src/index.js --since 2015-01-01', function(err, stdout, stderr) { + assert.ifError(err); + var work = JSON.parse(stdout); + assert.notEqual(work.total.hours, 0); + done(); }); + }); - it('Should analyse as without param', function(done){ - exec('node index.js --since always', function(err, stdout, stderr) { - assert.ifError(err); - var work = JSON.parse(stdout); - assert.equal(work.total.hours, totalHoursCount); - done(); - }); + it('Should analyse as without param', function(done) { + exec('node ./src/index.js --since always', function(err, stdout, stderr) { + assert.ifError(err); + var work = JSON.parse(stdout); + assert.equal(work.total.hours, totalHoursCount); + done(); }); }); - - }); diff --git a/tools/ci.sh b/tools/ci.sh new file mode 100755 index 0000000..480fa1a --- /dev/null +++ b/tools/ci.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# NOTE: Run this only from project root! + +# Run all commands and if one fails, return exit status of the last failed +# command + +EXIT_STATUS=0 + +echo -e "\n--> Linting code..\n" +npm run jscs || EXIT_STATUS=$? +npm run eslint || EXIT_STATUS=$? + +echo -e "\n--> Running tests..\n" +npm test || EXIT_STATUS=$? + +exit $EXIT_STATUS |