├── 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 | [](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 |
--------------------------------------------------------------------------------