├── src ├── index.integration.js ├── prepush-hook-file.js ├── prepush-ui.js ├── reporters │ ├── jsonStream.js │ ├── scenario.test.js │ ├── junit.js │ ├── junitTest.test.js │ └── cliSummary.js ├── run.test.js ├── streamReconnect.test.js ├── prepush-ui-non-tty.js ├── textUi.test.js ├── open.js ├── streamReconnect.js ├── gui-control.js ├── flow.js ├── textUi.js ├── prepush-ui-tty.test.js ├── safeSpawn.test.js ├── safeSpawn.js ├── MainProcess.js ├── init.js ├── analysers.js ├── config.js ├── index.js ├── prepush.js ├── run.js └── prepush-ui-tty.js ├── test ├── mocha.opts ├── setup.js ├── fixtures │ ├── testRepo │ │ ├── testFile.js │ │ └── .sidekickrc │ └── safeSpawnFixture.js └── runTest.js ├── sidekick.js ├── .eslintrc ├── .travis.yml ├── .gitignore ├── .sidekickrc ├── hooks └── pre-push ├── package.json └── README.md /src/index.integration.js: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --ui bdd 2 | -r test/setup.js 3 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | global.assert = require('chai').assert; 2 | global.expect = require('chai').expect; 3 | 4 | global.sinon = require("sinon"); 5 | sinon.assert.expose(assert, { prefix: "spy" }); 6 | -------------------------------------------------------------------------------- /sidekick.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /** 3 | * kicks off - required as browserify build loses the main module 4 | */ 5 | "use strict"; 6 | `sidekick needs a NodeJS version supporting es6! sidekickcode.com/cli-install`; 7 | require("./src/index").run(); 8 | -------------------------------------------------------------------------------- /src/prepush-hook-file.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | exports.newHookFile = newHookFile; 4 | exports.withComment = withComment; 5 | 6 | function withComment() { 7 | return `# sidekick prepush analysis\nexec sk prepush $@\n`; 8 | } 9 | 10 | function newHookFile() { 11 | return `#!/bin/sh 12 | ${withComment()}`; 13 | } 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "global": { 3 | "assert": false 4 | }, 5 | "env": { 6 | "mocha": true, 7 | "node": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 6 11 | }, 12 | "extends": "eslint:recommended", 13 | "rules": { 14 | "no-console": "off", 15 | "comma-dangle": "off" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/prepush-ui.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UI for prepush 3 | */ 4 | "use strict"; 5 | 6 | module.exports = exports = pickAndRunUI; 7 | 8 | const ttyUi = require("./prepush-ui-tty"); 9 | const nonTtyUi = require("./prepush-ui-non-tty"); 10 | 11 | 12 | function pickAndRunUI(setup) { 13 | return process.stdout.isTTY ? ttyUi(setup) : nonTtyUi(setup); 14 | } 15 | -------------------------------------------------------------------------------- /test/fixtures/testRepo/testFile.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var foo = void 0; 4 | 5 | function hello(x) { 6 | if(x == 0) { 7 | console.log("jshint will like this"); 8 | console.log("yup"); 9 | } 10 | } 11 | 12 | function hello(x) { 13 | if(x == 0) { 14 | console.log("jshint will like this") && console.log("yup"); 15 | return 16 | {}; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.0" 4 | - "5.1" 5 | - "4.2" 6 | 7 | notifications: 8 | email: false 9 | slack: we-are-sidekick:GeIT5wtuKMLImpFXYHWzIGzI 10 | 11 | script: 12 | - npm i 13 | - npm test 14 | - ./sidekick.js -v 15 | - echo If you are looking at this build config as an example of how to configure Sidekick on travis - be aware that the correct invocation is sidekick run --travis 16 | - ./sidekick.js run . --travis 17 | -------------------------------------------------------------------------------- /src/reporters/jsonStream.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSON output 3 | */ 4 | "use strict"; 5 | 6 | module.exports = exports = reporter; 7 | 8 | const defaultOutput = (x) => console.log(x); 9 | 10 | function reporter(emitter, outputter) { 11 | const output = outputter || defaultOutput; 12 | 13 | emitter.on("result", outputJson); 14 | 15 | emitter.on("error", outputJson); 16 | 17 | function outputJson(x) { 18 | output(JSON.stringify(x)); 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /src/run.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var run = require('../src/run'); 3 | 4 | describe("run command", function() { 5 | 6 | describe("parsing input", function() { 7 | it("disables travis and compare flags", function() { 8 | assert.instanceOf(run.flagsToCommand("/some/path", { 9 | versus: "x", "travis": true, 10 | }), Error); 11 | }) 12 | 13 | it("supports travis", function() { 14 | assert.equal(run.flagsToCommand("/some/path", { 15 | "travis": true, 16 | }).travis, true); 17 | }) 18 | 19 | }) 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | 36 | src/analysers/installed 37 | test/fixtures/analysers 38 | -------------------------------------------------------------------------------- /test/fixtures/testRepo/.sidekickrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "test", 4 | "app/build", 5 | "app/dist", 6 | "build", 7 | "app/demoRepo", 8 | "app/node_modules", 9 | "dist", 10 | "tmp", 11 | "app/browser/vendor" 12 | ], 13 | "languages": { 14 | "js": { 15 | "sidekick-js-todos": { 16 | "failCiOnError": false 17 | }, 18 | "sidekick-jscs": { 19 | "failCiOnError": false 20 | }, 21 | "sidekick-eslint": { 22 | "failCiOnError": true 23 | } 24 | }, 25 | "cs": { 26 | "sidekick-coffeelint": { 27 | "failCiOnError": false 28 | } 29 | }, 30 | "json": { 31 | "sidekick-david": { 32 | "failCiOnError": false 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.sidekickrc: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": [ 3 | "test", 4 | "app/build", 5 | "app/dist", 6 | "build", 7 | "app/demoRepo", 8 | "app/node_modules", 9 | "dist", 10 | "tmp", 11 | "app/browser/vendor" 12 | ], 13 | "languages": { 14 | "js": { 15 | "sidekick-js-todos": { 16 | "failCiOnError": false, 17 | "version": "1.0.1" 18 | }, 19 | "sidekick-jscs": { 20 | "failCiOnError": false 21 | }, 22 | "sidekick-eslint": { 23 | "failCiOnError": true 24 | } 25 | }, 26 | "cs": { 27 | "sidekick-coffeelint": { 28 | "failCiOnError": false 29 | } 30 | }, 31 | "json": { 32 | "sidekick-david": { 33 | "failCiOnError": false 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var chai = require('chai'); 4 | var assert = chai.assert; 5 | var expect = chai.expect; 6 | var chaiAsPromised = require("chai-as-promised"); 7 | chai.use(chaiAsPromised); 8 | 9 | var sinon = require('sinon'); 10 | 11 | var fs = require('fs-extra'); 12 | var path = require('path'); 13 | var exec = require('child_process').exec; 14 | 15 | var run = require('../src/run'); 16 | 17 | describe('installer', function() { 18 | 19 | describe('positive tests', function() { 20 | 21 | before(function(){ 22 | exec(`cd fixtures/testRepo && git init && git add .`, function(err, data){ 23 | }); 24 | }); 25 | 26 | it('Run in CI mode will installs all analysers in a config file', function(done){ 27 | 28 | this.timeout(40000); 29 | 30 | var testRepoPath = path.join(__dirname, '/fixtures/testRepo'); 31 | run(["run", testRepoPath, '--ci']).then(function(results){ 32 | done(); 33 | }, function(err){ 34 | done(); 35 | }); 36 | 37 | }); 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /test/fixtures/safeSpawnFixture.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // fixture script to use as a child process 3 | 4 | if(process.env.EXIT_WITH) { 5 | process.exit(NUMBER(process.env.EXIT_WITH)); 6 | 7 | 8 | } else if (process.env.OUTPUT_UP_WITH_GARBAGE) { 9 | console.log(JSON.stringify({ junkJson: "before" })); 10 | console.log("blah blah before"); 11 | console.log(JSON.stringify({ up: true })); 12 | console.log("blah blah after"); 13 | stayUp(); 14 | 15 | } else if (process.env.OUTPUT_UP) { 16 | console.log(JSON.stringify({ up: true })); 17 | 18 | stayUp(); 19 | 20 | 21 | } else if (process.env.OUTPUT_GARBAGE) { 22 | console.log('oaksdoskd\nsdjsaoijad\n{}\n{ scooby:do }\n{"scooby":"do"}\naoskdo'); 23 | 24 | stayUp(); 25 | 26 | 27 | } else if (process.env.EXPLICIT_FAILURE) { 28 | console.log(JSON.stringify({ junkJson: "before" })); 29 | console.log("blah blah before"); 30 | console.log(JSON.stringify({ error: "failed, sorry" })); 31 | console.log("blah blah after"); 32 | 33 | stayUp(); 34 | 35 | 36 | } else if (process.env.DO_NOTHING) { 37 | stayUp(); 38 | 39 | 40 | } else { 41 | throw new Error("fixture script not used correctly"); 42 | } 43 | 44 | function stayUp() { 45 | setTimeout(function() { 46 | }, 5000); 47 | } 48 | -------------------------------------------------------------------------------- /src/streamReconnect.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var reconnect = require("./streamReconnect"); 4 | var net = require('net'); 5 | var PORT = 19922; 6 | var _ = require("lodash"); 7 | 8 | describe('streamReconnect', function() { 9 | 10 | it('limits retries', function(done) { 11 | var attempt = 0; 12 | reconnect(function(err, connections, cb) { 13 | attempt += 1; 14 | cb(net.connect({ port: PORT })); 15 | }, { 16 | maxAttempts: 3, 17 | error: function(err) { 18 | assert.match(err.message, /too-many/); 19 | assert.equal(attempt, 3); 20 | done(); 21 | }, 22 | }); 23 | }) 24 | 25 | it('fires an initial connection', function(done) { 26 | reconnect(function(err, count) { 27 | assert.isUndefined(err); 28 | assert.equal(count, 0); 29 | done(); 30 | }, { 31 | error: done, 32 | }); 33 | }) 34 | 35 | it('passes errors to the getStream function', function(done) { 36 | reconnect(function(err, count, cb) { 37 | if(err) { 38 | assert.match(err.message, /ECONNREFUSED/); 39 | done(); 40 | } 41 | 42 | cb(net.connect({port: PORT})) 43 | }, { 44 | maxAttempts: 2, 45 | error: _.noop, 46 | }); 47 | }) 48 | 49 | }) 50 | -------------------------------------------------------------------------------- /hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # dev 4 | DEVELOPMENT=0 5 | PREVENT_PUSH=0 6 | OFFLINE=0 7 | 8 | development() { 9 | SK_DEBUG=1 DEVELOPMENT=1 iojs ~/dev/sk/deployed/app/cli/ui pre-push $@ 10 | } 11 | 12 | installed() { 13 | # sidekick prepush analysis 14 | sk prepush $@ 15 | } 16 | 17 | 18 | input() { 19 | if [[ $OFFLINE == "1" ]]; then 20 | branch=$(git status | head -n 1 | sed 's/On branch //') 21 | local=$(sha HEAD) 22 | remote=$(sha HEAD~5) 23 | echo refs/heads/$branch $local refs/heads/$branch $remote 24 | fi 25 | } 26 | 27 | sha() { 28 | git show $1 --format=%H | head -n 1 29 | } 30 | 31 | args="$@" 32 | if [[ $OFFLINE == "1" ]]; then 33 | branch=$(git show | head -n 1 | sed 's/On branch //') 34 | args="$branch $branch" 35 | fi 36 | 37 | if [[ $DEVELOPMENT == "1" ]]; then 38 | echo "running vs working copy" 39 | if [[ $OFFLINE == "1" ]]; then 40 | input | development $args 41 | else 42 | development $args 43 | fi 44 | else 45 | echo "running vs installed copy" 46 | if [[ $OFFLINE == "1" ]]; then 47 | input | installed $args 48 | else 49 | installed $args 50 | fi 51 | fi 52 | 53 | sk_exit=$? 54 | 55 | if [[ $PREVENT_PUSH == "1" && $sk_exit == "0" ]]; then 56 | echo "preventing push as sk exited with $sk_exit" 57 | exit 1 58 | fi 59 | 60 | exit $sk_exit 61 | -------------------------------------------------------------------------------- /src/prepush-ui-non-tty.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const textUi = require("./textUi"); 4 | 5 | const events = require("events"); 6 | 7 | const log = require("debug")("prepushUiNonTty"); 8 | 9 | module.exports = exports = run; 10 | 11 | function run(setup) { 12 | 13 | const main = setup.main; 14 | const prepush = setup.prepush; 15 | const pushDefinition = setup.push; 16 | const prefix = "push-process:" + pushDefinition.id + ":"; 17 | 18 | const api = new events.EventEmitter; 19 | 20 | main.once(prefix + "end", function(err, pushResult) { 21 | if(err) { 22 | console.error("error while running sidekick: " + err); 23 | } else { 24 | var output = textUi.banner([ 25 | "PUSH COMPLETE", 26 | "Push completed successfully in SidekickJS app :)", 27 | "", 28 | "Git reports an error when its push is cancelled. We cancel git's push so we can push after the fix commit. TL;DR it's fine, your code was pushed.", 29 | ], { 30 | width: 50, 31 | }); 32 | 33 | console.log(output); 34 | api.emit("exit", 0); 35 | } 36 | }); 37 | 38 | prepush.on("exitOptionalSkipping", function(exit) { 39 | console.error(exit.message); 40 | api.emit("exit", exit.code); 41 | }); 42 | 43 | prepush.on("handOffToGui", function(event) { 44 | console.log(event.message); 45 | }); 46 | 47 | return api; 48 | } 49 | -------------------------------------------------------------------------------- /src/textUi.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var ui = require("./textUi"); 4 | 5 | describe('banners', function() { 6 | 7 | it('makes banners', function() { 8 | var expected = 9 | `************************************************************ 10 | * * 11 | * PUSH COMPLETE * 12 | * * 13 | * push handled in SidekickJS app - everything is ok :) * 14 | * * 15 | ************************************************************`; 16 | 17 | 18 | var output = ui.banner([ 19 | "PUSH COMPLETE", 20 | "push handled in SidekickJS app - everything is ok :)", 21 | ], { 22 | width: 60, 23 | }) 24 | 25 | assert.equal(output, expected); 26 | }) 27 | 28 | 29 | it('handles real life use', function() { 30 | var output = ui.banner([ 31 | "PUSH COMPLETE", 32 | "Push completed successfully in SidekickJS app - everything is ok :)", 33 | "", 34 | "Git reported an error because its push was cancelled in favour of the push initated inside the SidekickJS app. This is fine, and your code was pushed.", 35 | ], { 36 | width: 50, 37 | }); 38 | 39 | assert.match(output, /PUSH COMPLETE/); 40 | }) 41 | 42 | }) 43 | -------------------------------------------------------------------------------- /src/open.js: -------------------------------------------------------------------------------- 1 | /** 2 | * opens sidekick at a repo 3 | */ 4 | "use strict"; 5 | 6 | // TODO if adding another similar command, refactor this and open to a common helper 7 | 8 | var git = require("@sidekick/git-helpers"); 9 | var MainProcess = require("./MainProcess"); 10 | var path = require("path"); 11 | var log = require("debug")("open"); 12 | 13 | module.exports = exports = run; 14 | 15 | function run(yargs) { 16 | 17 | var chosen = yargs.argv._[1]; 18 | var inspect = chosen ? path.resolve(process.cwd(), chosen) : process.cwd(); 19 | 20 | git.findRootGitRepo(inspect) 21 | .then(function(repo) { 22 | return MainProcess.acquire({ 23 | error: failure, 24 | }) 25 | .call({ 26 | timeout: 5000, 27 | }, "intent", { 28 | type: "browseRepository", 29 | path: repo, 30 | }); 31 | }) 32 | .then(function() { 33 | process.exit(0); 34 | }) 35 | .catch(git.NotAGitRepo, function(err) { 36 | console.error("sidekick works with git repositories - not in a git repo!"); 37 | process.exit(1); 38 | }) 39 | .catch(function(err) { 40 | if(err.code === "ENOENT") { 41 | console.error("no such file or directory: " + inspect); 42 | process.exit(1); 43 | } else { 44 | failure(err); 45 | } 46 | }); 47 | 48 | function failure(err) { 49 | log("failed to open unexpectedly: " + err.stack); 50 | console.error(err.message + " trying to open GUI at " + inspect); 51 | process.exit(1); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/reporters/scenario.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * shared test scenario for testing reporters 3 | * 4 | */ 5 | "use strict"; 6 | 7 | const EventEmitter = require("events").EventEmitter; 8 | const _ = require("lodash"); 9 | 10 | exports.create = function(extraEvents) { 11 | const emitter = new EventEmitter; 12 | 13 | const events = [ 14 | { 15 | name: "result", 16 | data: { 17 | analyser: "blubHint", 18 | path: "blub.js", 19 | issues: [ 20 | { message: "oh no" }, 21 | { message: "bad thing" }, 22 | ] 23 | } 24 | }, 25 | { 26 | name: "error", 27 | data: { 28 | analyser: "blubHint", 29 | path: "blub.js", 30 | error: { 31 | message: "Terrible", 32 | }, 33 | }, 34 | }, 35 | { 36 | name: "error", 37 | data: { 38 | analyser: "blubHint", 39 | path: "blub.js", 40 | error: { 41 | message: "crash", 42 | stdout: "", 43 | stderr: "Everything is horrible!", 44 | }, 45 | }, 46 | }, 47 | ].concat(extraEvents || []); 48 | 49 | events.push({ 50 | name: "end", 51 | data: {} 52 | }); 53 | 54 | emitter.eventsByName = _.groupBy(events, _.property("name")); 55 | 56 | 57 | emitter.start = function() { 58 | 59 | setImmediate(step); 60 | 61 | function step() { 62 | if(events.length) { 63 | const next = events.shift(); 64 | emitter.emit(next.name, next.data); 65 | 66 | 67 | setImmediate(step); 68 | } 69 | } 70 | }; 71 | 72 | return emitter; 73 | } 74 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sidekick", 3 | "version": "1.12.2", 4 | "description": "Your code, made perfect.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha src/*.test.js src/*/*.test.js", 8 | "coverage": "istanbul cover _mocha src/*/*.test.js" 9 | }, 10 | "bin": { 11 | "sidekick": "./sidekick.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/sidekickcode/sidekick.git" 16 | }, 17 | "keywords": [ 18 | "javascript", 19 | "lint", 20 | "quality", 21 | "static-analysis" 22 | ], 23 | "author": "", 24 | "engineStrict": true, 25 | "engines": { 26 | "node": ">= 4.2.0" 27 | }, 28 | "license": "AGPL-3.0", 29 | "bugs": { 30 | "url": "https://github.com/sidekickcode/tracker/issues" 31 | }, 32 | "homepage": "https://github.com/sidekickcode/sidekick#readme", 33 | "dependencies": { 34 | "@sidekick/analyser-manager": "^2.2.7", 35 | "@sidekick/common": "^3.3.9", 36 | "@sidekick/git-helpers": "^1.2.0", 37 | "@sidekick/runner": "^0.3.1", 38 | "bluebird": "2.9.34", 39 | "chalk": "1.1.3", 40 | "cli-spinner": "0.2.4", 41 | "debug": "2.2.0", 42 | "easy-stdin": "^1.0.1", 43 | "fs-extra": "^0.30.0", 44 | "lodash": "3.10.1", 45 | "proxytron": "^1.0.5", 46 | "rpcjs": "^1.1.0", 47 | "ttys": "0.0.3", 48 | "uuid": "^2.0.2", 49 | "yargs": "4.3.2" 50 | }, 51 | "devDependencies": { 52 | "chai": "3.5.0", 53 | "chai-as-promised": "5.2.0", 54 | "elementtree": "0.1.6", 55 | "flow-bin": "0.22.1", 56 | "istanbul": "0.4.2", 57 | "mocha": "2.4.5", 58 | "sinon": "1.17.3" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/streamReconnect.js: -------------------------------------------------------------------------------- 1 | /** 2 | * stream reconnection behaviour - will reconnect on "close", "end" or "error" event 3 | * of stream 4 | * 5 | * type reconnect = reconnect(connector, options: { delay = 250, maxAttempts = 50) 6 | * type connector = (error: Error | null, reconnectCount: number, getStream: streamCallback) => void 7 | * type streamCallback = (stream: Stream) => void 8 | */ 9 | 10 | var _ = require("lodash"); 11 | 12 | module.exports = exports = reconnect; 13 | 14 | function reconnect(getStream, opts) { 15 | var stream; 16 | var reconnectNumber = 0; 17 | var stopped = false; 18 | 19 | opts = _.defaults(opts || {}, { 20 | delay: 250, 21 | maxAttempts: 50, 22 | }); 23 | 24 | if(!opts.error) { 25 | throw new Error("must provide error handler"); 26 | } 27 | 28 | process.nextTick(connect); 29 | 30 | return { 31 | stop: stop, 32 | }; 33 | 34 | function connect(error) { 35 | if(stopped) { 36 | return; 37 | } 38 | 39 | // are we done on this connection attempt? 40 | var reconnecting = false; 41 | 42 | if(reconnectNumber >= opts.maxAttempts) { 43 | return opts.error(new Error("too-many-attempts")); 44 | } 45 | 46 | getStream(error, reconnectNumber, function(newStream) { 47 | stream = newStream; 48 | 49 | stream.once("close", retry); 50 | stream.once("end", retry); 51 | stream.once("error", retry); 52 | }); 53 | 54 | reconnectNumber += 1; 55 | 56 | function retry(err) { 57 | if(reconnecting) { 58 | return; 59 | } 60 | 61 | // last stream failed, so cleanup, then retry 62 | stream.removeListener("error", retry); 63 | stream.removeListener("end", retry); 64 | stream.removeListener("close", retry); 65 | 66 | reconnecting = true; 67 | connect(err); 68 | } 69 | } 70 | 71 | function stop() { 72 | stopped = true; 73 | 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /src/gui-control.js: -------------------------------------------------------------------------------- 1 | /** 2 | * messaging to gui 3 | */ 4 | 5 | const settings = require("@sidekick/common/settings"); 6 | const os = require("@sidekick/common/os"); 7 | 8 | const spawner = require("./safeSpawn"); 9 | const spawnApp = process.env.DEVELOPMENT ? devSpawn : realSpawn; 10 | const Promise = require("bluebird"); 11 | const fs = Promise.promisifyAll(require("fs")); 12 | 13 | const GUI_TIMEOUT_MILLI = 2500; 14 | 15 | const log = require("debug")("guiControl"); 16 | 17 | const _ = require("lodash"); 18 | 19 | exports.launch = launch; 20 | 21 | function launch() { 22 | return spawnApp() 23 | .then(function(child) { 24 | // we want to exit without gui exiting 25 | child.unref(); 26 | }); 27 | } 28 | 29 | 30 | function devSpawn() { 31 | log('dev spawn of ui'); 32 | 33 | const skGuiPath = __dirname + "/../../../sk-gui/build"; 34 | 35 | return spawnWith('/usr/bin/env', [ 36 | "npm", "run", "appDev" 37 | ], { 38 | cwd: skGuiPath, 39 | }) 40 | .catch((e) => e.code === "ENOENT", function(e) { 41 | const msg = `can't find sk-gui repo for dev launch at '${skGuiPath}': ${e.message}`; 42 | log(msg); 43 | throw Error(msg); 44 | }); 45 | } 46 | 47 | function realSpawn() { 48 | const path = process.MAIN_PROCESS_PATH || "/Applications/Sidekick.app/Contents/MacOS/Electron"; 49 | return spawnWith(path, [settings.port()]); 50 | } 51 | 52 | function spawnWith(cmd, args, opts) { 53 | var strippedEnv = _.omit(process.env, 'ELECTRON_RUN_AS_NODE'); 54 | 55 | return spawner(cmd, args, _.defaults(opts || {}, { 56 | // pass through our env - important so it can resolve node etc (in dev) 57 | env: _.defaults({ 58 | NO_AUTO_SHOW_GUI: true, 59 | }, strippedEnv), 60 | }), { 61 | timeout: GUI_TIMEOUT_MILLI, 62 | onCreate: function(child) { 63 | if(process.env.DEBUG) { 64 | child.stdout.on("data", function(data) { 65 | log("child stdout:" + data); 66 | }); 67 | 68 | child.stderr.on("data", function(data) { 69 | log("child stdout:" + data); 70 | }); 71 | } 72 | }, 73 | }); 74 | } 75 | 76 | -------------------------------------------------------------------------------- /src/flow.js: -------------------------------------------------------------------------------- 1 | /** 2 | * helpers for CLI processes 3 | */ 4 | "use strict"; 5 | 6 | const Promise = require("bluebird"); 7 | const RepoConfig = require("@sidekick/common/repoConfig"); 8 | 9 | const debug = require("debug")("cli:flow"); 10 | 11 | exports.Exit = Exit; 12 | 13 | // runs a sequence of functions that will return a Exit instance 14 | // if they desire and exit with a certain code 15 | exports.runValidations = function runValidations(vals, log) { 16 | 17 | return step(); 18 | 19 | function step() { 20 | return Promise.try(function() { 21 | if(vals.length === 0) { 22 | return Promise.resolve(true); 23 | } else { 24 | var validation = vals.shift(); 25 | debug("validation: " + validation.name); 26 | 27 | return validation() 28 | .then(function(exit) { 29 | return exit instanceof Exit ? exit : step(); 30 | }); 31 | } 32 | }); 33 | } 34 | }; 35 | 36 | exports.hasConfigValidation = function hasConfig(path) { 37 | return RepoConfig.load(path) 38 | .catch(SyntaxError, function(err) { 39 | return new Exit(1, `could not parse ".sidekickrc" as JSON, "${err.message}"`); 40 | }) 41 | .catch(function(err) { 42 | debug("failed: " + err.message); 43 | const msg = missingConfigErrorMessages(err); 44 | return new Exit(1, msg); 45 | }); 46 | 47 | function missingConfigErrorMessages(err) { 48 | switch(err.code) { 49 | case "ENOENT": 50 | return `there is no ".sidekickrc" file in the root of this repo. run "sk init" to create one, or "sk remove" to remove the hook`; 51 | case "EACCES": 52 | case "EPERM": 53 | return `had issues opening ".sidekickrc" (file permissions?)`; 54 | default: 55 | return `had an unexpected issue when opening ".sidekickrc" (${err.code || err.message}). run "sk init" to create a ".sidekickrc" file`; 56 | } 57 | } 58 | }; 59 | 60 | // value object 61 | function Exit(code, message) { 62 | this.code = code; 63 | this.message = message; 64 | this.debug = new Error().stack; 65 | 66 | this.isBad = function() { 67 | return code !== 0; 68 | }; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/reporters/junit.js: -------------------------------------------------------------------------------- 1 | /** 2 | * junit XML output 3 | * 4 | * ## References 5 | * 6 | * - http://llg.cubic.org/docs/junit/ 7 | * - http://nose2.readthedocs.org/en/latest/plugins/junitxml.html 8 | * - https://svn.jenkins-ci.org/trunk/hudson/dtkit/dtkit-format/dtkit-junit-model/src/main/resources/com/thalesgroup/dtkit/junit/model/xsd/junit-4.xsd?p=41398 - p = revision number 9 | * 10 | * 11 | */ 12 | "use strict"; 13 | 14 | module.exports = exports = reporter; 15 | 16 | const defaultOutput = (x) => console.log(x); 17 | 18 | function reporter(emitter, outputter) { 19 | const output = outputter || defaultOutput; 20 | 21 | output(` 22 | `); 23 | 24 | emitter.on("result", function(result) { 25 | outputTestcase(result, failures()); 26 | 27 | function failures() { 28 | return result.issues.map((issue) => { 29 | return `${cdata(issue.message)}` 30 | }).join("\n"); 31 | } 32 | }); 33 | 34 | emitter.on("error", function(result) { 35 | const err = result.error; 36 | 37 | outputTestcase(result, error() + stdio()); 38 | 39 | function error() { 40 | return ``; 41 | } 42 | 43 | function stdio() { 44 | if(!("stderr" in err)) { 45 | return ""; 46 | } 47 | return `${cdata(err.stdout)}> 48 | ${cdata(err.stderr)}`; 49 | } 50 | }); 51 | 52 | emitter.on("end", function() { 53 | output(``); 54 | }); 55 | 56 | function pathToClassName(path) { 57 | return path.replace("/", ".").replace(/\s+/g,"_"); 58 | } 59 | 60 | function outputTestcase(result, content) { 61 | output(` 62 | ${content} 63 | `); 64 | } 65 | 66 | function cdata(x) { 67 | return `` 68 | } 69 | 70 | function escapeAttr(s) { 71 | var pairs = { 72 | "&": "&", 73 | '"': """, 74 | "'": "'", 75 | "<": "<", 76 | ">": ">" 77 | }; 78 | for (var r in pairs) { 79 | if (typeof(s) !== "undefined") { 80 | s = s.replace(new RegExp(r, "g"), pairs[r]); 81 | } 82 | } 83 | return s || ""; 84 | } 85 | 86 | } 87 | 88 | -------------------------------------------------------------------------------- /src/textUi.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _ = require("lodash"); 4 | 5 | exports.banner = banner; 6 | 7 | // takes a list of paras and options. provide empty strings for spacer paras 8 | // 9 | // opts: 10 | // width: number of characters for each line width 11 | function banner(paragraphs, opts) { 12 | opts = _.defaults(opts || {}, { 13 | width: 80, 14 | }); 15 | 16 | var borderWidth = 1; 17 | var paddingWidth = 2; 18 | 19 | var spaceForText = opts.width - (paddingWidth * 2) - (borderWidth * 2); 20 | 21 | // add top spacer row 22 | paragraphs = [""].concat(paragraphs); 23 | 24 | var lines = _.transform(paragraphs, function(lines, para) { 25 | if(spacerParagraph(para)) { 26 | var rawLines = [""]; 27 | } else { 28 | var rawLines = lineBreaks(spaceForText, para); 29 | rawLines.push(""); 30 | } 31 | 32 | var formatted = _.map(rawLines, function(line) { 33 | return `* ${padAlign(line, spaceForText)} *`; 34 | }); 35 | [].push.apply(lines, formatted); 36 | }, []); 37 | 38 | var verticalBorder = "*".repeat(opts.width); 39 | return verticalBorder + "\n" + lines.join("\n") + "\n" + verticalBorder; 40 | 41 | function spacerParagraph(para) { 42 | return para === ""; 43 | } 44 | } 45 | 46 | 47 | 48 | function lineBreaks(availableWidth, text) { 49 | if(text.length <= availableWidth) { 50 | return [text]; 51 | } 52 | 53 | var words = text.split(" "); 54 | var lines = []; 55 | 56 | var line; 57 | var currentLineLength; 58 | initializeLine(); 59 | 60 | _.each(words, function(w) { 61 | if(lineLength(w) > availableWidth) { 62 | initializeLine(); 63 | } 64 | line.push(w); 65 | currentLineLength += w.length; 66 | }); 67 | 68 | return _.invoke(lines, "join", " "); 69 | 70 | function lineLength(nextWord) { 71 | // spaces including that required for next word (words - 1) 72 | var spaces = line.length; 73 | return spaces + nextWord.length + currentLineLength; 74 | } 75 | 76 | function initializeLine() { 77 | currentLineLength = 0; 78 | line = []; 79 | lines.push(line); 80 | } 81 | } 82 | 83 | function padAlign(str, w) { 84 | if(str.length > w) { 85 | throw new Error("can't pad string over available width"); 86 | } 87 | 88 | var paddingRight = (w - str.length) / 2; 89 | var paddingLeft; 90 | 91 | if(paddingRight.toFixed(0) !== paddingRight.toString()) { 92 | paddingRight = Math.floor(paddingRight); 93 | paddingLeft = paddingRight + 1; 94 | } else { 95 | paddingLeft = paddingRight; 96 | } 97 | 98 | return " ".repeat(paddingLeft) + str + " ".repeat(paddingRight); 99 | } 100 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sidekick 2 | 3 | [![Build Status](https://travis-ci.org/sidekickcode/sidekick.svg?branch=master)](https://travis-ci.org/sidekickcode/sidekick) 4 | 5 | Sidekick - your code, made perfect. 6 | 7 | ## Pre-requisites 8 | 9 | We need `git` to be installed on your machine. 10 | 11 | 12 | ## Installation 13 | 14 | ```sh 15 | npm i sidekick -g 16 | ``` 17 | 18 | ## Initialisation 19 | 20 | Sidekick does not ship with any analysers, so you will need to install them. Its really easy to do: 21 | 22 | ```sh 23 | sidekick analysers --install 24 | ``` 25 | 26 | You can check that your system is configured correctly: 27 | 28 | ```sh 29 | sidekick config 30 | ``` 31 | 32 | If `git` is not available on your path, then you will need to tell us where it has been installed to: 33 | 34 | ```sh 35 | sidekick config --git=/some/path/to/git 36 | ``` 37 | 38 | 39 | ## Usage 40 | 41 | ###You can run Sidekick on your CI server: 42 | 43 | ```sh 44 | sidekick run --ci 45 | ``` 46 | 47 | This will install all the analysers that are needed, run them against your code and optionally fail the build. 48 | 49 | #### Travis integration 50 | 51 | ```sh 52 | sidekick run --travis 53 | ``` 54 | 55 | Will analyse just the changes that prompted the travis build. This is great for analysing Pull Requests with 56 | just 2 lines of config! 57 | 58 | ###You can run Sidekick against code on your machine: 59 | 60 | ```sh 61 | cd your/repo 62 | sidekick run 63 | ``` 64 | 65 | or 66 | 67 | ```sh 68 | sidekick run path/to/your/repo 69 | ``` 70 | 71 | This will evaluate the working copy of the repo's code on your machine. 72 | 73 | You can use `--compare` and `--versus` cli arguments to compare your working copy with other local or remote branches. 74 | 75 | ###You can configure how sidekick analyses your files 76 | 77 | By default, we look at the contents of your repo and run analysers that we think will be useful, e.g. if we find 78 | JavaScript files, we will run a JavaScript TODO/FIXME finder, if we find a `package.json` file, we will run our 79 | `david-dm` analyser on your dependencies.. 80 | 81 | You can add a `.sidekickrc` file to your repo to tell us what analysers you would like to run, and which ones can 82 | fail the build. To create a default `.sidekickrc` file: 83 | 84 | ```sh 85 | sidekick init 86 | ``` 87 | 88 | ## Git push integration and GUI 89 | 90 | Sidekick also has a git pre-push hook and a GUI that helps you fix your issues before they are pushed to a remote repo. 91 | 92 | This GUI is in beta at the moment. 93 | 94 | If you want to get support then we have a [chat room](https://gitter.im/sidekickcode/support). 95 | If you want to raise issues then you can do so [here](https://github.com/sidekickcode/tracker/issues). 96 | 97 | Thanks for trying Sidekick. 98 | -------------------------------------------------------------------------------- /src/prepush-ui-tty.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const createUi = require("./prepush-ui-tty")._runUi; 4 | const events = require("events"); 5 | const sinon = require("sinon"); 6 | 7 | const readline = require("readline"); 8 | const stream = require("stream"); 9 | const util = require("util"); 10 | 11 | 12 | describe('prepush-ui-tty', function() { 13 | 14 | var setup 15 | var push; 16 | var main; 17 | var cursor; 18 | var ui 19 | var pushActor; 20 | var prepush; 21 | 22 | beforeEach(function() { 23 | 24 | cursor = readline.createInterface({ input: new StubReadable, output: new StubWritable}); 25 | main = new events.EventEmitter; 26 | pushActor = new events.EventEmitter; 27 | prepush = new events.EventEmitter; 28 | main.getActor = function(id) { 29 | assert.equal(id, push.id); 30 | return pushActor; 31 | } 32 | 33 | push = { 34 | id: 1, 35 | }; 36 | 37 | setup = { 38 | main, 39 | push, 40 | prepush, 41 | cursor, 42 | }; 43 | 44 | 45 | ui = createUi(setup); 46 | 47 | }); 48 | 49 | describe('ending', function() { 50 | it('exits 0 on success', function() { 51 | var exitSpy = sinon.spy(); 52 | ui.on("exit", exitSpy); 53 | pushActor.emit("end"); 54 | 55 | assert.spyCalledOnce(exitSpy); 56 | }) 57 | 58 | it('requests confirmation to skip on error', function() { 59 | var writer = sinon.spy(cursor, "question"); 60 | pushActor.emit("end", new Error); 61 | 62 | assert.spyCalledOnce(writer); 63 | assert.spyCalledWith(writer, sinon.match(/push anyway?/)); 64 | }) 65 | }) 66 | 67 | // can't get the stdout stream to capture output 68 | describe('exitOptionallySkipping', function() { 69 | 70 | it('outputs exit message', function() { 71 | prepush.emit('exitOptionallySkipping', { 72 | message: "oh NOES", 73 | }) 74 | 75 | assert.match(cursor.output.content, /oh NOES/); 76 | }) 77 | 78 | it('prompts for skip', function() { 79 | prepush.emit('exitOptionallySkipping', { 80 | message: "oh NOES", 81 | }); 82 | 83 | assert.match(cursor.output.content, /without analysis?/); 84 | }) 85 | 86 | }) 87 | 88 | }) 89 | 90 | 91 | function StubReadable() { 92 | this._read = function() {}; 93 | this.isTTY = true; 94 | stream.Readable.call(this); 95 | } 96 | util.inherits(StubReadable, stream.Readable); 97 | 98 | function StubWritable() { 99 | this.content = ""; 100 | // being a bit naughty, and overwriting the public write message 101 | // to keep it sync 102 | this.write = function(d) { 103 | this.content += d; 104 | }; 105 | this.isTTY = true; 106 | stream.Writable.call(this); 107 | } 108 | util.inherits(StubWritable, stream.Writable); 109 | -------------------------------------------------------------------------------- /src/safeSpawn.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var spawn = require("./safeSpawn"); 4 | 5 | describe('safeSpawn', function() { 6 | 7 | this.timeout(150); 8 | 9 | it('is resolved with child process when process outputs up message', function(done) { 10 | spawnFixture({ OUTPUT_UP: 1 }) 11 | .nodeify(function(err, child) { 12 | if(err) { 13 | return done(err); 14 | } 15 | assert.isFunction(child.unref); 16 | done(); 17 | }) 18 | }); 19 | 20 | it('is resolved with child process when process outputs up message with garbage around it', function(done) { 21 | spawnFixture({ OUTPUT_UP_WITH_GARBAGE: 1 }) 22 | .nodeify(function(err, child) { 23 | if(err) { 24 | return done(err); 25 | } 26 | assert.isFunction(child.unref); 27 | done(); 28 | }) 29 | }); 30 | 31 | it('is rejected if process exits malformed data', function(done) { 32 | spawnFixture({ OUTPUT_GARBAGE: 1 }) 33 | .nodeify(function(err) { 34 | done(err ? null : new Error("should have been rejected")); 35 | }) 36 | }); 37 | 38 | it('is rejected if process exits fail before data', function(done) { 39 | spawnFixture({ EXIT_WITH: 1 }) 40 | .nodeify(function(err) { 41 | done(err ? null : new Error("should have been rejected")); 42 | }) 43 | }); 44 | 45 | it('is rejected if process exits with explit error', function(done) { 46 | spawnFixture({ EXPLICIT_FAILURE: 1 }) 47 | .nodeify(function(err) { 48 | if(err) { 49 | assert.equal(err.reason, "failed, sorry", "should have picked up explicit failure"); 50 | done(); 51 | } else { 52 | done(new Error("should have been rejected")); 53 | } 54 | }) 55 | }); 56 | 57 | it('is rejected if process exits successfully before data', function(done) { 58 | spawnFixture({ EXIT_WITH: 0 }) 59 | .nodeify(function(err) { 60 | done(err ? null : new Error("should have been rejected")); 61 | }) 62 | }); 63 | 64 | 65 | it('is rejected with error if process cannot be started', function(done) { 66 | spawn("/usr/bin/notTHEREREREROAKSDOSKD", [], {}, { timeout: 75 }) 67 | .nodeify(function(err) { 68 | if(err) { 69 | assert.notEqual(err.message, "Timeout"); 70 | done(); 71 | } else { 72 | done(new Error("should have been rejected")); 73 | } 74 | }) 75 | }); 76 | 77 | it("is rejected with timeout if process doesn't send up", function(done) { 78 | spawnFixture({ DO_NOTHING: 1 }) 79 | .nodeify(function(err) { 80 | if(err) { 81 | assert.equal(err.message, "Timeout"); 82 | done(); 83 | } else { 84 | done(new Error("should have been rejected")); 85 | } 86 | }) 87 | }); 88 | 89 | function spawnFixture(env) { 90 | return spawn(process.execPath, [__dirname + "/../test/fixtures/safeSpawnFixture.js"], { 91 | env: env, 92 | }, { 93 | timeout: 130, 94 | }); 95 | } 96 | }) 97 | -------------------------------------------------------------------------------- /src/safeSpawn.js: -------------------------------------------------------------------------------- 1 | /** 2 | * child_process.spawn with extra assurances the process started correctly 3 | * 4 | * we give assurances via simple contract the other process follows to let us know when it's ready: 5 | * 6 | * if up and everything is ok, output this line 7 | * 8 | * { up: true }\n 9 | * 10 | * if failing for a known reason, output the following line 11 | * 12 | * { error: "error string }\n 13 | * 14 | * this is designed for use with processes that are expected to be up for a while for later communication 15 | */ 16 | "use strict"; 17 | 18 | const spawn = require("child_process").spawn; 19 | const _ = require("lodash"); 20 | var Promise = require("bluebird"); 21 | 22 | module.exports = exports = Promise.method(spawner); 23 | 24 | function spawner(cmd, args, spawnOpts, opts) { 25 | if(!opts || isNaN(opts.timeout)) { 26 | throw new Error("requires opts.timeout in milliseconds"); 27 | } 28 | 29 | var spawnOptions = _.defaults(spawnOpts || {}, { 30 | detached: true, 31 | }); 32 | 33 | var spawned = new Promise(function(resolve, reject) { 34 | var child = spawn(cmd, args, spawnOptions); 35 | 36 | if(opts.onCreate) { 37 | opts.onCreate(child); 38 | } 39 | 40 | child.once("exit", function(code, signal) { 41 | var error = new Error("ChildExit"); 42 | error.code = code; 43 | error.signal = signal; 44 | fail(error); 45 | }); 46 | 47 | child.once("error", fail); 48 | 49 | var buf = ""; 50 | child.stdout.on("data", listenForUp); 51 | 52 | setTimeout(function() { 53 | if(!spawned.isResolved()) { 54 | fail(new Error("Timeout")); 55 | } 56 | }, opts.timeout); 57 | 58 | return; 59 | 60 | 61 | function listenForUp(data) { 62 | buf += data; 63 | 64 | var nextLineIndex; 65 | while(nextLineIndex = buf.indexOf("\n"), nextLineIndex !== -1) { 66 | var nextLine = buf.slice(0, nextLineIndex); 67 | var b4 = buf; 68 | buf = buf.slice(nextLineIndex + 1); 69 | 70 | try { 71 | var parsed = JSON.parse(nextLine); 72 | } catch(e) { 73 | // not our line 74 | continue; 75 | } 76 | 77 | if(parsed.up) { 78 | resolve(child); 79 | return; 80 | } else if(parsed.error) { 81 | var error = new Error("ExplicitFailure"); 82 | error.reason = parsed.error; 83 | fail(error); 84 | 85 | return; 86 | } else { 87 | // not the message we're looking for 88 | } 89 | } 90 | } 91 | 92 | function fail(err) { 93 | cleanup(); 94 | reject(err); 95 | } 96 | 97 | function cleanup() { 98 | child.stdout.removeAllListeners(); 99 | child.removeAllListeners(); 100 | 101 | // we want to exit even if child never does 102 | child.unref(); 103 | 104 | // tell it to give up 105 | child.kill(); 106 | 107 | child = null; 108 | } 109 | 110 | }); 111 | 112 | return spawned; 113 | } 114 | -------------------------------------------------------------------------------- /src/reporters/junitTest.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const reporter = require("./junit"); 4 | const scenario = require("./scenario.test.js"); 5 | const et = require("elementtree"); 6 | const _ = require("lodash"); 7 | 8 | describe('junit reporter', function() { 9 | 10 | const self = this; 11 | 12 | before(function(done) { 13 | self.scenario = scenario.create([ 14 | { 15 | name: "result", 16 | path: "blub.js", 17 | data: { 18 | analyser: "blubCheck", 19 | path: "blub.js", 20 | issues: [ 21 | { message: " your xml!", 32 | stdout: " e.text.indexOf(' /not.+a.+good/.test(f.attrib.name)); 105 | assert(difficultName, "couldn't find failure node for 'horrible name' result"); 106 | 107 | assert.equal(difficultName.attrib.name, "horrible_fileName_--_not_a_good_idea.js.blubHint"); 108 | 109 | }); 110 | 111 | 112 | }) 113 | -------------------------------------------------------------------------------- /src/MainProcess.js: -------------------------------------------------------------------------------- 1 | /** 2 | * connect to main processes's RPC 3 | */ 4 | "use strict"; 5 | 6 | var net = require("net"); 7 | var rpc = require("rpcjs"); 8 | var actors = require("rpcjs/actors"); 9 | var settings = require("@sidekick/common/settings"); 10 | var gui = require("./gui-control"); 11 | var Promise = require("bluebird"); 12 | var _ = require("lodash"); 13 | var log = require("debug")("MainProxy"); 14 | var reconnect = require("./streamReconnect"); 15 | var streamTransport = require("rpcjs/transports/streamTransport"); 16 | 17 | 18 | // singleton 19 | var client; 20 | 21 | exports.get = function() { 22 | if(!client) { 23 | throw new Error("not initialised"); 24 | } 25 | return client; 26 | }; 27 | 28 | exports.acquire = function(opts) { 29 | if(client) { 30 | throw new Error("already initialised"); 31 | } 32 | 33 | client = rpc.client(_.defaults(opts || {}, { 34 | name: "cli2main", 35 | Promise: Promise, 36 | })); 37 | 38 | var bufferedTransport = new BufferedTransport; 39 | 40 | client.setSend(bufferedTransport.send); 41 | 42 | actors.mixin(client); 43 | 44 | var reconnector = reconnect(attemptConnection, { 45 | maxAttempts: 2, 46 | error: function(err) { 47 | // we're done 48 | throw err; 49 | }, 50 | }); 51 | 52 | return client; 53 | 54 | function attemptConnection(err, reconnectCount, cb) { 55 | log("attempting to connect to main, retries %d, err '%s'", reconnectCount, err && err.message); 56 | if(reconnectCount === 0) { 57 | // initial attempt 58 | connect(); 59 | } else if(err) { 60 | // other end isn't up yet 61 | if(err.code === "ECONNREFUSED") { 62 | log("booting main"); 63 | launchGui(); 64 | } else { 65 | // we're done 66 | throw err; 67 | } 68 | } else { 69 | // we have 2 attempts - initial, which might succeed, then second where we have 70 | // an error. if we are not on first attempt, or reconnecting after failure, something is up! 71 | throw new Error("AssertionError"); 72 | } 73 | 74 | function launchGui() { 75 | gui.launch() 76 | .then(function() { 77 | log("heard gui up"); 78 | connect(); 79 | reconnector.stop(); 80 | }) 81 | .catch(function(err) { 82 | log("spawn fail", err); 83 | throw new Error("CouldNotSpawnMain"); 84 | }); 85 | } 86 | 87 | function connect() { 88 | log("connecting"); 89 | 90 | var stream = net.connect({ 91 | port: settings.port(), 92 | }, function() { 93 | log("connected"); 94 | 95 | var disconnect = streamTransport.incoming(client, stream); 96 | bufferedTransport.setSend(_.partial(streamTransport.send, stream)); 97 | 98 | stream.once("end", function() { 99 | log("cli2main stream ended"); 100 | }); 101 | 102 | stream.on("error", logErr); 103 | }); 104 | 105 | cb(stream); 106 | 107 | function logErr(err) { 108 | log("cli2main stream error", err); 109 | } 110 | } 111 | } 112 | }; 113 | 114 | function BufferedTransport() { 115 | var buf = []; 116 | var realSend; 117 | 118 | this.send = function(x) { 119 | if(realSend) { 120 | realSend(x); 121 | } else { 122 | buf.push(x); 123 | } 124 | }; 125 | 126 | this.setSend = function(to) { 127 | realSend = to; 128 | 129 | send(); 130 | 131 | function send() { 132 | if(buf.length > 0) { 133 | process.nextTick(function() { 134 | var msg = buf.shift(); 135 | realSend(msg); 136 | send(); 137 | }); 138 | } else { 139 | buf = null; 140 | } 141 | } 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /src/init.js: -------------------------------------------------------------------------------- 1 | /** 2 | * creates a default .sidekickrc file for a repo. 3 | */ 4 | "use strict"; 5 | const path = require('path'); 6 | 7 | const log = require("debug")("cli:init"); 8 | 9 | const _ = require("lodash"); 10 | 11 | const EventEmitter = require("events").EventEmitter; 12 | 13 | const fs = require('fs-extra'); 14 | 15 | const yargs = require("yargs"); 16 | 17 | const git = require('@sidekick/git-helpers'); 18 | const repoConfig = require('@sidekick/common/repoConfig'); 19 | 20 | const reporters = Object.create(null); 21 | reporters["cli-summary"] = require("./reporters/cliSummary"); 22 | reporters["json-stream"] = require("./reporters/jsonStream"); 23 | reporters["junit"] = require("./reporters/junit"); 24 | 25 | module.exports = exports = function() { 26 | const command = parseInput(); 27 | log("command: %j", command); 28 | 29 | const events = new EventEmitter; 30 | 31 | const reporter = getReporter(command.reporter); 32 | 33 | reporter(events, null, command); 34 | 35 | const CONFIG_FILE = '.sidekickrc'; 36 | 37 | 38 | git.findRootGitRepo(process.cwd(), function(err, locationOfRepoRoot){ 39 | if(err){ 40 | log(`Unable to run init: ${err.message}`); 41 | const errMessage = `Unable to create .sidekickrc. Cannot find repo root, starting in: ${process.cwd()}`; 42 | doExit(1, errMessage, err); 43 | } 44 | 45 | const configFileAbsPath = path.join(locationOfRepoRoot, CONFIG_FILE); 46 | 47 | try { 48 | fs.statSync(configFileAbsPath); 49 | events.emit('message', `${CONFIG_FILE} already exists for this repo at: ${locationOfRepoRoot}`) 50 | } catch(e) { 51 | events.emit('message', `Creating default ${CONFIG_FILE} file..`); 52 | repoConfig.load(locationOfRepoRoot) 53 | .then((RC) => { 54 | const defaultConfig = RC.getContents(); 55 | log(`Default config: ${defaultConfig}`); 56 | fs.writeFileSync(configFileAbsPath, defaultConfig); 57 | events.emit('message', `Created .sidekickrc file in ${locationOfRepoRoot}`); 58 | }) 59 | } 60 | }); 61 | }; 62 | 63 | function parseInput() /*: { install?: boolean } */ { 64 | const argv = yargs.argv; 65 | 66 | const cmd = { 67 | reporter: argv.reporter, 68 | }; 69 | return cmd; 70 | } 71 | 72 | 73 | function getReporter(name) { 74 | // default to summary report 75 | if(!name) { 76 | return reporters["cli-summary"]; 77 | } 78 | 79 | const reporter = reporters[name]; 80 | if(reporter) { 81 | return reporter; 82 | } 83 | 84 | try { 85 | return require(name); 86 | } catch(e) { 87 | console.error(`couldn't load reporter '${name}': ${e.stack}`); 88 | doExit(1); 89 | } 90 | } 91 | 92 | function outputError(e) { 93 | console.error(e); 94 | } 95 | 96 | function errorWithCode(code) { 97 | return function(err) { 98 | return err.code === code; 99 | }; 100 | } 101 | 102 | function doExit(code, message, error) { 103 | log(`exiting with code '${code}' with msg '${message}' ` + (error ? error.stack : "")); 104 | if(message) { 105 | outputError(message); 106 | } 107 | 108 | process.exit(code); 109 | } 110 | 111 | function fail(err) { 112 | log("UNEXPECTED FAILURE " + (err ? (err.stack || err) : " without error passed")); 113 | doExit(1, "sidekick suffered an unexpected failure", err); 114 | } 115 | 116 | exports.help = ` 117 | usage: sidekick init 118 | 119 | Creates a .sidekickrc file in the git root of the current repo. 120 | 121 | If sidekick gui is installed, will open the gui to help configure the current repo. 122 | Otherwise, it will create a .sidekickrc file for the current repo. 123 | File is created after parsing the current repo to see what analysers could be helpful - use a text editor to change. 124 | 125 | `; 126 | -------------------------------------------------------------------------------- /src/analysers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * outputs information on the installed analysers in CLI mode. 3 | */ 4 | "use strict"; 5 | const path = require('path'); 6 | 7 | const log = require("debug")("cli:analysers"); 8 | 9 | const _ = require("lodash"); 10 | const Promise = require('bluebird'); 11 | 12 | const EventEmitter = require("events").EventEmitter; 13 | 14 | const analyserManager = require("@sidekick/analyser-manager"); 15 | const os = require("@sidekick/common/os"); 16 | 17 | const yargs = require("yargs"); 18 | 19 | const proxy = require("proxytron"); 20 | 21 | const reporters = Object.create(null); 22 | reporters["cli-summary"] = require("./reporters/cliSummary"); 23 | reporters["json-stream"] = require("./reporters/jsonStream"); 24 | reporters["junit"] = require("./reporters/junit"); 25 | 26 | module.exports = exports = function() { 27 | const command = parseInput(); 28 | log("command: %j", command); 29 | 30 | const events = new EventEmitter; 31 | 32 | const reporter = getReporter(command.reporter); 33 | 34 | reporter(events, null, command); 35 | 36 | const INSTALL_LOCATION = path.join(os.userDataPath(), '/installed_analysers'); 37 | const AM = new analyserManager(INSTALL_LOCATION); 38 | 39 | if(command.installAnalysers){ 40 | events.emit('message', 'Fetching list of analysers to install..'); 41 | 42 | proxy({ 43 | from: AM, 44 | to: events, 45 | events: { 46 | downloading: null, 47 | downloaded: null, 48 | installing: null, 49 | installed: null, 50 | } 51 | }); 52 | 53 | AM.init() 54 | .then(() => { 55 | log('analysers: ' + JSON.stringify(_.values(AM.ALL_ANALYSERS))); 56 | const analyserList = getAllAnalysers(_.values(AM.ALL_ANALYSERS)); 57 | log('analysers: ' + JSON.stringify(analyserList)); 58 | events.emit('message', `Found ${analyserList.length} analysers to install.\n`); 59 | Promise.all(_.map(analyserList, (analyser) => { 60 | log('installing analyser: ' + JSON.stringify(analyser)); 61 | return AM.installAnalyser(analyser, true); //force install of latest 62 | })) 63 | .then(() => { 64 | events.emit('message', '\nInstalled all analysers.'); 65 | }) 66 | }); 67 | } else { 68 | log('fetching analyser list'); 69 | AM.init() 70 | .then(() => { 71 | //return list of installed analysers 72 | const allInstalledAnalysers = AM.getAllInstalledAnalysers(); 73 | log('have installed analysers: ' + JSON.stringify(allInstalledAnalysers)); 74 | const analyserList = allInstalledAnalysers.join('\n '); 75 | events.emit('message', `\nWe found ${allInstalledAnalysers.length} installed analysers:\n\n ${analyserList}\n`); 76 | }); 77 | } 78 | 79 | function getAllAnalysers(analyserConfigs){ 80 | return _.map(analyserConfigs, function(analyserConfig){ 81 | log('analyserConfig: ' + JSON.stringify(analyserConfig)); 82 | return {name: analyserConfig.config.analyser, 83 | version: 'latest'} 84 | }) 85 | } 86 | 87 | }; 88 | 89 | function parseInput() /*: { install?: boolean } */ { 90 | const argv = yargs 91 | .boolean("install") 92 | .argv; 93 | 94 | const cmd = { 95 | installAnalysers : argv.install, 96 | reporter: argv.reporter, 97 | }; 98 | return cmd; 99 | } 100 | 101 | 102 | function getReporter(name) { 103 | // default to summary report 104 | if(!name) { 105 | return reporters["cli-summary"]; 106 | } 107 | 108 | const reporter = reporters[name]; 109 | if(reporter) { 110 | return reporter; 111 | } 112 | 113 | try { 114 | return require(name); 115 | } catch(e) { 116 | console.error(`couldn't load reporter '${name}': ${e.stack}`); 117 | doExit(1); 118 | } 119 | } 120 | 121 | function outputError(e) { 122 | console.error(e); 123 | } 124 | 125 | function errorWithCode(code) { 126 | return function(err) { 127 | return err.code === code; 128 | }; 129 | } 130 | 131 | function doExit(code, message, error) { 132 | log(`exiting with code '${code}' with msg '${message}' ` + (error ? error.stack : "")); 133 | if(message) { 134 | outputError(message); 135 | } 136 | 137 | process.exit(code); 138 | } 139 | 140 | function fail(err) { 141 | log("UNEXPECTED FAILURE " + (err ? (err.stack || err) : " without error passed")); 142 | doExit(1, "sidekick suffered an unexpected failure", err); 143 | } 144 | 145 | exports.help = ` 146 | usage: sidekick analysers [ --install ] 147 | 148 | Returns information about the installed analysers. 149 | 150 | Installation 151 | 152 | With the --install flag, all available analysers will be installed (latest versions). 153 | `; 154 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * outputs status info. 3 | */ 4 | "use strict"; 5 | const path = require('path'); 6 | 7 | const log = require("debug")("cli:config"); 8 | 9 | const _ = require("lodash"); 10 | 11 | const EventEmitter = require("events").EventEmitter; 12 | 13 | const analyserManager = require("@sidekick/analyser-manager"); 14 | const os = require("@sidekick/common/os"); 15 | const userSettings = require("@sidekick/common/userSettings"); 16 | 17 | const yargs = require("yargs"); 18 | 19 | const reporters = Object.create(null); 20 | reporters["cli-summary"] = require("./reporters/cliSummary"); 21 | reporters["json-stream"] = require("./reporters/jsonStream"); 22 | reporters["junit"] = require("./reporters/junit"); 23 | 24 | module.exports = exports = function() { 25 | const command = parseInput(); 26 | log("command: %j", command); 27 | 28 | const events = new EventEmitter; 29 | 30 | const reporter = getReporter(command.reporter); 31 | 32 | reporter(events, null, command); 33 | 34 | const INSTALL_LOCATION = path.join(os.userDataPath(), '/installed_analysers'); 35 | const AM = new analyserManager(INSTALL_LOCATION); 36 | 37 | if(command.git){ 38 | events.emit('message', `Setting git path to: ${command.git}`); 39 | userSettings.setProperty('gitBin', command.git); 40 | userSettings.save(); 41 | } else { 42 | //return config info 43 | AM.init() 44 | .then(() => { 45 | userSettings.load(); 46 | const installedAnalysers = AM.getAllInstalledAnalysers(); 47 | var output = `\nSidekick config settings:\n`; 48 | events.emit('message', output); 49 | 50 | output = ` Sidekick analysers installed: ${installedAnalysers.length > 0 ? 'OK' : 'NO (run \'sidekick analysers --install\' to install).'}`; 51 | events.emit('message', output, installedAnalysers.length > 0 ? 'green' : 'yellow'); 52 | 53 | userSettings.isGitReachable() 54 | .then(function(){ 55 | log('git found OK: '); 56 | output = ` Sidekick can find git: OK\n`; 57 | events.emit('message', output, 'green'); 58 | }, function(err){ 59 | log(`Cannot find git at: ${userSettings.getGitBin()}.`, err); 60 | output = ` Sidekick can find git: NO (cannot find git at: ${userSettings.getGitBin()}).\n`; 61 | events.emit('message', output, 'yellow'); 62 | }); 63 | }); 64 | } 65 | }; 66 | 67 | function parseInput() /*: { git?: string } */ { 68 | const argv = yargs.argv; 69 | 70 | const cmd = { 71 | reporter: argv.reporter, 72 | }; 73 | 74 | if(argv.git) { 75 | cmd.git = argv.git; 76 | } 77 | 78 | return cmd; 79 | } 80 | 81 | 82 | function getReporter(name) { 83 | // default to summary report 84 | if(!name) { 85 | return reporters["cli-summary"]; 86 | } 87 | 88 | const reporter = reporters[name]; 89 | if(reporter) { 90 | return reporter; 91 | } 92 | 93 | try { 94 | return require(name); 95 | } catch(e) { 96 | console.error(`couldn't load reporter '${name}': ${e.stack}`); 97 | doExit(1); 98 | } 99 | } 100 | 101 | function outputError(e) { 102 | console.error(e); 103 | } 104 | 105 | function errorWithCode(code) { 106 | return function(err) { 107 | return err.code === code; 108 | }; 109 | } 110 | 111 | function doExit(code, message, error) { 112 | log(`exiting with code '${code}' with msg '${message}' ` + (error ? error.stack : "")); 113 | if(message) { 114 | outputError(message); 115 | } 116 | 117 | process.exit(code); 118 | } 119 | 120 | function fail(err) { 121 | log("UNEXPECTED FAILURE " + (err ? (err.stack || err) : " without error passed")); 122 | doExit(1, "sidekick suffered an unexpected failure", err); 123 | } 124 | 125 | exports.help = ` 126 | usage: sidekick config [ --git ] 127 | 128 | Returns config information about Sidekick, e.g. 129 | 130 | Sidekick can find git: OK 131 | Sidekick analysers installed: OK 132 | 133 | We have 2 pre-requisites for correct operation: 134 | 1) You need to have git available. 135 | 2) You need to install our analysers. 136 | 137 | Git 138 | 139 | If git is already installed and can be reached in the terminal then you are good to go! 140 | Otherwise, you will need to tell Sidekick where git has been installed to: 141 | 142 | sidekick config --git=/some/path/to/git 143 | 144 | Analysers 145 | 146 | usage: sidekick analysers 147 | Please see the help for the 'analysers' command for more info. 148 | 149 | `; 150 | 151 | /* 152 | function isGitAvailable() { 153 | var property = userSettings.getProperty("gitBin"); 154 | return validGit(property || "git"); 155 | } 156 | 157 | function validGit(gitPath) { 158 | return ng.$q(function(resolve, reject) { 159 | childProcess.exec(gitPath + " --version", function(err, stdout) { 160 | if(err) { 161 | reject(err) 162 | } else { 163 | var isGit = /\bgit\b/.test(stdout); 164 | // if we're here, something is either git or pretending to be git :) 165 | isGit ? resolve() : reject(new Error("not git")); 166 | } 167 | }); 168 | }); 169 | }*/ 170 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const VERSION = require("../package.json").version; 4 | 5 | const log = require("debug")("cli"); 6 | 7 | const yargs = require("yargs"); 8 | const tracking = require("@sidekick/common/tracking"); 9 | 10 | exports.run = run; 11 | 12 | // safe lookup w/o proto 13 | const commands = Object.create(null); 14 | commands.version = showVersion; 15 | commands.help = helpCommand; 16 | commands.open = require("./open"); 17 | commands.prepush = require("./prepush"); 18 | commands["pre-push"] = commands.prepush; 19 | commands.run = require("./run"); 20 | commands.analysers = require("./analysers"); 21 | commands.config = require("./config"); 22 | commands.init = require('./init'); 23 | 24 | const help = 25 | ` 26 | Usage: sidekick [ arg, ... ] 27 | 28 | sidekick run [ some/repo/path ] [ --versus commitish ] [ --compare commitish (default: working copy) ] [ --reporter npmPackageName|absolutePath ] [ --ci ] [ --travis ] [ --no-ci-exit-code ] 29 | 30 | runs sidekick in cli mode, reporting results via reporter. will exit with status code 1 if any isues are detected 31 | - disable this with --no-ci-exit-code 32 | 33 | without a --versus, simply analyses all files in the repo. with --versus compares current working copy (i.e the files 34 | in the repo, commited or not) vs specified commit. With both --versus and --compare, will analyse changes 35 | that have happened since the commit at versus, until the commit at compare. 36 | 37 | sidekick run --versus origin/master # working copy vs latest fetched commit from origin/master 38 | sidekick run --versus head~5 # working copy vs 5 commits ago 39 | sidekick run --compare HEAD --versus head~5 # current commit vs 5 commits ago 40 | sk run --travis # travis integration - only analyses code that changed in PR etc (will set --ci if not set to false) 41 | 42 | CI 43 | 44 | With --ci flag, sidekick will install all analysers specified in your .sidekickrc file. If an analyser relies 45 | on a config file, e.g. '.eslintrc', these settings will be used. If no config can be found, then the analyser will 46 | not run. 47 | 48 | With --travis flag, sidekick will only analyse the files that changed in the commit that started the travis build. 49 | 50 | sidekick config [ --git ] 51 | 52 | outputs config information on the Sidekick installation 53 | 54 | with --git will set the path to your git installation 55 | 56 | sidekick analysers [ --install ] 57 | 58 | outputs information on the installed analysers 59 | 60 | with --install will install all available analysers 61 | 62 | sidekick init 63 | 64 | Creates a .sidekickrc file in the git root of the current repo. 65 | 66 | If sidekick gui is installed, will open the gui to help configure the current repo. 67 | Otherwise, it will create a .sidekickrc file for the current repo. 68 | File is created after parsing the current repo to see what analysers could be helpful - use a text editor to change. 69 | 70 | sidekick help [ command ] 71 | sidekick command -h --help 72 | 73 | shows this dialog, or more detailed help on a command if available 74 | 75 | sidekick version 76 | 77 | reports the version (also sidekick -v sidekick --version) 78 | 79 | You can chat to us about sidekick at: https://gitter.im/sidekickcode/support 80 | You can raise issues with sidekick at: https://github.com/sidekickcode/tracker/issues 81 | 82 | sidekick version ${VERSION} 83 | `; 84 | 85 | function run() { 86 | const cmd = yargs.argv._[0]; 87 | const fn = commands[cmd]; 88 | 89 | log(' *** CLI STARTUP *** '); 90 | 91 | tracking.start({ 92 | version: VERSION, 93 | }); 94 | 95 | process.on("exit", function(code) { 96 | log("exit " + code); 97 | }); 98 | 99 | process.on("uncaughtException", handleUnexpectedException); 100 | 101 | process.on("unhandledRejection", function(err) { 102 | log("unhandled promise rejection! " + err.stack || err); 103 | handleUnexpectedException(err); 104 | }); 105 | 106 | 107 | if(typeof fn === "function") { 108 | if(yargs.argv.h || yargs.argv.help) { 109 | return helpCommand(yargs); 110 | } 111 | 112 | try { 113 | fn(yargs); 114 | } catch(e) { 115 | handleUnexpectedException(e); 116 | } 117 | } else { 118 | if(yargs.argv.v || yargs.argv.version) { 119 | showVersion(); 120 | } else { 121 | failWithHelp(cmd); 122 | } 123 | } 124 | } 125 | 126 | function handleUnexpectedException(err) { 127 | log("uncaughtException! " + err.stack || err); 128 | tracking.error(err); 129 | process.stderr.write("sk suffered an unexpected error", () => { 130 | process.exit(1); 131 | }); 132 | } 133 | 134 | function showHelp() { 135 | write(help); 136 | } 137 | 138 | function helpCommand(yargs) { 139 | const argv = yargs.argv; 140 | const cmd = argv._[1]; 141 | const command = commands[cmd]; 142 | if(command) { 143 | if(command.help) { 144 | write(command.help); 145 | } else { 146 | write(`there is no additional help for the '${cmd}' command beyond the below\n`); 147 | failWithHelp(cmd); 148 | } 149 | } else { 150 | showHelp(); 151 | } 152 | } 153 | 154 | function showVersion() { 155 | write(`sidekick ${VERSION}`); 156 | } 157 | 158 | function failWithHelp(cmd) { 159 | if(cmd) { 160 | write(`'${cmd}' is not a sidekick command, see usage:\n`); 161 | } 162 | process.stdout.write(help, () => { 163 | process.exit(1); 164 | }); 165 | } 166 | 167 | if(require.main === module) { 168 | run(); 169 | } 170 | 171 | function write(m) { 172 | console.log(m); 173 | } 174 | -------------------------------------------------------------------------------- /src/prepush.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const yargs = require("yargs"); 4 | const uuid = require("uuid"); 5 | 6 | // TODO is the ttys module really needed? 7 | var ttys; 8 | if(process.stdout.isTTY) { 9 | ttys = require("ttys"); 10 | } else { 11 | ttys = process; 12 | } 13 | 14 | const readline = require("readline"); 15 | const stdin = require("easy-stdin"); 16 | const git = require("@sidekick/git-helpers"); 17 | const chalk = require("chalk"); 18 | const EventEmitter = require("events").EventEmitter; 19 | 20 | const flow = require("./flow"); 21 | const runValidations = flow.runValidations; 22 | const Exit = flow.Exit; 23 | 24 | const prepushUi = require("./prepush-ui"); 25 | 26 | const Promise = require("bluebird"); 27 | 28 | const _ = require("lodash"); 29 | 30 | const MainProcess = require("./MainProcess"); 31 | 32 | // long timeout, as we're probably booting UI 33 | const MAIN_TIMEOUT = 25000; 34 | 35 | const log = require("debug")("prepush"); 36 | 37 | module.exports = exports = function() { 38 | 39 | const events = new EventEmitter; 40 | var skipped = false; 41 | 42 | stdin(function(err, stdinContents) { 43 | if(err) { 44 | return fail(err); 45 | } 46 | 47 | const cwd = process.cwd(); 48 | 49 | runPrepush(cwd, stdinContents); 50 | }); 51 | 52 | return; 53 | 54 | 55 | function runPrepush(cwd, stdinContents) { 56 | git.findRootGitRepo(cwd) 57 | .error(git.NotAGitRepo, function() { 58 | doExit(1, "sidekick git hooks must be run in a git repo"); 59 | }) 60 | .then(function() { 61 | return git.prepush(yargs.argv._.slice(1), stdinContents, cwd) 62 | .then(validateAndPrepare) 63 | .then(function(exitOrPush) { 64 | if(exitOrPush instanceof Exit) { 65 | const exit = exitOrPush; 66 | 67 | if(exit.code === 0) { 68 | return doExit(0, exit.message); 69 | } 70 | 71 | exitOptionallySkipping(exit); 72 | } else { 73 | return prepush(exitOrPush); 74 | } 75 | }); 76 | }) 77 | .catch(fail) 78 | } 79 | 80 | function doExit(code, message, error) { 81 | log(`exiting with code '${code}' with msg '${message}' ` + (error ? error.stack : "")); 82 | if(message) { 83 | outputError(message + "\n"); 84 | } 85 | 86 | process.exit(code); 87 | } 88 | 89 | function errorWithCode(code) { 90 | return function(err) { 91 | return err.code === code; 92 | } 93 | } 94 | 95 | function validateAndPrepare(pushInfo) { 96 | if(pushInfo.actions.length === 0) { 97 | log("no actions in push"); 98 | return new Exit(0); 99 | } 100 | 101 | const targets = _.where(pushInfo.actions, actionSupportedByPushSystem); 102 | 103 | // nothing to do 104 | if(targets.length === 0) { 105 | log("no updates in push"); 106 | return new Exit(0); 107 | } 108 | 109 | // TODO remove when we support multiple pushes 110 | if(targets.length > 1) { 111 | return new Exit(1, "sidekick currently only supports pushing to one branch at a time."); 112 | } 113 | 114 | const target = targets[0]; 115 | 116 | log("about to run validations"); 117 | 118 | // TODO in future, don't enforce this but save work as we can't analyse 119 | return runValidations([isFastForwardOrCreate, _.partial(flow.hasConfigValidation, pushInfo.repoPath), isPushingCurrentBranch], log) 120 | .then(function(exit) { 121 | return exit instanceof Exit ? exit : pushInfo; 122 | }); 123 | 124 | function isFastForwardOrCreate() { 125 | if(target.type === git.CREATE_BRANCH) { 126 | return Promise.resolve(true); 127 | } 128 | 129 | return git.branchTipContainsAncestorAsync(pushInfo.repoPath, { ancestor: target.remoteSha, tip: target.localSha }) 130 | .then(function(yes) { 131 | if(!yes) { 132 | return new Exit(1, "sidekick only supports fast-forward pushes - please merge with remote first"); 133 | } 134 | }); 135 | } 136 | 137 | function isPushingCurrentBranch() { 138 | return git.getCurrentBranch(pushInfo.repoPath) 139 | .then(function(current) { 140 | if(current !== target.localBranch) { 141 | return new Exit(1, "sidekick currently only supports push from the current branch"); 142 | } 143 | }); 144 | } 145 | } 146 | 147 | function fail(err) { 148 | log("UNEXPECTED FAILURE " + (err ? (err.stack || err) : " without error passed")); 149 | doExit(1, "sidekick suffered an unexpected failure", err); 150 | } 151 | 152 | function prepush(pushInfo) { 153 | log('prepush: ' + JSON.stringify(pushInfo)); 154 | 155 | const id = uuid(); 156 | const cmd = _.defaults({ 157 | id: id, 158 | type: "push", 159 | }, pushInfo); 160 | 161 | 162 | cmd.actions = cmd.actions.map(function(action) { 163 | if(actionSupportedByPushSystem(action)) { 164 | return _.defaults({ 165 | id: uuid(), 166 | }, action); 167 | } 168 | return action; 169 | }); 170 | 171 | const main = MainProcess.acquire({ 172 | error: fail, 173 | }); 174 | 175 | const ui = prepushUi({ 176 | main: main, 177 | prepush: events, 178 | push: cmd, 179 | }); 180 | 181 | ui.on("exit", function(code) { 182 | log("ui asked for exit " + code); 183 | doExit(code); 184 | }); 185 | 186 | ui.on("skip", function() { 187 | doExit(0); 188 | }); 189 | 190 | if(_.some(cmd.actions, requiresGuiPicking)) { 191 | bootGuiToPickComparison(); 192 | } else { 193 | startAnalysis(); 194 | } 195 | 196 | return; 197 | 198 | function bootGuiToPickComparison() { 199 | events.emit("handOffToGui", { 200 | message: "can't guess which branch to compare with, booting GUI to compare", 201 | }); 202 | 203 | main.call("intent", { 204 | timeout: MAIN_TIMEOUT, 205 | type: "pickComparisonTarget", 206 | push: cmd, 207 | }) 208 | .done(function() { 209 | doExit(1); 210 | }, function(err) { 211 | exitOptionallySkipping(new Exit(1, "sidekick could not boot to pick comparison target")); 212 | }); 213 | } 214 | 215 | function startAnalysis() { 216 | events.emit("start"); 217 | 218 | log("sending pushStart"); 219 | main.call({ 220 | timeout: MAIN_TIMEOUT, 221 | }, "pushStart", cmd) 222 | .catch(fail); 223 | } 224 | } 225 | 226 | function exitOptionallySkipping(exit) { 227 | events.emit("exitOptionalSkipping", exit); 228 | } 229 | 230 | }; 231 | 232 | function actionSupportedByPushSystem(action) { 233 | return action.type === git.UPDATE_BRANCH || 234 | action.type === git.CREATE_BRANCH; 235 | } 236 | 237 | function output(m) { 238 | console.log(m); 239 | } 240 | 241 | function outputError(m) { 242 | console.error(m); 243 | } 244 | 245 | function prepushDescription(actions) { 246 | return actions.map(function(action) { 247 | if(action.type === git.UPDATE_BRANCH) { 248 | return `Push to ${action.remoteRef} from ${action.localRef} queued for analysis.`; 249 | } 250 | return `skipping ${action.type} to ${action.remoteRef}`; 251 | }).join("\n"); 252 | } 253 | 254 | function requiresGuiPicking(action) { 255 | return action.type === git.CREATE_BRANCH; 256 | } 257 | 258 | 259 | -------------------------------------------------------------------------------- /src/reporters/cliSummary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CLI output - writes directly to stdout 3 | */ 4 | "use strict"; 5 | 6 | const chalk = require('chalk'); 7 | const _ = require('lodash'); 8 | const Spinner = require('cli-spinner').Spinner; 9 | const debug = require('debug')('cliSummary'); 10 | 11 | const readline = require("readline"); 12 | 13 | const os = require('@sidekick/common/os'); 14 | 15 | const SUCCESS_TICK = chalk.green(os.isPosix() ? "✔" : "√"); 16 | 17 | module.exports = exports = reporter; 18 | 19 | const Readable = require("stream").Readable; 20 | const defaultOutput = (x) => console.log(x); 21 | 22 | 23 | const MESSAGE_TYPE = { 24 | INFO : 'cyan', 25 | ERROR : 'yellow', 26 | TITLE: 'magenta', 27 | FATAL: 'red', 28 | }; 29 | 30 | function reporter(emitter, outputter, command) { 31 | var spinner; 32 | var processed = 0; 33 | 34 | const metas = []; 35 | const errors = []; 36 | const output = outputter || defaultOutput; 37 | const canFail = []; 38 | 39 | const ignoreInput = new Readable; 40 | ignoreInput._read = _.noop; 41 | 42 | var cursor = readline.createInterface({ input: ignoreInput, output: process.stdout }); 43 | 44 | var installLines = {}; 45 | var curInstallerLine = 0; 46 | 47 | emitter.on("start", function(err, data, analysis /*: Analysis */){ 48 | if(command.ci) { 49 | outputString('Starting analysis'); 50 | } else { 51 | spinner = new Spinner('Analysing..'); 52 | spinner.start(); 53 | } 54 | 55 | try { 56 | var jobCount = _.reduce(analysis.plan.raw.byAnalysers, function(sum, analysersForLang){ 57 | return sum + (analysersForLang.analysers.length * analysersForLang.paths.length); 58 | }, 0); 59 | 60 | if(jobCount === 0){ 61 | outputString(`No files that need to be analysed :-)`) 62 | } else { 63 | var jobStr = pluralise('job', jobCount); 64 | var timeStr = ` (should take about ${timeToRun(jobCount)})`; 65 | var title = `${chalk.green('Sidekick')} is running ${jobCount} analysis ${jobStr}${timeStr}.`; 66 | outputString(title); 67 | } 68 | 69 | } catch (e){} //not the end of the world if we cant get timings 70 | 71 | function timeToRun(fileCount){ 72 | var realCount = parseInt(fileCount / 3); //takes about 300ms to do a job 73 | if(realCount < 20){ 74 | if(realCount < 1){ 75 | realCount = 1; //less than 3 jobs 76 | } 77 | return `${realCount} ${pluralise('second', realCount)}`; 78 | } else if(realCount <=30) { 79 | return '30 seconds'; 80 | } else { 81 | var minutes = parseInt(Math.floor(realCount / 60)); 82 | if(minutes < 1){ 83 | minutes = 1; 84 | } 85 | return `${minutes} ${pluralise('minute', minutes)}`; 86 | } 87 | } 88 | 89 | }); 90 | 91 | emitter.on("result", function(meta){ 92 | metas.push(meta); 93 | processed ++; 94 | }); 95 | 96 | emitter.on("error", function(err){ 97 | errors.push(err); 98 | }); 99 | 100 | emitter.on("end", function(){ 101 | outputSummary(getSummariesByAnalyser()); 102 | }); 103 | 104 | emitter.on('message', function(message, colour){ 105 | outputString(message, colour); 106 | }); 107 | 108 | //TODO needs to be 1 line per analyser 109 | emitter.on('downloading', function(data){ 110 | data = data[0]; 111 | debug(JSON.stringify(data)); 112 | if(data.canFailCi){ 113 | canFail.push(data.analyser); 114 | } 115 | outputString(`Downloading analyser: ${data.analyser} (${data.version})`); 116 | installLines[data.analyser] = curInstallerLine; 117 | curInstallerLine ++; 118 | }); 119 | emitter.on('downloaded', function(data){ 120 | data = data[0]; 121 | outputString(`Downloaded analyser: ${data.analyser}`); 122 | }); 123 | emitter.on('installing', function(data){ 124 | data = data[0]; 125 | outputString(`Installing analyser: ${data.analyser}`); 126 | }); 127 | emitter.on('installed', function(data){ 128 | data = data[0]; 129 | outputString(SUCCESS_TICK + ` Installed analyser: ${data.analyser}`); 130 | }); 131 | 132 | function outputString(x, colour) { 133 | if(colour){ 134 | try{ 135 | console.log(chalk[colour](x)); 136 | } catch(err){ 137 | outputJson(err); 138 | } 139 | } else { 140 | output(x); 141 | } 142 | } 143 | function outputJson(x) { 144 | output(JSON.stringify(x)); 145 | } 146 | 147 | function outputLine(str, line){ 148 | const TO_THE_RIGHT_OF_CURSOR = 1; 149 | readline.moveCursor(process.stdout, 0, 3 - line); 150 | readline.clearLine(process.stdout, TO_THE_RIGHT_OF_CURSOR); 151 | cursor.write(str); 152 | } 153 | 154 | function getMetaByAnalyser(analyser){ 155 | return _.values(_.groupBy(metas, 'analyser')[analyser]); 156 | } 157 | 158 | function getSummariesByAnalyser(){ 159 | var issuesByAnalyser = _.groupBy(metas, 'analyser'); 160 | var errByAnalyser = _.groupBy(errors, 'analyser'); 161 | 162 | return _.map(issuesByAnalyser, function(value, key){ 163 | const analyserFullname = value[0].analyser; 164 | var analyserName = value[0].analyserDisplayName || key; 165 | var summary = `-- ${analyserName} ${getDashes(analyserName)}`; //format is [-- analyserName ----------] (dashes fill upto 30 chars) 166 | 167 | var errStr = '.'; 168 | if(numErrorsForAnalyser(key) > 0){ 169 | var numErrors = errByAnalyser[key].length; 170 | errStr = ` (but couldn\'t analyse ${numErrors} ${pluralise('file', numErrors)}).`; 171 | } 172 | 173 | var totalIssues = _.reduce(value, function(acc, perFile){ 174 | return acc + perFile.issues.length; 175 | }, 0); 176 | 177 | debug('val: ' + JSON.stringify(value)); 178 | 179 | //some analysers specify an itemType for their annotations, e.g. 'security violation' 180 | var itemType = value[0].analyserItemType || 'issue'; //each annotation for an analyser will have the same itemType (if specified) 181 | var itemTypeStr = pluralise(itemType, totalIssues); 182 | var details = `We found ${totalIssues} ${itemTypeStr}${errStr}`; 183 | var failIssues = _.indexOf(canFail, analyserFullname) !== -1 ? totalIssues : 0; 184 | 185 | debug(`total: ${totalIssues}, fail: ${failIssues}`); 186 | 187 | return {title: summary, details: details, analyser: key, totalIssues: totalIssues, failIssues: failIssues}; 188 | }); 189 | 190 | function numErrorsForAnalyser(analyser){ 191 | if(_.keys(errByAnalyser).length > 0){ 192 | if(errByAnalyser[analyser]){ 193 | return errByAnalyser[analyser].length; 194 | } else { 195 | return 0; 196 | } 197 | } else { 198 | return 0; 199 | } 200 | } 201 | 202 | function getDashes(analyserName){ 203 | const LINE_LENGTH = 28; 204 | if(analyserName.length < LINE_LENGTH){ 205 | return Array(LINE_LENGTH - analyserName.length).join('-'); 206 | } else { 207 | return ''; 208 | } 209 | } 210 | } 211 | 212 | function outputMeta(summary) { 213 | outputString(' '); 214 | outputString('Analysis results:', MESSAGE_TYPE.TITLE); 215 | 216 | summary.forEach(function(summaryLine){ 217 | outputString(summaryLine.title, MESSAGE_TYPE.INFO); 218 | outputString(summaryLine.details + '\n', MESSAGE_TYPE.ERROR); 219 | 220 | var analyserMeta = getMetaByAnalyser(summaryLine.analyser); 221 | 222 | analyserMeta.forEach(function(meta){ 223 | if(meta.issues.length > 0){ 224 | outputString(meta.path); 225 | outputString(prettifyMeta(meta), MESSAGE_TYPE.ERROR); 226 | } 227 | }); 228 | outputString('', MESSAGE_TYPE.INFO); 229 | }); 230 | 231 | function prettifyMeta(meta){ 232 | var issues = ''; 233 | meta.issues.forEach(function(issue){ 234 | if(issue.startLine > -1) { 235 | issues += `Line ${issue.startLine}: ${issue.message}\n`; 236 | } else { 237 | issues += `${issue.message}\n`; 238 | } 239 | }); 240 | return issues; 241 | } 242 | 243 | } 244 | 245 | function outputSummary(summary) { 246 | if(!command.ci) { 247 | spinner.stop(true); //clear console spinner line 248 | } 249 | outputMeta(summary); 250 | 251 | var totalIssues = _.reduce(summary, function(acc, val){ 252 | return acc + val.totalIssues; 253 | }, 0); 254 | 255 | var failIssues = _.reduce(summary, function(acc, val){ 256 | return acc + val.failIssues; 257 | }, 0); 258 | 259 | if(command.ci){ 260 | const otherIssues = totalIssues - failIssues; 261 | outputString(`Analysis summary: ${failIssues > 0 ? chalk.red(failIssues) : failIssues} ${pluralise('issue', failIssues)} found that will fail the build (${otherIssues} other ${pluralise('issue', otherIssues)} found)`, MESSAGE_TYPE.TITLE); 262 | } else { 263 | outputString(`Analysis summary: ${totalIssues} ${pluralise('issue', totalIssues)} found`, MESSAGE_TYPE.TITLE); 264 | } 265 | 266 | summary.forEach(function(summaryLine){ 267 | outputString(` ${summaryLine.title} ${getCanFailStr(summaryLine.analyser, summaryLine.failIssues)}`, MESSAGE_TYPE.INFO); 268 | outputString(' ' + summaryLine.details + '\n', MESSAGE_TYPE.ERROR); 269 | }); 270 | 271 | function getCanFailStr(analyserName, failIssues){ 272 | if(command.ci && _.indexOf(canFail, analyserName) !== -1){ 273 | if(failIssues > 0){ 274 | return `${chalk.red('(will fail build)')}`; 275 | } else { 276 | return `(will fail build)`; 277 | } 278 | } else { 279 | return ''; 280 | } 281 | } 282 | } 283 | } 284 | 285 | function pluralise(str, count){ 286 | var suffix = 's'; //e.g. issue becomes issues 287 | if(_.endsWith(str, 'y')){ 288 | suffix = 'ies'; //e.g. dependency becomes dependencies 289 | return count === 1 ? str : str.substr(0, str.length - 1) + suffix; 290 | } else { 291 | return count === 1 ? str : str + suffix; 292 | } 293 | 294 | } 295 | -------------------------------------------------------------------------------- /src/run.js: -------------------------------------------------------------------------------- 1 | /** 2 | * runs SK in CLI mode. 3 | */ 4 | "use strict"; 5 | 6 | const git = require("@sidekick/git-helpers"); 7 | const log = require("debug")("cli:run"); 8 | 9 | const _ = require("lodash"); 10 | const Promise = require('bluebird'); 11 | 12 | const EventEmitter = require("events").EventEmitter; 13 | 14 | const runner = require("@sidekick/runner"); 15 | 16 | const userSettings = require('@sidekick/common/userSettings'); 17 | 18 | const yargs = require("yargs"); 19 | 20 | const reporters = Object.create(null); 21 | reporters["cli-summary"] = require("./reporters/cliSummary"); 22 | reporters["json-stream"] = require("./reporters/jsonStream"); 23 | reporters["junit"] = require("./reporters/junit"); 24 | 25 | module.exports = exports = function() { 26 | const argv = yargs 27 | .boolean("ci") 28 | .boolean("travis") 29 | .boolean("noCiExitCode") 30 | .argv; 31 | 32 | const path = argv._[1] || process.cwd(); 33 | const command = flagsToCommand(path, argv); 34 | if(command instanceof Error) { 35 | return doExit(1, command.message); 36 | } 37 | 38 | log("command: %j", command); 39 | 40 | const events = new EventEmitter; 41 | 42 | const reporter = getReporter(command.reporter); 43 | 44 | reporter(events, null, command); 45 | 46 | userSettings.load(); 47 | 48 | userSettings.isGitReachable() 49 | .then(function(){ 50 | git.setGitBin(userSettings.getGitBin()); 51 | 52 | return git.findRootGitRepo(command.path) 53 | .catch(git.NotAGitRepo, function() { 54 | return doExit(1, "sidekick run must be run on a git repo"); 55 | }) 56 | .then(function createTarget(repoPath) { 57 | return createGitTarget(command, repoPath, events) 58 | }) 59 | .catch(fail) 60 | .then(function(target) { 61 | 62 | log("starting analysis with %j", target); 63 | 64 | return runner.session({ 65 | target: target, 66 | shouldInstall: command.ci, 67 | events: events, 68 | }) 69 | 70 | }) 71 | .then((session) => runSession(session, command, events)) 72 | }, function(err){ 73 | return doExit(1, "Cannot run analysis - unable to find git at: " + userSettings.getGitBin()); 74 | }); 75 | }; 76 | 77 | exports.flagsToCommand = flagsToCommand; 78 | 79 | function flagsToCommand(path, argv) /*: { versus?: string, compare?: string, ci: Boolean, reporter?: string } | Error */ { 80 | 81 | const cmd = { 82 | path: path, 83 | reporter: argv.reporter, 84 | }; 85 | 86 | if(argv.travis && (argv.versus || argv.compare)) { 87 | return Error("travis integration determines commit range - do not supply --versus or --compare"); 88 | } 89 | 90 | // will later be replaced by commit values 91 | if(argv.versus) { 92 | cmd.versus = argv.versus; 93 | } 94 | 95 | if(argv.compare) { 96 | cmd.compare = argv.compare; 97 | } 98 | 99 | cmd.ci = argv.ci; 100 | cmd.noCiExitCode = argv.noCiExitCode; 101 | cmd.travis = argv.travis; 102 | if(cmd.travis === true) { 103 | cmd.ci = true; 104 | } 105 | return cmd; 106 | } 107 | 108 | 109 | function getReporter(name) { 110 | // default to summary report 111 | if(!name) { 112 | return reporters["cli-summary"]; 113 | } 114 | 115 | const reporter = reporters[name]; 116 | if(reporter) { 117 | return reporter; 118 | } 119 | 120 | try { 121 | return require(name); 122 | } catch(e) { 123 | console.error(`couldn't load reporter '${name}': ${e.stack}`); 124 | doExit(1); 125 | } 126 | } 127 | 128 | function runSession(session, command, events) { 129 | let heardIssues = false; 130 | 131 | const analysis = session.start(); 132 | 133 | analysis.on("start", function(err, data) { 134 | log('heard analysisStart'); 135 | events.emit("start", err, data, analysis); 136 | }); 137 | 138 | analysis.on("fileAnalyserEnd", emitResultForReporter); 139 | 140 | // when the session is finished, we have no more tasks schedules 141 | // and node should exit 142 | process.on('exit', function(code) { 143 | if(code !== 0) { 144 | // leave it as is, we're exiting as a result of failure elsewhere 145 | return; 146 | } 147 | const runExitCode = command.noCiExitCode ? 0 148 | : (heardIssues ? 1 : 0); 149 | 150 | log(`run changing process exit code to: ${runExitCode}, heardIssues ${heardIssues}, --no-ci-exit-code=${command.noCiExitCode}`); 151 | process.exit(runExitCode); 152 | }); 153 | 154 | analysis.on("end", function() { 155 | log('heard analysisEnd'); 156 | events.emit("end"); 157 | }); 158 | 159 | function emitResultForReporter(err, file, analyser, issues) { 160 | 161 | if(err) { 162 | events.emit("error", { 163 | path: file.path, 164 | analyser: analyser.analyser, 165 | error: err, 166 | }); 167 | } else { 168 | // if running on CI and the analyser that found meta is marked as 'failCiOnError - fail the build 169 | if(command.ci && analyser.failCiOnError === true) { 170 | heardIssues = heardIssues || issues.meta.length > 0; 171 | } 172 | 173 | events.emit("result", { 174 | path: file.path, 175 | analyser: analyser.analyser, 176 | analyserVersion: analyser.version, 177 | analyserDisplayName: analyser.displayName, 178 | analyserItemType: analyser.itemType, 179 | issues: issues.meta.map((i) => { 180 | return _.defaults(_.omit(i, "analyser", "version", "location"), i.location) 181 | }), 182 | }); 183 | } 184 | } 185 | } 186 | 187 | function createGitTarget(command, repoPath, events) { 188 | command = travis(command, events); 189 | const validated = _.mapValues(_.pick(command, "versus", "compare"), validate); 190 | 191 | return Promise.props(validated) 192 | .then(function(all) { 193 | const invalid = _.transform(all, function(invalid, error, name) { 194 | if(error instanceof Error) { 195 | invalid[name] = error; 196 | } 197 | }); 198 | 199 | if(_.isEmpty(invalid)) { 200 | // extend command with head/base. 201 | return { 202 | type: "git", 203 | path: repoPath, 204 | compare: all.compare, 205 | versus: all.versus, 206 | }; 207 | 208 | } else { 209 | return Promise.reject(Error(_.values(invalid).join(", and "))); 210 | } 211 | }); 212 | 213 | function validate(commitish, name) { 214 | if(commitish) { 215 | return git.parseCommitish(command.repoPath, commitish) 216 | .catch(function(e) { 217 | return Error(`cannot parse '${commitish}' as commitish value for '--${name}'`); 218 | }); 219 | } else { 220 | return `missing value for '--${name}'`; 221 | } 222 | } 223 | 224 | function travis(command, events) { 225 | if(!command.travis) { 226 | return command; 227 | } 228 | 229 | if(process.env.TRAVIS_COMMIT_RANGE) { 230 | // travis' commit range is '...' incorrectly - https://github.com/travis-ci/travis-ci/issues/4596 231 | const headBase = process.env.TRAVIS_COMMIT_RANGE.split(/\.{2,3}/); 232 | 233 | events.emit("message", `Travis build. Determined commit comparison to be: ${headBase[0]} to ${headBase[1]}`); 234 | 235 | return _.defaults({ 236 | compare: headBase[0], 237 | versus: headBase[1], 238 | }, command); 239 | } else { 240 | events.emit("message", `--travis build specified. Flag ignored because we don't appear to be running on TravisCi (no TRAVIS_COMMIT_RANGE env var)`); 241 | } 242 | 243 | return command; 244 | } 245 | } 246 | 247 | function outputError(e) { 248 | console.error(e); 249 | } 250 | 251 | function errorWithCode(code) { 252 | return function(err) { 253 | return err.code === code; 254 | }; 255 | } 256 | 257 | function doExit(code, message, error) { 258 | log(`exiting with code '${code}' with msg '${message}' ` + (error ? error.stack : "")); 259 | if(message) { 260 | outputError(message); 261 | } 262 | 263 | process.exit(code); 264 | } 265 | 266 | function fail(err) { 267 | log("UNEXPECTED FAILURE " + (err ? (err.stack || err) : " without error passed")); 268 | doExit(1, "sidekick suffered an unexpected failure", err); 269 | } 270 | 271 | exports.help = ` 272 | usage: sidekick run [ some/repo/path ] [ --versus commitish ] [ --compare commitish ] [ --reporter npmPackageName|absolutePath ] [ --ci ] [ --travis ] [ --no-ci-exit-code ] 273 | 274 | Runs sidekick in cli mode, reporting results via reporter. 275 | 276 | Compare and Versus 277 | 278 | Without a --versus, simply analyses all files in the repo. with --versus compares current working copy 279 | (i.e the files in the repo, committed or not) vs specified commit. With both --versus and --compare, 280 | will analyse changes that have happened since the commit at versus, until the commit at compare. 281 | 282 | Examples 283 | 284 | sidekick run --versus origin/master # working copy vs latest fetched commit from origin/master 285 | sidekick run --versus head~5 # working copy vs 5 commits ago 286 | sidekick run --compare HEAD --versus head~5 # current commit vs 5 commits ago 287 | 288 | CI 289 | 290 | With --ci flag, sidekick will install all analysers specified in your .sidekickrc file. If an analyser relies 291 | on a config file, e.g. '.eslintrc', these settings will be used. If no config can be found, then the analyser will 292 | not run. 293 | 294 | With --travis flag, sidekick will only analyse code that changed in PR etc (will set --ci to true if not explicitly set to false) 295 | 296 | Reporters 297 | 298 | Without --reporter, a summary reporter will be used that totals up the issues found. 299 | 300 | json-stream - a stream of newline delimited JSON: http://ndjson.org 301 | 302 | e.g {}\\n{}\\n{} 303 | 304 | junit - junit compatible XML. incremental results, emitting a per analyser per file 305 | (i.e every (file, analyser) pair), with elements per issue found. 306 | 307 | Exit code 308 | 309 | Will exit with status code 1 if any issues are detected - disable this with --no-ci-exit-code 310 | 311 | `; 312 | 313 | -------------------------------------------------------------------------------- /src/prepush-ui-tty.js: -------------------------------------------------------------------------------- 1 | /** 2 | * UI for prepush 3 | */ 4 | "use strict"; 5 | 6 | const _ = require("lodash"); 7 | const log = require("debug")("prepushUiTty"); 8 | 9 | const events = require("events"); 10 | 11 | const readline = require("readline"); 12 | 13 | const chalk = require("chalk"); 14 | const green = chalk.green; 15 | const yellow = chalk.yellow; 16 | 17 | const SUCCESS_TICK = chalk.green("✔"); 18 | 19 | const MainProcess = require("./MainProcess"); 20 | 21 | const ttys = require("ttys"); 22 | 23 | module.exports = exports = pushUi; 24 | 25 | exports._runUi = runUi; 26 | 27 | const PUSH_CANCELLED_MESSAGE = exports.PUSH_CANCELLED_MESSAGE = 28 | `\nCancelling push... (↓ ${green("don't worry")} - 'error:' below is just telling you that you decided not to continue with the push)\n`; 29 | 30 | const COMPLETE_MESSSAGE = 31 | `${green("Sidekick")} ↓ ${green("PUSHED")} - we cancelled the original push to switch it for the one with your fixes, so git correctly reports the original 'failed to push' 32 | `; 33 | 34 | function pushUi(setup) { 35 | // use tty module to ensure we end up with a tty in the slightly weird git hook environment 36 | _.defaults(setup, { input: ttys.stdin, output: ttys.stdout }) 37 | 38 | setup.cursor = readline.createInterface(_.pick(setup, "input", "output")); 39 | 40 | return runUi(setup); 41 | } 42 | 43 | function runUi(setup) { 44 | 45 | const main = setup.main; 46 | const prepush = setup.prepush; 47 | 48 | const cursor = setup.cursor; 49 | const pushDefinition = setup.push; 50 | 51 | let lineDy = 0; 52 | let lineDx = 0; 53 | 54 | const state = { 55 | analysed: null, 56 | total: null, 57 | issuesInModifiedLines: null, 58 | uiOpened: false, 59 | action: "", 60 | status: "unstarted", 61 | // null = unknown, then bool 62 | success: null, 63 | userInput: "", 64 | issueCount: 0, 65 | }; 66 | 67 | const commands = { 68 | s: _.once(skip), 69 | o: _.once(open), 70 | }; 71 | 72 | const api = new events.EventEmitter; 73 | 74 | function init() { 75 | listenForEvents(); 76 | listenForTerm(); 77 | renderAnalysis(); 78 | observePush(); 79 | handleInput(); 80 | } 81 | 82 | init(); 83 | 84 | return api; 85 | 86 | function handleInput() { 87 | cursor.input.resume(); 88 | 89 | cursor.input.on("data", function(data) { 90 | log("got data " + data); 91 | data.toString().split("").forEach(function(c) { 92 | const cmd = commands[c]; 93 | if(cmd) { 94 | log("command: " + cmd.name); 95 | cmd(); 96 | renderAnalysis(); 97 | } 98 | }); 99 | 100 | // hide the characters 101 | readline.moveCursor(setup.input, -data.length, 0); 102 | readline.clearLine(setup.input, 1); 103 | }); 104 | } 105 | 106 | function listenForEvents() { 107 | log('listenForEvents'); 108 | prepush.on("exitOptionallySkipping", exitOptionallySkipping); 109 | prepush.on("handOffToGui", handOff); 110 | } 111 | function exitOptionallySkipping(exit) { 112 | // output the message 113 | write(exit.message); 114 | 115 | cursor.question("\nPush without analysis? (y/n): ", function(a) { 116 | const skip = a.trim() === "y"; 117 | if(skip) { 118 | exit(0); 119 | } else { 120 | exitWithMessage(1, PUSH_CANCELLED_MESSAGE); 121 | } 122 | }); 123 | } 124 | 125 | function handOff(event) { 126 | write(`${event.message} (s) to skip and push`); 127 | } 128 | 129 | function exitWithMessage(code, msg) { 130 | write(msg); 131 | exit(code); 132 | } 133 | 134 | function observePush() { 135 | 136 | log('observePush'); 137 | const pushProcess = main.getActor(pushDefinition.id); 138 | 139 | pushProcess.once("started", function() { 140 | log("heard push started"); 141 | }); 142 | 143 | pushProcess.once("end", function(err, pushResult) { 144 | if(err) { 145 | state.error = err; 146 | skipDueToFailure(); 147 | } else { 148 | completeExit(pushResult); 149 | } 150 | }); 151 | 152 | pushProcess.once("initialAnalysesEnd", function() { 153 | log("heard initialAnalysesEnd"); 154 | state.success = true; 155 | renderAnalysis(); 156 | 157 | if(!state.issuesInModifiedLines && state.success && !state.uiOpened) { 158 | noIssuesFoundExit(); 159 | } 160 | }); 161 | 162 | pushProcess.once("firstIssueInModifiedLines", function() { 163 | log("first issue detected"); 164 | state.issuesInModifiedLines = true; 165 | renderAnalysis(); 166 | }); 167 | 168 | pushProcess.on("updateStarted", function(update) { 169 | log("heard update start: " + update.id); 170 | 171 | const updateProcess = main.getActor(update.id); 172 | updateProcess.on("progress", progress); 173 | 174 | updateProcess.on("metaFound", metaFound); 175 | 176 | updateProcess.once("end", function() { 177 | log("heard end"); 178 | updateProcess.removeListener("progress", progress); 179 | state.status = "complete"; 180 | renderAnalysis() 181 | }); 182 | 183 | function progress(update) { 184 | state.status = "started"; 185 | 186 | state.total = update.total; 187 | // TODO kludge for analysed > total, which needs to be fixed upstream 188 | state.analysed = Math.min(update.total, update.analysed); 189 | renderAnalysis(); 190 | } 191 | 192 | function metaFound(meta) { 193 | log(`got meta: ${JSON.stringify(meta)}`); 194 | //FIXME - we are emitting meta from analysis_listener twice for each file!! 195 | state.issueCount = state.issueCount + (meta.inModifiedLines / 2); 196 | renderAnalysis(); 197 | } 198 | }); 199 | } 200 | 201 | function open() { 202 | state.uiOpened = true; 203 | main.call("pushReviewUi", pushDefinition.id); 204 | renderAnalysis(); 205 | } 206 | 207 | function skip() { 208 | api.emit("skip"); 209 | 210 | main.call("pushSkip", pushDefinition.id); 211 | exit(0); 212 | } 213 | 214 | function fail(err) { 215 | if(err.message === "working-copy-unclean") { 216 | renderUnclean(); 217 | } else if(err.message === "branch-not-current") { 218 | renderNotCurrentBranch(); 219 | } else { 220 | log(err && (err.stack || err)); 221 | exit(1); 222 | } 223 | } 224 | 225 | function skipDueToFailure() { 226 | reset(); 227 | skipOrContinue(`${green('Sidekick')} analysis failed unexpectedly, push anyway?`); 228 | } 229 | 230 | function renderUnclean() { 231 | reset(); 232 | skipOrContinue("You have unsaved changes in your working copy (modified or un-tracked files). Sidekick doesn't support this at present to keep your work safe.\n\nStash or create a temporary branch to enable analysis\n\nSkip analysis and push anyway?"); 233 | } 234 | 235 | function renderNotCurrentBranch() { 236 | skipOrContinue("You're pushing to a branch you currently haven't checked out. Sidekick will support this, but we've decided against switching branches automatically for now. Please checkout the branch before pushing\n\nSkip analysis and push anyway?"); 237 | } 238 | 239 | function skipOrContinue(question) { 240 | cursor.question(question + " (y/n)\n>", shouldPush); 241 | } 242 | 243 | function shouldPush(answer) { 244 | const yes = /^\s*y\s*$/.test(answer); 245 | if(!yes) { 246 | console.error(PUSH_CANCELLED_MESSAGE); 247 | } 248 | exit(yes ? 0 : 1); 249 | } 250 | 251 | 252 | function renderAnalysis() { 253 | reset(); 254 | writeLine(`${analysisStatusText()}`); //Sidekick started.. 255 | writeLine(optionsText()); //(o) to open ui, (s) to skip 256 | writeLine(``); // 257 | writeLine(header()); // Analysis summary: 258 | writeLine(progressText()); // 2 of 12 files analysed 259 | writeLine(actionText()); // issues found in modified lines 260 | writeLine(``); // 261 | writeLine(footer()); //Opening UI so you can fix.. 262 | } 263 | 264 | function optionsText() { 265 | var options = []; 266 | 267 | if(!state.issuesInModifiedLines && !state.uiOpened) { 268 | options.push("(o) to open review UI"); 269 | } 270 | 271 | options.push("(s) to skip analysis and push"); 272 | 273 | return options.join(", "); 274 | } 275 | 276 | function progressText(){ 277 | if(state.status === 'started' || state.status === 'complete') { 278 | return ` ${state.analysed}/${state.total} modified files analysed.`; 279 | } else { 280 | return ''; 281 | } 282 | 283 | } 284 | 285 | function analysisStatusText() { 286 | switch(state.status) { 287 | case "waiting": 288 | case "unstarted": return `${green('Sidekick')} starting up..`; 289 | case "complete": return `${green('Sidekick')} finished.`; 290 | case "started": return `${green('Sidekick')} running..`; 291 | default: 292 | throw new Error(`unknown state ${state.status}`); 293 | } 294 | } 295 | 296 | function header(){ 297 | return ` ${chalk.magenta('Analysis summary:')}`; 298 | } 299 | 300 | function footer() { 301 | if(state.issuesInModifiedLines){ 302 | return "Opening the UI so you can fix.."; 303 | } else { 304 | return ''; 305 | } 306 | } 307 | 308 | function actionText() { 309 | //2 char indent to allow for icons 310 | if(state.status === "unstarted" || state.status === 'waiting') { 311 | return ` ${green('All good so far')}.`; 312 | } else if(state.status === "started") { 313 | if(state.issuesInModifiedLines){ 314 | return ` ${yellow(state.issueCount + ' issues')} found in modified lines.`; 315 | } 316 | } else if(state.status === 'complete'){ 317 | if(!state.issuesInModifiedLines){ 318 | return ` ${state.success ? SUCCESS_TICK + " " : " "}${green('All good')} - continuing with push.`; 319 | } else { 320 | return ` ${yellow(state.issueCount + ' issues')} found in modified lines.`; 321 | } 322 | } 323 | } 324 | 325 | function reset() { 326 | readline.moveCursor(setup.input, -lineDx, -lineDy); 327 | readline.clearScreenDown(setup.input); 328 | lineDy = 0; 329 | lineDx = 0; 330 | } 331 | 332 | function writeLine(line) { 333 | cursor.write(line + "\n") 334 | lineDy += 1; 335 | } 336 | 337 | function listenForTerm() { 338 | log('listenForTerm'); 339 | cursor.on('SIGINT', function() { 340 | exit(1); 341 | }) 342 | } 343 | 344 | function exit(code) { 345 | api.emit("exit", code); 346 | } 347 | 348 | function noIssuesFoundExit() { 349 | exit(0); 350 | } 351 | 352 | function completeExit(pushResult) { 353 | reset(); 354 | if(pushResult === "pushed") { 355 | writeLine(COMPLETE_MESSSAGE); 356 | exit(1); 357 | } else { 358 | // we didn't add anything to push, so leave it to git 359 | exit(0); 360 | } 361 | } 362 | 363 | function write(line) { 364 | cursor.write(line + "\n"); 365 | } 366 | 367 | } 368 | --------------------------------------------------------------------------------