diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..beffa30 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..9b1c8b1 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +/dist diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..7b84619 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": [ + "oclif", + "oclif-typescript" + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..cb1d9d5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: Continuous Integration + +on: + pull_request: + push: + branches: + - '*' + +jobs: + test: + name: Run unit tests + runs-on: ubuntu-latest + + steps: + - name: Checkout + id: checkout + uses: actions/checkout@v4 + + - name: Setup node.js + id: setup-node + uses: actions/setup-node@v3 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + id: npm-ci + run: npm ci + + - name: Run tests + id: npm-test + run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d10025 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +**/.DS_Store +*-debug.log +*-error.log +/.idea +/.nyc_output +/dist +/lib +/package-lock.json +/tmp +/yarn.lock +node_modules +oclif.manifest.json diff --git a/.mocharc.json b/.mocharc.json new file mode 100644 index 0000000..4a09d14 --- /dev/null +++ b/.mocharc.json @@ -0,0 +1,12 @@ +{ + "require": [ + "test/helpers/init.js", + "ts-node/register" + ], + "watch-extensions": [ + "ts" + ], + "recursive": true, + "reporter": "spec", + "timeout": 60000 +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9d7ceb4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Ọlalérè Williams + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d36bae --- /dev/null +++ b/README.md @@ -0,0 +1,59 @@ +# SizeUp CLI + +This repository contains a CLI that wraps the [`sizeup` library](https://github.com/lerebear/sizeup) in order to provide a way to estimate the reviewability of a local diff. + +## Installation + +```sh +npm install sizeup-cli +``` + +## Usage + +Run using `npx` e.g. + +```sh +npx sizeup --help +``` + +The output of the above help command looks like this: + +```sh +Estimate how difficult a diff will be to review + +USAGE + $ sizeup [DIFF] [-c ] [-t ] [-v] + +ARGUMENTS + DIFF + [default: @wip] An identifier for the diff to evalute. + + The following identifiers are supported: + + @wip - special identifier that denotes the modified files in the git working tree (i.e. the result of `git diff`) + @staged - special identifer that denotes the files in the git staging area (i.e. the result of `git diff --staged`) + - The URL of a pull request on GitHub (e.g. "https://github.com/lerebear/sizeup/pull/1") + +FLAGS + -c, --config-path= Path to configuration file for the sizeup lib. + For more details, see: https://github.com/lerebear/sizeup#configuration + -t, --token-path= Path to a file containing a GitHub API token. + If this flag is omitted and the `diff` argument is a URL, then this tool will prompt for a token instead. + -v, --verbose Explain scoring procedure in detail + +DESCRIPTION + Estimate how difficult a diff will be to review + +EXAMPLES + Estimate the reviewability of the diff of the modified files in the git working tree + + $ sizeup @wip + + Estimate the reviewability of the diff of the staged files in the git index using a custom configuration file + + $ sizeup @staged --config-path experimental.yaml + + (Re)compute the reviewability of the diff from an existing pull request + + $ sizeup https://github.com/lerebear/sizeup/pull/1 +``` diff --git a/bin/dev b/bin/dev new file mode 100755 index 0000000..bbc3f51 --- /dev/null +++ b/bin/dev @@ -0,0 +1,17 @@ +#!/usr/bin/env node + +const oclif = require('@oclif/core') + +const path = require('path') +const project = path.join(__dirname, '..', 'tsconfig.json') + +// In dev mode -> use ts-node and dev plugins +process.env.NODE_ENV = 'development' + +require('ts-node').register({project}) + +// In dev mode, always show stack traces +oclif.settings.debug = true; + +// Start the CLI +oclif.run().then(oclif.flush).catch(oclif.Errors.handle) diff --git a/bin/dev.cmd b/bin/dev.cmd new file mode 100644 index 0000000..077b57a --- /dev/null +++ b/bin/dev.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\dev" %* \ No newline at end of file diff --git a/bin/run b/bin/run new file mode 100755 index 0000000..a7635de --- /dev/null +++ b/bin/run @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +const oclif = require('@oclif/core') + +oclif.run().then(require('@oclif/core/flush')).catch(require('@oclif/core/handle')) diff --git a/bin/run.cmd b/bin/run.cmd new file mode 100644 index 0000000..968fc30 --- /dev/null +++ b/bin/run.cmd @@ -0,0 +1,3 @@ +@echo off + +node "%~dp0\run" %* diff --git a/package.json b/package.json new file mode 100644 index 0000000..34b7e44 --- /dev/null +++ b/package.json @@ -0,0 +1,68 @@ +{ + "name": "sizeup-cli", + "version": "0.1.0", + "description": "CLI wrapper for the sizeup library", + "author": "Ọlálérè Williams", + "bin": { + "sizeup": "./bin/run" + }, + "homepage": "https://github.com/lerebear/sizeup-cli", + "license": "MIT", + "main": "dist/index.js", + "repository": "lerebear/sizeup-cli", + "files": [ + "/bin", + "/dist", + "/npm-shrinkwrap.json", + "/oclif.manifest.json" + ], + "dependencies": { + "@oclif/core": "^3", + "@oclif/plugin-help": "^5", + "@oclif/plugin-plugins": "^3.6.1", + "octokit": "^3.1.1", + "simple-git": "^3.20.0", + "sizeup": "github:lerebear/sizeup#v0.1.1" + }, + "devDependencies": { + "@oclif/test": "^2.5.3", + "@types/chai": "^4", + "@types/mocha": "^9.0.0", + "@types/node": "^16.18.50", + "chai": "^4", + "eslint": "^7.32.0", + "eslint-config-oclif": "^4", + "eslint-config-oclif-typescript": "^1.0.3", + "mocha": "^9", + "oclif": "^3.15.0", + "shx": "^0.3.3", + "ts-node": "^10.9.1", + "tslib": "^2.6.2", + "typescript": "^4.9.5" + }, + "oclif": { + "bin": "sizeup", + "dirname": "sizeup", + "default": ".", + "commands": "./dist", + "plugins": [ + "@oclif/plugin-help" + ] + }, + "scripts": { + "build": "shx rm -rf dist && tsc -b", + "lint": "eslint . --ext .ts --config .eslintrc", + "postpack": "shx rm -f oclif.manifest.json", + "posttest": "npm run lint", + "prepack": "npm run build && oclif manifest", + "test": "mocha --forbid-only \"test/**/*.test.ts\"" + }, + "engines": { + "node": ">=12.0.0" + }, + "bugs": "https://github.com/lerebear/sizeup-cli/issues", + "keywords": [ + "oclif" + ], + "types": "dist/index.d.ts" +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..1117adb --- /dev/null +++ b/src/index.ts @@ -0,0 +1,120 @@ +import {Args, Command, Flags, ux} from '@oclif/core' +import {simpleGit} from 'simple-git' +import SizeUpLibrary from 'sizeup' +import {Octokit} from 'octokit' +import * as fs from 'node:fs' + +export default class SizeUp extends Command { + static description = 'Estimate how difficult a diff will be to review'; + + static examples = [ + { + description: 'Estimate the reviewability of the diff of the modified files in the git working tree', + command: '<%= config.bin %> @wip', + }, + { + description: 'Estimate the reviewability of the diff of the staged files in the git index using a custom configuration file', + command: '<%= config.bin %> @staged --config-path experimental.yaml', + }, + { + description: '(Re)compute the reviewability of the diff from an existing pull request', + command: '<%= config.bin %> https://github.com/lerebear/sizeup/pull/1', + }, + ]; + + static flags = { + 'config-path': Flags.string({ + char: 'c', + description: 'Path to configuration file for the sizeup lib.\n' + + 'For more details, see: https://github.com/lerebear/sizeup#configuration', + required: false, + }), + 'token-path': Flags.string({ + char: 't', + description: 'Path to a file containing a GitHub API token.\n' + + 'If this flag is omitted and the `diff` argument is a URL, then this tool will prompt for a token instead.', + required: false, + }), + verbose: Flags.boolean({ + char: 'v', + description: 'Explain scoring procedure in detail', + required: false, + }), + }; + + static args = { + diff: Args.string({ + description: 'An identifier for the diff to evalute.\n\n' + + 'The following identifiers are supported:\n\n' + + ' @wip - special identifier that denotes the modified files in the git working tree (i.e. the result of `git diff`)\n' + + ' @staged - special identifer that denotes the files in the git staging area (i.e. the result of `git diff --staged`)\n' + + ' - The URL of a pull request on GitHub (e.g. "https://github.com/lerebear/sizeup/pull/1")', + required: false, + default: '@wip', + }), + }; + + async run(): Promise { + const {args, flags} = await this.parse(SizeUp) + + const git = simpleGit() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_scheme, _blank, _domain, owner, repo, _path, number] = args.diff.split('/') + let octokit: Octokit | undefined + let token: string | undefined + let diff: string | undefined + + switch (args.diff) { + case '@wip': + ux.action.start('Retrieving diff from the working tree') + diff = await git.diff() + ux.action.stop() + break + case '@stage': + case '@staged': + case '@cache': + case '@cached': + ux.action.start('Retrieving diff from the staging area') + diff = await git.diff(['--staged']) + ux.action.stop() + break + default: + token = flags['token-path'] ? + fs.readFileSync(flags['token-path']).toString().trim() : + await ux.prompt('Please enter a GitHub API token', {type: 'hide'}) + octokit = new Octokit({auth: token}) + try { + ux.action.start(`Retrieving diff from ${args.diff}`) + diff = ( + await octokit.rest.pulls.get({ + owner: owner, + repo: repo, + // eslint-disable-next-line camelcase + pull_number: Number.parseInt(number, 10), + mediaType: {format: 'diff'}, + }) + ).data as unknown as string + } catch (error) { + const message = (error instanceof Error) ? error.message : '' + ux.action.stop(`failed (${message.toLowerCase()})`) + return + } + + ux.action.stop() + break + } + + if (diff) { + ux.action.start(`Evaluating the diff with the ${flags['config-path'] ? `config from ${flags['config-path']}` : 'default config'}`) + const score = SizeUpLibrary.evaluate(diff!, flags['config-path']) + ux.action.stop() + this.log(`Your diff scored ${score.result}${(score.category ? ` (${score.category.name})` : '')}.`) + + if (flags.verbose) { + this.log(`The score was computed as follows:\n${score.toString()}`) + } + } else { + this.log(`The diff identified by '${args.diff}' was empty.`) + } + } +} diff --git a/test/helpers/init.js b/test/helpers/init.js new file mode 100644 index 0000000..338e715 --- /dev/null +++ b/test/helpers/init.js @@ -0,0 +1,6 @@ +const path = require('path') +process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json') +process.env.NODE_ENV = 'development' + +global.oclif = global.oclif || {} +global.oclif.columns = 80 diff --git a/test/index.test.ts b/test/index.test.ts new file mode 100644 index 0000000..0035d18 --- /dev/null +++ b/test/index.test.ts @@ -0,0 +1,13 @@ +import {expect, test} from '@oclif/test' +import sizeup from '../src' + +describe('sizeup', () => { + test + .stderr() + .stdout() + .do(() => sizeup.run([])) + .it('runs sizeup cmd', ctx => { + expect(ctx.error).to.be.undefined + expect(ctx.stdout).to.match(/^(Your diff scored|The diff identified by '@wip' was empty)/) + }) +}) diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..95898fc --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig", + "compilerOptions": { + "noEmit": true + }, + "references": [ + {"path": ".."} + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..34727db --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "declaration": true, + "importHelpers": true, + "module": "commonjs", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "target": "es2019" + }, + "include": [ + "src/**/*" + ] +}