├── .husky ├── .gitignore ├── commit-msg └── prepare-commit-msg ├── _config.yml ├── .eslintignore ├── logo.png ├── .github ├── FUNDING.yml ├── boring-cyborg.yml ├── CODEOWNERS ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── lint.yml │ ├── release.yml │ └── tests.yml ├── bin ├── prune-merged-branches └── disallow-master-commits ├── .vscode ├── extensions.json └── settings.json ├── commitlint.config.js ├── tsconfig.eslint.json ├── tsconfig.json ├── src ├── main-branch.ts ├── disallow-master-commits.ts ├── __snapshots__ │ ├── disallow-master-commits.spec.ts.snap │ └── prune-merged-branches.spec.ts.snap ├── disallow-master-commits.spec.ts ├── prune-merged-branches.spec.ts ├── prune-merged-branches.ts └── copyright-checker.ts ├── jest.config.js ├── CHANGELOG.md ├── .gitignore ├── .eslintrc.js ├── jest.setup.ts ├── package.json └── README.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-midnight -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .eslintrc.js -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kibibit/dev-tools/beta/logo.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: 2 | - https://paypal.me/thatkookooguy?locale.x=en_US -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" -------------------------------------------------------------------------------- /.github/boring-cyborg.yml: -------------------------------------------------------------------------------- 1 | labelPRBasedOnFilePath: 2 | 3 | Tools: 4 | - .github/**/* 5 | - .vscode/**/* 6 | - .husky/**/* -------------------------------------------------------------------------------- /.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | . "$(dirname "$0")/_/husky.sh" 3 | exec < /dev/tty && node_modules/.bin/cz --hook || true 4 | -------------------------------------------------------------------------------- /bin/prune-merged-branches: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { pruneMergedBranches } = require('../lib/prune-merged-branches'); 3 | pruneMergedBranches(); -------------------------------------------------------------------------------- /bin/disallow-master-commits: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { disallowMainBranchesCommits } = require('../lib/disallow-master-commits'); 3 | const yargs = require('yargs'); 4 | const { hideBin } = require('yargs/helpers'); 5 | 6 | const argv = yargs(hideBin(process.argv)).argv; 7 | disallowMainBranchesCommits(argv._); 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "actboy168.tasks", 4 | "codeandstuff.package-json-upgrade", 5 | "coenraads.bracket-pair-colorizer", 6 | "dbaeumer.vscode-eslint", 7 | "eamodio.gitlens", 8 | "jock.svg", 9 | "mhutchie.git-graph", 10 | "wayou.vscode-todo-highlight", 11 | "wix.vscode-import-cost" 12 | ] 13 | } -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ '@commitlint/config-angular' ], 3 | rules: { 4 | 'type-enum': [ 5 | 2, 6 | 'always', [ 7 | 'build', 8 | 'chore', 9 | 'ci', 10 | 'docs', 11 | 'feat', 12 | 'fix', 13 | 'perf', 14 | 'refactor', 15 | 'revert', 16 | 'style', 17 | 'test' 18 | ] 19 | ] 20 | } 21 | }; -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Each line is a file pattern followed by one or more owners. 2 | 3 | # These owners will be the default owners for everything in 4 | # the repo. Unless a later match takes precedence, 5 | # @global-owner1 and @global-owner2 will be requested for 6 | # review when someone opens a pull request. 7 | * @thatkookooguy 8 | 9 | # Build\Github Actions Owners 10 | /tools/ @thatkookooguy 11 | /.github/ @thatkookooguy 12 | /.devcontainer/ @thatkookooguy 13 | /.vscode/ @thatkookooguy -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "declarationMap": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | "baseUrl": "./", 14 | "paths": { 15 | }, 16 | "incremental": true, 17 | "skipLibCheck": true 18 | }, 19 | "exclude": [ "node_modules", "test", "dist", "lib" ] 20 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "declarationMap": true, 6 | "removeComments": true, 7 | "emitDecoratorMetadata": true, 8 | "experimentalDecorators": true, 9 | "allowSyntheticDefaultImports": true, 10 | "target": "es2017", 11 | "sourceMap": true, 12 | "esModuleInterop": true, 13 | "outDir": "./lib", 14 | "baseUrl": "./", 15 | "paths": { 16 | }, 17 | "incremental": true, 18 | "skipLibCheck": true 19 | }, 20 | "exclude": [ 21 | "node_modules", 22 | "test", 23 | "dist", 24 | "lib", 25 | "examples", 26 | "**/*spec.ts", 27 | "**/*mock.ts", 28 | "jest.config.js", 29 | "jest.setup.ts" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 🚨 Review the [guidelines for contributing](../CONTRIBUTING.md) to this repository. 🚨 3 | 4 | Please explain the changes you made here. 5 | 6 | You can explain individual changes as a list: 7 | 8 | - [ ] feature name: extra details 9 | - [ ] bug: extra details (resolves #`issue_number`) 10 | 11 | ### Checklist 12 | Please check if your PR fulfills the following requirements: 13 | - [ ] Code compiles correctly (`npm run build`) 14 | - [ ] Code is linted 15 | - [ ] Created tests which fail without the change (if possible) 16 | - All **relevant** tests are passing 17 | - [ ] Server Unit Tests 18 | - [ ] Client Unit Tests 19 | - [ ] Achievements Unit Tests 20 | - [ ] API Tests 21 | - [ ] E2E Tests 22 | - [ ] Extended the README / documentation, if necessary -------------------------------------------------------------------------------- /src/main-branch.ts: -------------------------------------------------------------------------------- 1 | import { BranchSummary } from 'simple-git/promise'; 2 | 3 | 4 | export const MAIN_BRANCHES = [ 5 | 'master', 6 | 'main', 7 | 'develop', 8 | 'dev', 9 | 'staging', 10 | 'next', 11 | 'beta', 12 | 'alpha' 13 | ]; 14 | 15 | export function getMainBranch( 16 | branchSummaryResult: BranchSummary, 17 | mainBranchList = MAIN_BRANCHES 18 | ) { 19 | for (const branchName of mainBranchList) { 20 | if (branchSummaryResult[branchName]) { 21 | return branchSummaryResult[branchName]; 22 | } 23 | } 24 | } 25 | 26 | export function checkIsMainBranchCheckedOut( 27 | branchSummaryResult: BranchSummary, 28 | mainBranchList = MAIN_BRANCHES 29 | ) { 30 | const currentCheckedoutBranch = branchSummaryResult.current; 31 | return mainBranchList.includes(currentCheckedoutBranch) ? 32 | currentCheckedoutBranch : 33 | false; 34 | } 35 | -------------------------------------------------------------------------------- /src/disallow-master-commits.ts: -------------------------------------------------------------------------------- 1 | import { isEmpty } from 'lodash'; 2 | import simpleGit from 'simple-git/promise'; 3 | 4 | import { checkIsMainBranchCheckedOut } from './main-branch'; 5 | 6 | const git = simpleGit(); 7 | 8 | export async function disallowMainBranchesCommits(mainBranchList?: string[]) { 9 | try { 10 | mainBranchList = isEmpty(mainBranchList) ? undefined : mainBranchList; 11 | const branchSummaryResult = await git.branch(['-vv']); 12 | const isMainBranchCheckedOut = 13 | checkIsMainBranchCheckedOut(branchSummaryResult, mainBranchList); 14 | 15 | if (isMainBranchCheckedOut) { 16 | console.log([ 17 | 'Should not commit directly to ', 18 | `${ isMainBranchCheckedOut } branch when working locally` 19 | ].join('')); 20 | process.exit(1); 21 | } 22 | process.exit(0); 23 | } catch (err) { 24 | console.error(err); 25 | process.exit(1); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - beta 9 | 10 | jobs: 11 | build: 12 | name: Build Production 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout commit 16 | uses: actions/checkout@v2 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14 21 | - name: Cache node modules 22 | uses: actions/cache@v2 23 | env: 24 | cache-name: cache-node-modules 25 | with: 26 | path: '**/node_modules' 27 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 28 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 29 | - name: Install Dependencies 30 | run: npm install 31 | - name: Build 32 | run: npm run build -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | roots: [ 4 | '/src' 5 | ], 6 | setupFilesAfterEnv: [ 7 | './jest.setup.ts' 8 | ], 9 | testMatch: [ 10 | '**/__tests__/**/*.+(ts|tsx|js)', 11 | '**/?(*.)+(spec|test).+(ts|tsx|js)' 12 | ], 13 | transform: { 14 | '^.+\\.(ts|tsx)$': 'ts-jest' 15 | }, 16 | collectCoverageFrom: [ 17 | '**/*.(t|j)s', 18 | '!**/*.decorator.ts', 19 | '!**/*.mock.ts', 20 | '!**/index.ts', 21 | '!**/dev-tools/**/*.ts' 22 | ], 23 | reporters: [ 24 | 'default', 25 | [ 26 | 'jest-stare', 27 | { 28 | 'resultDir': './test-results', 29 | 'reportTitle': 'jest-stare!', 30 | 'additionalResultsProcessors': [ 31 | 'jest-junit' 32 | ], 33 | 'coverageLink': './coverage/lcov-report/index.html' 34 | } 35 | ] 36 | ], 37 | coverageReporters: [ 38 | 'json', 39 | 'lcov', 40 | 'text', 41 | 'clover', 42 | 'html' 43 | ], 44 | coverageDirectory: './test-results/coverage' 45 | }; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "editor.insertSpaces": true, 4 | "editor.detectIndentation": false, 5 | "editor.rulers": [120], 6 | "editor.matchBrackets": "always", 7 | "debug.javascript.autoAttachFilter": "smart", 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": true, 10 | }, 11 | "eslint.format.enable": true, 12 | "bracketPairColorizer.colorMode": "Consecutive", 13 | "bracketPairColorizer.forceUniqueOpeningColor": true, 14 | "bracketPairColorizer.showBracketsInGutter": true, 15 | "window.title": "${activeEditorShort}${separator}${rootName} [kibibit]", 16 | "debug.javascript.terminalOptions": { 17 | "skipFiles": [ 18 | "/**" 19 | ] 20 | }, 21 | "svg.preview.background": "black", 22 | "markdown.preview.scrollEditorWithPreview": false, 23 | "markdown.preview.scrollPreviewWithEditor": false, 24 | "todotodohighlight.isEnable": true, 25 | "todohighlight.keywordsPattern": "TODO(@\\w+?)?:", 26 | "todohighlight.defaultStyle": { 27 | "color": "black", 28 | "backgroundColor": "rgba(255, 221, 87, .8)", 29 | "overviewRulerColor": "rgba(255, 221, 87, .8)", 30 | "fontWeight": "bold", 31 | "borderRadius": "2px", 32 | "isWholeLine": true, 33 | } 34 | } -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | - beta 10 | 11 | jobs: 12 | run-linters: 13 | name: Run linters Server 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Commit 18 | uses: actions/checkout@v2 19 | with: 20 | fetch-depth: 0 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v1 23 | with: 24 | node-version: 14 25 | - name: Cache node modules 26 | uses: actions/cache@v2 27 | env: 28 | cache-name: cache-node-modules 29 | with: 30 | path: '**/node_modules' 31 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 32 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 33 | - name: Install Dependencies 34 | run: npm install 35 | - name: Run linters 36 | uses: wearerequired/lint-action@v1.9.0 37 | with: 38 | # github_token: ${{ secrets.BOT_TOKEN }} 39 | # Enable linters 40 | eslint: true 41 | # Eslint options 42 | # eslint_dir: server/ 43 | eslint_extensions: ts -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | - beta 7 | jobs: 8 | release: 9 | name: Release 10 | runs-on: ubuntu-18.04 11 | steps: 12 | - name: Checkout Commit 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 16 | token: ${{ secrets.BOT_TOKEN }} 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14 21 | npm-version: 8 22 | - name: Cache node modules 23 | uses: actions/cache@v2 24 | env: 25 | cache-name: cache-node-modules 26 | with: 27 | path: '**/node_modules' 28 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 29 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 30 | - name: Install Dependencies 31 | run: npm install 32 | - name: Build 33 | run: npm run build --if-present 34 | - name: Release 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }} 37 | GIT_AUTHOR_NAME: k1b1b0t 38 | GIT_AUTHOR_EMAIL: k1b1b0t@kibibit.io 39 | GIT_COMMITTER_NAME: k1b1b0t 40 | GIT_COMMITTER_EMAIL: k1b1b0t@kibibit.io 41 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | run: npm run semantic-release -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | workflow_dispatch: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - beta 9 | 10 | jobs: 11 | serverunittest: 12 | name: Tests 13 | runs-on: ubuntu-18.04 14 | steps: 15 | - name: Checkout Commit 16 | uses: actions/checkout@v2 17 | with: 18 | fetch-depth: 0 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: 14 23 | - name: Cache node modules 24 | uses: actions/cache@v2 25 | env: 26 | cache-name: cache-node-modules 27 | with: 28 | path: '**/node_modules' 29 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.lock') }} 31 | - name: Install Dependencies 32 | run: npm install 33 | - name: Run Tests 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 37 | run: npm run test:cov 38 | - name: Archive test results & coverage 39 | uses: actions/upload-artifact@v2 40 | with: 41 | name: tests 42 | path: test-results 43 | - name: Upload coverage to Codecov 44 | uses: codecov/codecov-action@v1 45 | with: 46 | token: ${{ secrets.CODECOV_TOKEN }} 47 | directory: ./test-results/coverage 48 | fail_ci_if_error: true -------------------------------------------------------------------------------- /src/__snapshots__/disallow-master-commits.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: alpha 1`] = `"Should not commit directly to alpha branch when working locally"`; 4 | 5 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: beta 1`] = `"Should not commit directly to beta branch when working locally"`; 6 | 7 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: dev 1`] = `"Should not commit directly to dev branch when working locally"`; 8 | 9 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: develop 1`] = `"Should not commit directly to develop branch when working locally"`; 10 | 11 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: main 1`] = `"Should not commit directly to main branch when working locally"`; 12 | 13 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: master 1`] = `"Should not commit directly to master branch when working locally"`; 14 | 15 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: next 1`] = `"Should not commit directly to next branch when working locally"`; 16 | 17 | exports[`Disallow Master Branch Commits should DISALLOW commits if on some main branch main branch: staging 1`] = `"Should not commit directly to staging branch when working locally"`; 18 | -------------------------------------------------------------------------------- /src/__snapshots__/prune-merged-branches.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Prune Merged Branches should do nothing if no merged branches found 1`] = `"PROJECT IS CLEAN! WELL DONE!"`; 4 | 5 | exports[`Prune Merged Branches should move to another branch if needed 1`] = ` 6 | "╔═════════════════════╤════════════════════════════╤══════════════════════╗ 7 | ║ Branch Name │ Origin Branch Name │ Origin Branch Status ║ 8 | ╟─────────────────────┼────────────────────────────┼──────────────────────╢ 9 | ║ release/fix-release │ origin/release/fix-release │ GONE ║ 10 | ╚═════════════════════╧════════════════════════════╧══════════════════════╝ 11 | " 12 | `; 13 | 14 | exports[`Prune Merged Branches should move to another branch if needed 2`] = `"trying to delete checkedout branch release/fix-release. moving to main"`; 15 | 16 | exports[`Prune Merged Branches should print branches failed to delete 1`] = `"release/fix-release: DELETED"`; 17 | 18 | exports[`Prune Merged Branches should print branches failed to delete 2`] = `"main: FAILED"`; 19 | 20 | exports[`Prune Merged Branches should print branches that are gone 1`] = ` 21 | "╔══════════════════════════╤═════════════════════════════════╤══════════════════════╗ 22 | ║ Branch Name │ Origin Branch Name │ Origin Branch Status ║ 23 | ╟──────────────────────────┼─────────────────────────────────┼──────────────────────╢ 24 | ║ release/fix-release │ origin/release/fix-release │ GONE ║ 25 | ╟──────────────────────────┼─────────────────────────────────┼──────────────────────╢ 26 | ║ release/new-main-release │ origin/release/new-main-release │ GONE ║ 27 | ╚══════════════════════════╧═════════════════════════════════╧══════════════════════╝ 28 | " 29 | `; 30 | 31 | exports[`Prune Merged Branches should print branches that are gone 2`] = `"DONE"`; 32 | 33 | exports[`Prune Merged Branches should print branches that are gone 3`] = `"release/fix-release: DELETED"`; 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | achievibit changelog 2 | 3 | # [1.0.0-beta.5](https://github.com/Kibibit/dev-tools/compare/v1.0.0-beta.4...v1.0.0-beta.5) (2022-04-04) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * **security:** run security audit and fix problems ([9290f86](https://github.com/Kibibit/dev-tools/commit/9290f86bd5293dbe369cfded3046b20ef459b510)) 9 | 10 | # [1.0.0-beta.4](https://github.com/Kibibit/dev-tools/compare/v1.0.0-beta.3...v1.0.0-beta.4) (2021-11-12) 11 | 12 | 13 | ### Features 14 | 15 | * **scripts:** can accept a list of branches to disallow ([7e88c49](https://github.com/Kibibit/dev-tools/commit/7e88c4910e127bd22672737a69edeed0cabfac0a)) 16 | * **module:** change code to run as module ([1219701](https://github.com/Kibibit/dev-tools/commit/121970194408e3215679167514767f6cd2294b23)) 17 | 18 | # [1.0.0-beta.3](https://github.com/Kibibit/dev-tools/compare/v1.0.0-beta.2...v1.0.0-beta.3) (2021-11-02) 19 | 20 | 21 | ### Features 22 | 23 | * **tests:** start covering with tests ([b9a9041](https://github.com/Kibibit/dev-tools/commit/b9a90419c4e391d2fde8d783133e769868ead1de)) 24 | 25 | # [1.0.0-beta.2](https://github.com/Kibibit/dev-tools/compare/v1.0.0-beta.1...v1.0.0-beta.2) (2021-11-01) 26 | 27 | 28 | ### Bug Fixes 29 | 30 | * **release:** add lib files ([3eaa4c1](https://github.com/Kibibit/dev-tools/commit/3eaa4c1a0f2a97c02cbdc7e7a25b782a22d48e96)) 31 | 32 | # 1.0.0-beta.1 (2021-11-01) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * **package:** don't run husky on semantic release ([c11468b](https://github.com/Kibibit/dev-tools/commit/c11468bc2ae1c9a50372c9c392852f5564f047cf)) 38 | * **github:** remove caching ([6801341](https://github.com/Kibibit/dev-tools/commit/6801341bdc605dcb395bfdd0c4187c9af1c83b81)) 39 | * **release:** remove package-lock to fix npm installing on pipelines ([f6e7e7b](https://github.com/Kibibit/dev-tools/commit/f6e7e7b02ab13825044351b035627877d06b1590)) 40 | 41 | 42 | ### Features 43 | 44 | * **release:** everything needed for a release ([65f9e6e](https://github.com/Kibibit/dev-tools/commit/65f9e6e31cecf845b400856ba2bc64fa9cdb244d)) 45 | -------------------------------------------------------------------------------- /src/disallow-master-commits.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import mockConsole from 'jest-mock-console'; 3 | import { mockProcessExit } from 'jest-mock-process'; 4 | import simpleGit from 'simple-git/promise'; 5 | 6 | import { disallowMainBranchesCommits } from './disallow-master-commits'; 7 | 8 | describe('Disallow Master Branch Commits', () => { 9 | let mockExit; 10 | let restoreConsole; 11 | 12 | beforeEach(() => { 13 | jest.clearAllMocks(); 14 | mockExit = mockProcessExit(); 15 | restoreConsole = mockConsole(); 16 | (simpleGit as any).clearMocks(); 17 | }); 18 | 19 | afterEach(() => { 20 | mockExit.mockRestore(); 21 | restoreConsole(); 22 | }); 23 | 24 | it('should allow commits if on normal branch', async () => { 25 | await disallowMainBranchesCommits(); 26 | expect(mockExit).toHaveBeenCalledWith(0); 27 | }); 28 | 29 | describe('should DISALLOW commits if on some main branch', () => { 30 | test.each([ 31 | ['master'], 32 | ['main'], 33 | ['develop'], 34 | ['dev'], 35 | ['staging'], 36 | ['next'], 37 | ['beta'], 38 | ['alpha'] 39 | ])( 40 | 'main branch: %s', 41 | async (branchName) => { 42 | (simpleGit as any).branchReturnObj = mockMainBranch(branchName); 43 | await disallowMainBranchesCommits(); 44 | expect(mockExit).toHaveBeenCalledWith(1); 45 | expect(console.log).toHaveBeenCalledTimes(1); 46 | expect((console.log as jest.Mock).mock.calls[0][0]) 47 | .toMatchSnapshot(); 48 | } 49 | ); 50 | }); 51 | }); 52 | 53 | function mockMainBranch(name = 'main', isCurrent = true) { 54 | return { 55 | all: [ name ], 56 | branches: { 57 | main: { 58 | current: false, 59 | name: name, 60 | commit: 'cd2ec48', 61 | label: '[origin/main: behind 16] ci(github): add pipelines' 62 | }, 63 | 'release/fix-release': { 64 | current: false, 65 | name: 'release/fix-release', 66 | commit: '8f64df7', 67 | label: '[origin/release/fix-release: gone] blah' 68 | } 69 | }, 70 | current: isCurrent ? name : 'release/fix-release', 71 | detached: false 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | test-results 3 | 4 | .DS_Store 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | .pnpm-debug.log* 14 | 15 | # Diagnostic reports (https://nodejs.org/api/report.html) 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | 18 | # Runtime data 19 | pids 20 | *.pid 21 | *.seed 22 | *.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | lib-cov 26 | 27 | # Coverage directory used by tools like istanbul 28 | coverage 29 | *.lcov 30 | 31 | # nyc test coverage 32 | .nyc_output 33 | 34 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 35 | .grunt 36 | 37 | # Bower dependency directory (https://bower.io/) 38 | bower_components 39 | 40 | # node-waf configuration 41 | .lock-wscript 42 | 43 | # Compiled binary addons (https://nodejs.org/api/addons.html) 44 | build/Release 45 | 46 | # Dependency directories 47 | node_modules/ 48 | jspm_packages/ 49 | 50 | # Snowpack dependency directory (https://snowpack.dev/) 51 | web_modules/ 52 | 53 | # TypeScript cache 54 | *.tsbuildinfo 55 | 56 | # Optional npm cache directory 57 | .npm 58 | 59 | # Optional eslint cache 60 | .eslintcache 61 | 62 | # Microbundle cache 63 | .rpt2_cache/ 64 | .rts2_cache_cjs/ 65 | .rts2_cache_es/ 66 | .rts2_cache_umd/ 67 | 68 | # Optional REPL history 69 | .node_repl_history 70 | 71 | # Output of 'npm pack' 72 | *.tgz 73 | 74 | # Yarn Integrity file 75 | .yarn-integrity 76 | 77 | # dotenv environment variables file 78 | .env 79 | .env.test 80 | .env.production 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # Serverless directories 104 | .serverless/ 105 | 106 | # FuseBox cache 107 | .fusebox/ 108 | 109 | # DynamoDB Local files 110 | .dynamodb/ 111 | 112 | # TernJS port file 113 | .tern-port 114 | 115 | # Stores VSCode versions used for testing VSCode extensions 116 | .vscode-test 117 | 118 | # yarn v2 119 | .yarn/cache 120 | .yarn/unplugged 121 | .yarn/build-state.yml 122 | .yarn/install-state.gz 123 | .pnp.* 124 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: [ 5 | './tsconfig.eslint.json' 6 | ], 7 | sourceType: 'module', 8 | }, 9 | plugins: [ 10 | '@typescript-eslint/eslint-plugin', 11 | 'unused-imports', 12 | 'simple-import-sort', 13 | 'import' 14 | ], 15 | extends: [ 16 | 'plugin:@typescript-eslint/eslint-recommended', 17 | 'plugin:@typescript-eslint/recommended', 18 | ], 19 | ignorePatterns: [ 20 | '.eslintrc.js', 21 | '**/*.event.ts' 22 | ], 23 | root: true, 24 | env: { 25 | node: true, 26 | jest: true, 27 | }, 28 | rules: { 29 | 'unused-imports/no-unused-imports': 'error', 30 | 'simple-import-sort/imports': ['error', { 31 | groups: [ 32 | // 1. built-in node.js modules 33 | [`^(${require("module").builtinModules.join("|")})(/|$)`], 34 | // 2.1. package that start without @ 35 | // 2.2. package that start with @ 36 | ['^\\w', '^@\\w'], 37 | // 3. @nestjs packages 38 | ['^@nestjs\/'], 39 | // 4. @kibibit external packages 40 | ['^@kibibit\/'], 41 | // 5. Internal kibibit packages (inside this project) 42 | ['^@kb-'], 43 | // 6. Parent imports. Put `..` last. 44 | // Other relative imports. Put same-folder imports and `.` last. 45 | ["^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], 46 | // 7. Side effect imports. 47 | // https://riptutorial.com/javascript/example/1618/importing-with-side-effects 48 | ["^\\u0000"] 49 | ] 50 | }], 51 | 'import/first': 'error', 52 | 'import/newline-after-import': 'error', 53 | 'import/no-duplicates': 'error', 54 | 'eol-last': [ 2, 'windows' ], 55 | 'comma-dangle': [ 'error', 'never' ], 56 | 'max-len': [ 'error', { 'code': 80, "ignoreComments": true } ], 57 | 'quotes': ["error", "single"], 58 | '@typescript-eslint/no-empty-interface': 'error', 59 | '@typescript-eslint/member-delimiter-style': 'error', 60 | '@typescript-eslint/explicit-function-return-type': 'off', 61 | '@typescript-eslint/explicit-module-boundary-types': 'off', 62 | '@typescript-eslint/naming-convention': [ 63 | "error", 64 | { 65 | "selector": "interface", 66 | "format": ["PascalCase"], 67 | "custom": { 68 | "regex": "^I[A-Z]", 69 | "match": true 70 | } 71 | } 72 | ], 73 | "semi": "off", 74 | "@typescript-eslint/semi": ["error"] 75 | } 76 | }; 77 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import inquirer from 'inquirer'; 2 | import simpleGit from 'simple-git/promise'; 3 | 4 | jest.mock('inquirer', () => ({ 5 | prompt: jest.fn().mockReturnValue(Promise.resolve({ 6 | whatToDo: 'prune all gone branches' 7 | })) 8 | })); 9 | 10 | jest.mock('simple-git/promise', () => { 11 | const DEFAULT_RETURN = { 12 | all: [ 13 | 'beta', 14 | 'feature/improve-things', 15 | 'fix-release-files', 16 | 'main', 17 | 'release/fix-release', 18 | 'release/new-main-release' 19 | ], 20 | branches: { 21 | beta: { 22 | current: false, 23 | name: 'beta', 24 | commit: '3eaa4c1', 25 | label: '[origin/beta: behind 2] fix(release): add lib files' 26 | }, 27 | 'feature/improve-things': { 28 | current: true, 29 | name: 'feature/improve-things', 30 | commit: '94337fd', 31 | label: 'chore(release): 1.0.0-beta.2 [skip ci]' 32 | }, 33 | 'fix-release-files': { 34 | current: false, 35 | name: 'fix-release-files', 36 | commit: '3eaa4c1', 37 | label: 'fix(release): add lib files' 38 | }, 39 | main: { 40 | current: false, 41 | name: 'main', 42 | commit: 'cd2ec48', 43 | label: '[origin/main: behind 16] ci(github): add pipelines' 44 | }, 45 | 'release/fix-release': { 46 | current: false, 47 | name: 'release/fix-release', 48 | commit: '8f64df7', 49 | label: '[origin/release/fix-release: gone] message' 50 | }, 51 | 'release/new-main-release': { 52 | current: false, 53 | name: 'release/new-main-release', 54 | commit: 'a71bc34', 55 | label: '[origin/release/new-main-release: gone] message' 56 | } 57 | }, 58 | current: 'feature/improve-things', 59 | detached: false 60 | }; 61 | 62 | const DELETED_BRANCHES = { 63 | all: [{ 64 | branch: 'release/fix-release', 65 | success: true 66 | }] 67 | }; 68 | 69 | const simplePromiseBranchMock = jest.fn() 70 | .mockImplementation(() => { 71 | return Promise.resolve(simplePromise.branchReturnObj); 72 | }); 73 | const simpleDeleteLocalBranchesMock = jest.fn() 74 | .mockImplementation(() => { 75 | return Promise.resolve(simplePromise.deleteReturnObj); 76 | }); 77 | const simplePromise = () => ({ 78 | branch: simplePromiseBranchMock, 79 | deleteLocalBranches: simpleDeleteLocalBranchesMock, 80 | fetch: () => jest.fn().mockImplementation(() => console.log('fetched!')) 81 | }); 82 | simplePromise.branchReturnObj = DEFAULT_RETURN; 83 | simplePromise.deleteReturnObj = DELETED_BRANCHES; 84 | simplePromise.clearMocks = () => { 85 | simplePromise.branchReturnObj = DEFAULT_RETURN; 86 | simplePromise.deleteReturnObj = DELETED_BRANCHES; 87 | simplePromiseBranchMock.mockClear(); 88 | simpleDeleteLocalBranchesMock.mockClear(); 89 | }; 90 | 91 | return simplePromise; 92 | }); 93 | 94 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 95 | const simpleGitMock = simpleGit; 96 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 97 | const inquirerMock = inquirer; 98 | -------------------------------------------------------------------------------- /src/prune-merged-branches.spec.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | import mockConsole from 'jest-mock-console'; 3 | import { mockProcessExit } from 'jest-mock-process'; 4 | import simpleGit from 'simple-git/promise'; 5 | import strip from 'strip-ansi'; 6 | 7 | import { pruneMergedBranches } from './prune-merged-branches'; 8 | 9 | describe('Prune Merged Branches', () => { 10 | let mockExit; 11 | let restoreConsole; 12 | 13 | beforeEach(() => { 14 | jest.clearAllMocks(); 15 | mockExit = mockProcessExit(); 16 | restoreConsole = mockConsole(); 17 | (simpleGit as any).clearMocks(); 18 | }); 19 | 20 | afterEach(() => { 21 | mockExit.mockRestore(); 22 | restoreConsole(); 23 | }); 24 | 25 | it('should print branches that are gone', async () => { 26 | // restoreConsole(); 27 | await pruneMergedBranches(); 28 | expect(console.log).toHaveBeenCalledTimes(3); 29 | expect(strip((console.log as jest.Mock).mock.calls[0][0])) 30 | .toMatchSnapshot(); 31 | expect(strip((console.log as jest.Mock).mock.calls[1][0])) 32 | .toMatchSnapshot(); 33 | expect(strip((console.log as jest.Mock).mock.calls[2][0])) 34 | .toMatchSnapshot(); 35 | }); 36 | 37 | it('should print branches failed to delete', async () => { 38 | // restoreConsole(); 39 | (simpleGit as any).deleteReturnObj = { 40 | all: [ 41 | { 42 | branch: 'release/fix-release', 43 | success: true 44 | }, 45 | { 46 | branch: 'main', 47 | success: false 48 | } 49 | ] 50 | }; 51 | await pruneMergedBranches(); 52 | expect(console.log).toHaveBeenCalledTimes(4); 53 | expect(strip((console.log as jest.Mock).mock.calls[2][0])) 54 | .toMatchSnapshot(); 55 | expect(strip((console.log as jest.Mock).mock.calls[3][0])) 56 | .toMatchSnapshot(); 57 | }); 58 | 59 | it('should do nothing if no merged branches found', async () => { 60 | (simpleGit as any).branchReturnObj = { 61 | all: ['main'], 62 | branches: { 63 | main: { 64 | current: false, 65 | name: 'main', 66 | commit: 'cd2ec48', 67 | label: '[origin/main: behind 16] ci(github): add pipelines' 68 | } 69 | }, 70 | current: 'main', 71 | detached: false 72 | }; 73 | await pruneMergedBranches(); 74 | expect(console.log).toHaveBeenCalledTimes(1); 75 | expect(strip((console.log as jest.Mock).mock.calls[0][0])) 76 | .toMatchSnapshot(); 77 | }); 78 | 79 | it('should move to another branch if needed', async () => { 80 | // restoreConsole(); 81 | (simpleGit as any).branchReturnObj = { 82 | all: ['main'], 83 | branches: { 84 | main: { 85 | current: false, 86 | name: 'main', 87 | commit: 'cd2ec48', 88 | label: '[origin/main: behind 16] ci(github): add pipelines' 89 | }, 90 | 'release/fix-release': { 91 | current: false, 92 | name: 'release/fix-release', 93 | commit: '8f64df7', 94 | label: '[origin/release/fix-release: gone] blah' 95 | } 96 | }, 97 | current: 'release/fix-release', 98 | detached: false 99 | }; 100 | await pruneMergedBranches(); 101 | expect(console.log).toHaveBeenCalledTimes(1); 102 | expect(strip((console.log as jest.Mock).mock.calls[0][0])) 103 | .toMatchSnapshot(); 104 | expect(console.warn).toHaveBeenCalledTimes(1); 105 | expect(strip((console.warn as jest.Mock).mock.calls[0][0])) 106 | .toMatchSnapshot(); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@kibibit/dev-tools", 3 | "version": "1.0.0-beta.5", 4 | "description": "some useful scripts", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "files": [ 8 | "/lib", 9 | "/src" 10 | ], 11 | "bin": { 12 | "@kibibit/prune-merged": "bin/prune-merged-branches", 13 | "kb-prune-merged": "bin/prune-merged-branches", 14 | "@kibibit/disallow-master-commits": "bin/disallow-master-commits", 15 | "kb-disallow-master-commits": "bin/disallow-master-commits" 16 | }, 17 | "keywords": [ 18 | "cli" 19 | ], 20 | "scripts": { 21 | "disallow-master-commits": "bin/disallow-master-commits", 22 | "prebuild": "rimraf lib", 23 | "build": "tsc", 24 | "semantic-release:setup": "semantic-release-cli setup", 25 | "lint": "eslint -c ./.eslintrc.js \"{src,apps,libs,test}/**/*.ts\"", 26 | "lint:fix": "eslint -c ./.eslintrc.js \"{src,apps,libs,test}/**/*.ts\" --fix", 27 | "semantic-release": "cross-env HUSKY=0 semantic-release", 28 | "test": "jest", 29 | "test:watch": "jest --watch --verbose", 30 | "test:cov": "jest --coverage --verbose", 31 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 32 | "contributors:all": "cross-env HUSKY=0 node ./tools/get-all-contributors.js", 33 | "contributors:add": "cross-env HUSKY=0 all-contributors add", 34 | "contributors:generate": "cross-env HUSKY=1 all-contributors generate", 35 | "prepare": "husky install" 36 | }, 37 | "author": "thatkookooguy ", 38 | "license": "MIT", 39 | "dependencies": { 40 | "cli-table": "^0.3.11", 41 | "inquirer": "^8.2.2", 42 | "lodash": "^4.17.21", 43 | "simple-git": "^3.5.0", 44 | "yargs": "^17.4.0" 45 | }, 46 | "devDependencies": { 47 | "@commitlint/cli": "^16.2.3", 48 | "@commitlint/config-angular": "^16.2.3", 49 | "@commitlint/config-conventional": "^16.2.1", 50 | "@semantic-release/changelog": "^6.0.1", 51 | "@semantic-release/commit-analyzer": "^9.0.2", 52 | "@semantic-release/git": "^10.0.1", 53 | "@semantic-release/github": "^8.0.4", 54 | "@semantic-release/npm": "^9.0.1", 55 | "@semantic-release/release-notes-generator": "^10.0.3", 56 | "@types/cli-table": "^0.3.0", 57 | "@types/inquirer": "^8.2.1", 58 | "@types/lodash": "^4.14.181", 59 | "@typescript-eslint/eslint-plugin": "^5.17.0", 60 | "@typescript-eslint/parser": "^5.17.0", 61 | "all-contributors-cli": "^6.20.0", 62 | "commitizen": "^4.2.4", 63 | "cross-env": "^7.0.3", 64 | "cz-conventional-changelog-emoji": "^0.1.0", 65 | "eslint": "^8.12.0", 66 | "eslint-plugin-import": "^2.25.4", 67 | "eslint-plugin-simple-import-sort": "^7.0.0", 68 | "eslint-plugin-unused-imports": "^2.0.0", 69 | "husky": "^7.0.4", 70 | "jest": "^27.5.1", 71 | "jest-mock-console": "^1.2.3", 72 | "jest-mock-process": "^1.4.1", 73 | "jest-stare": "^2.3.0", 74 | "semantic-release": "^19.0.2", 75 | "ts-jest": "^27.1.4", 76 | "ts-node": "^10.7.0", 77 | "typescript": "^4.6.3" 78 | }, 79 | "config": { 80 | "commitizen": { 81 | "path": "./node_modules/cz-conventional-changelog-emoji" 82 | } 83 | }, 84 | "publishConfig": { 85 | "access": "public" 86 | }, 87 | "release": { 88 | "branches": [ 89 | { 90 | "name": "main" 91 | }, 92 | { 93 | "name": "beta", 94 | "channel": "beta", 95 | "prerelease": "beta" 96 | } 97 | ], 98 | "plugins": [ 99 | [ 100 | "@semantic-release/commit-analyzer", 101 | { 102 | "preset": "angular", 103 | "parserOpts": { 104 | "noteKeywords": [ 105 | "BREAKING CHANGE", 106 | "BREAKING CHANGES", 107 | "BREAKING" 108 | ] 109 | } 110 | } 111 | ], 112 | [ 113 | "@semantic-release/release-notes-generator", 114 | { 115 | "preset": "angular", 116 | "parserOpts": { 117 | "noteKeywords": [ 118 | "BREAKING CHANGE", 119 | "BREAKING CHANGES", 120 | "BREAKING" 121 | ] 122 | }, 123 | "writerOpts": { 124 | "commitsSort": [ 125 | "subject", 126 | "scope" 127 | ] 128 | } 129 | } 130 | ], 131 | [ 132 | "@semantic-release/changelog", 133 | { 134 | "changelogFile": "CHANGELOG.md", 135 | "changelogTitle": "achievibit changelog" 136 | } 137 | ], 138 | [ 139 | "@semantic-release/git", 140 | { 141 | "assets": [ 142 | "CHANGELOG.md" 143 | ] 144 | } 145 | ], 146 | "@semantic-release/npm", 147 | "@semantic-release/git", 148 | "@semantic-release/github" 149 | ] 150 | }, 151 | "repository": { 152 | "type": "git", 153 | "url": "https://github.com/Kibibit/dev-tools.git" 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | @kibibit/dev-tools 5 |

6 |

7 |

8 | 9 |

10 |

11 | 12 | 13 | 14 | 15 | 16 | Build 17 | 18 | 19 | Tests 20 | 21 | 22 | 23 | All Contributors 24 | 25 |

26 |

27 | Scripts to make development easier 28 |

29 |
30 | 31 | ## Usage 32 | 33 | Install globally 34 | ```bash 35 | npm install --global @kibibit/dev-tools 36 | ``` 37 | or locally 38 | ```bash 39 | npm install --save-dev @kibibit/dev-tools 40 | ``` 41 | Then, go into a git project folder and run 42 | ```bash 43 | kb-prune-merged 44 | ``` 45 | to prune local branches that merged and deleted on cloud 46 | 47 | Or use this command in your git hooks to prevent accidental commits to protected branches: 48 | ```bash 49 | kb-disallow-master-commits 50 | ``` 51 | for example, [husky](https://github.com/typicode/husky) 52 | 53 | ### package.json 54 | ```json 55 | /* ... */ 56 | "scripts": { 57 | "disallow-master-commits": "kb-disallow-master-commits" 58 | } 59 | ``` 60 | Then, add it as a pre-commit git hook: 61 | ```bash 62 | npx husky add .husky/pre-commit "npm run disallow-master-commits" 63 | git add .husky/pre-commit 64 | ``` 65 | 66 | 76 | 77 | ## Contributors ✨ 78 | 79 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 |

Neil Kalman

💻 📖 🎨 🚧 🚇 ⚠️
88 | 89 | 90 | 91 | 92 | 93 | 94 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome! 95 | 96 |
Logo made by Freepik from www.flaticon.com
97 |
98 | 99 | ## Stay in touch 100 | 101 | - Author - [Neil Kalman](https://github.com/thatkookooguy) 102 | - Website - [https://github.com/kibibit](https://github.com/kibibit) 103 | - StackOverflow - [thatkookooguy](https://stackoverflow.com/users/1788884/thatkookooguy) 104 | - Twitter - [@thatkookooguy](https://twitter.com/thatkookooguy) 105 | - Twitter - [@kibibit_opensrc](https://twitter.com/kibibit_opensrc) -------------------------------------------------------------------------------- /src/prune-merged-branches.ts: -------------------------------------------------------------------------------- 1 | import Table from 'cli-table'; 2 | import inquirer from 'inquirer'; 3 | import { chain,trim } from 'lodash'; 4 | import { BranchSummaryBranch } from 'simple-git'; 5 | import simpleGit, { BranchSummary } from 'simple-git/promise'; 6 | 7 | import { MAIN_BRANCHES } from './main-branch'; 8 | 9 | interface IExtraData { isMerged?: boolean; remoteName?: string } 10 | 11 | type BranchWithExtras = BranchSummaryBranch & IExtraData; 12 | 13 | const git = simpleGit(); 14 | const remoteInfoRegex = /^\[(.*?)\]\s/g; 15 | 16 | const chars = { 17 | 'top': '═', 'top-mid': '╤', 'top-left': '╔', 'top-right': '╗' 18 | , 'bottom': '═', 'bottom-mid': '╧', 'bottom-left': '╚', 'bottom-right': '╝' 19 | , 'left': '║', 'left-mid': '╟', 'mid': '─', 'mid-mid': '┼' 20 | , 'right': '║', 'right-mid': '╢', 'middle': '│' 21 | }; 22 | 23 | export async function pruneMergedBranches() { 24 | try { 25 | await git.fetch(['-p']); // prune? is it necassery? 26 | const branchSummaryResult = await git.branch(['-vv']); 27 | const localBranches = branchSummaryResult.branches; 28 | const localBranchesWithGoneRemotes: BranchWithExtras[] = 29 | chain(localBranches) 30 | .filter((item) => !MAIN_BRANCHES.includes(item.name)) 31 | .forEach((item: BranchWithExtras) => { 32 | // console.log('an item appeared!', item); 33 | const remoteInfo = item.label.match(remoteInfoRegex); 34 | 35 | if (remoteInfo) { 36 | const parsedRemoteInfo = trim(remoteInfo[0], '[] '); 37 | const isMerged = parsedRemoteInfo.endsWith(': gone'); 38 | const remoteBranchName = parsedRemoteInfo.replace(': gone', ''); 39 | 40 | item.isMerged = isMerged; 41 | item.remoteName = remoteBranchName; 42 | } 43 | }) 44 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 45 | .filter((item) => (item as any).isMerged) 46 | .value(); 47 | const branchNames = chain(localBranchesWithGoneRemotes).map('name').value(); 48 | 49 | if (!branchNames.length) { 50 | console.log('PROJECT IS CLEAN! WELL DONE!'); 51 | process.exit(0); 52 | return; 53 | } 54 | 55 | // interaction! 56 | const table = new Table({ 57 | head: ['Branch Name', 'Origin Branch Name', 'Origin Branch Status'], 58 | chars 59 | }); 60 | 61 | localBranchesWithGoneRemotes 62 | .forEach((item) => table.push([item.name, item.remoteName, 'GONE'])); 63 | 64 | console.log(`${table.toString()}\n`); 65 | 66 | const ACTION_ANSWERS = { 67 | PRUNE_ALL: 'prune all gone branches', 68 | SELECT_BRANCHES: 'Selected Individual branches to delete' 69 | }; 70 | 71 | const answers = await inquirer 72 | .prompt([ 73 | { 74 | type: 'list', 75 | name: 'whatToDo', 76 | message: [ 77 | 'These branches have been deleted from the origin. ', 78 | 'What do you want to do with them?' 79 | ].join(''), 80 | choices: chain(ACTION_ANSWERS).values().value() 81 | } 82 | ]); 83 | 84 | if (answers.whatToDo === ACTION_ANSWERS.PRUNE_ALL) { 85 | await moveToAnotherBranchIfNeeded(branchSummaryResult, branchNames); 86 | 87 | const result = await git.deleteLocalBranches(branchNames, true); 88 | console.log('DONE'); 89 | chain(result.all) 90 | .map((item) => getStatusString(item)) 91 | .forEach((item) => console.log(item)) 92 | .value(); 93 | return; 94 | } 95 | 96 | if (answers.whatToDo === ACTION_ANSWERS.SELECT_BRANCHES) { 97 | const answers = await inquirer 98 | .prompt([ 99 | { 100 | type: 'checkbox', 101 | message: [ 102 | 'These branches have been deleted from the origin. ', 103 | 'Do you want to prune them?' 104 | ].join(''), 105 | name: 'pruneBranches', 106 | choices: branchNames 107 | } 108 | ]); 109 | 110 | await moveToAnotherBranchIfNeeded(branchSummaryResult, branchNames); 111 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 112 | const deleteBranchPromises: Promise[] = answers.pruneBranches 113 | .map((branchName: string) => git.deleteLocalBranch(branchName, true)); 114 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 115 | const result: any = await Promise.all(deleteBranchPromises); 116 | console.log('DONE'); 117 | console.log(result); 118 | chain(result.all) 119 | .map((item) => getStatusString(item)) 120 | .forEach((item) => console.log(item)); 121 | return; 122 | } 123 | } catch (err) { 124 | console.error(err); 125 | process.exit(1); 126 | return; 127 | } 128 | } 129 | 130 | async function moveToAnotherBranchIfNeeded( 131 | branchSummaryResult: BranchSummary, 132 | branchesToDelete: string[] 133 | ) { 134 | const suspectedMainBranch = branchSummaryResult.all 135 | .find((branchName: string) => MAIN_BRANCHES.includes(branchName)) as string; 136 | const currentCheckedoutBranch = branchSummaryResult.current; 137 | 138 | // console.log('main branch:', suspectedMainBranch); 139 | 140 | if (branchesToDelete.includes(currentCheckedoutBranch)) { 141 | console.warn([ 142 | `trying to delete checkedout branch ${ currentCheckedoutBranch }. `, 143 | `moving to ${ suspectedMainBranch }` 144 | ].join('')); 145 | await git.checkout(suspectedMainBranch); 146 | } 147 | } 148 | 149 | function getStatusString(item: { branch: string; success: boolean }) { 150 | return `${ item.branch }: ${ item.success ? 'DELETED' : 'FAILED' }`; 151 | } 152 | -------------------------------------------------------------------------------- /src/copyright-checker.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | 3 | import { readdirSync, readFileSync, readJsonSync, writeFileSync } from 'fs-extra'; 4 | import checker from 'license-checker'; 5 | import { chain, isArray } from 'lodash'; 6 | import { Align, getMarkdownTable } from 'markdown-table-ts'; 7 | import axios from 'axios'; 8 | import { Octokit } from '@octokit/rest'; 9 | 10 | const file = './dep-licenses'; 11 | 12 | interface ILicense { 13 | name: string; 14 | path: string; 15 | repository: string; 16 | homepage: string; 17 | description: string; 18 | directDep: boolean; 19 | licenses: string; 20 | licenseShield: string; 21 | nameShield: string; 22 | logoShield: string; 23 | logo: string; 24 | fromGitHub: any; 25 | } 26 | 27 | export class CopyrightChecker { 28 | licenses: ILicense[] | Record; 29 | currentPackageData: Record; 30 | onlyDirect: boolean; 31 | checkDependencyCopyrights( 32 | cwd: string, 33 | onlyDirect: boolean = false, 34 | group: boolean = false, 35 | type: 'markdown' | 'plain' = 'markdown' 36 | ) { 37 | this.currentPackageData = readJsonSync(join(cwd, 'package.json')); 38 | this.onlyDirect = onlyDirect; 39 | checker.init( 40 | { 41 | start: cwd, 42 | production: true 43 | }, 44 | async (err, json) => { 45 | if (err) { 46 | console.log(err); //Handle error 47 | } else { 48 | this.licenses = chain(json) 49 | .mapValues((value: ILicense, key) => ({ 50 | name: key, 51 | ...value 52 | })) 53 | .values() 54 | .value() as any as ILicense[]; 55 | 56 | 57 | await Promise.all(this.licenses.map(async (value: ILicense) => { 58 | await this.getPackageExtraData(value); 59 | return value; 60 | })); 61 | 62 | this.licenses = chain(this.licenses) 63 | .filter((license: any) => 64 | !(license as ILicense).name.includes(this.currentPackageData.name) 65 | ) 66 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 67 | .value() as any as ILicense[]; 68 | 69 | this.licenses = group ? 70 | chain(this.licenses) 71 | .groupBy('licenses') 72 | .value() : 73 | this.licenses; 74 | 75 | writeFileSync( 76 | `${file}.${type === 'markdown' ? 'md' : 'txt'}`, 77 | type === 'markdown' ? 78 | this.printLicenseSummary() : 79 | this.printLicenseSummaryPlain() 80 | ); 81 | } 82 | } 83 | ); 84 | } 85 | 86 | printLicenseSummaryPlain() { 87 | return chain(isArray(this.licenses) ? [this.licenses] : this.licenses) 88 | .map((modules: ILicense[], key) => { 89 | const parsedModules = modules 90 | // sort order: 91 | // - direct dependencies first 92 | // - then if they have logo 93 | // - then alphabetically 94 | .sort((a, b) => { 95 | if (a.directDep && !b.directDep) { 96 | return -1; 97 | } 98 | if (!a.directDep && b.directDep) { 99 | return 1; 100 | } 101 | // if (a.logo && !b.logo) { 102 | // return -1; 103 | // } 104 | // if (!a.logo && b.logo) { 105 | // return 1; 106 | // } 107 | return a.name.localeCompare(b.name); 108 | }) 109 | .filter((module: ILicense) => !this.onlyDirect || module.directDep) 110 | .map((module) => [ 111 | module.name 112 | ].join('\n')); 113 | 114 | return parsedModules.length ? 115 | [ 116 | ...(typeof key === 'string' ? [ 117 | '\n-------------------', 118 | `${key} - License Summary`, 119 | '-------------------\n' 120 | ] : []), 121 | ...parsedModules 122 | ].join('\n') : ''; 123 | }) 124 | .value() 125 | .join('\n\n'); 126 | 127 | } 128 | 129 | printLicenseSummary() { 130 | return chain(isArray(this.licenses) ? [this.licenses] : this.licenses) 131 | .map((modules: ILicense[], key) => { 132 | const parsedModules = modules 133 | // sort order: 134 | // - direct dependencies first 135 | // - then if they have logo 136 | // - then alphabetically 137 | .sort((a, b) => { 138 | if (a.directDep && !b.directDep) { 139 | return -1; 140 | } 141 | if (!a.directDep && b.directDep) { 142 | return 1; 143 | } 144 | // if (a.logo && !b.logo) { 145 | // return -1; 146 | // } 147 | // if (!a.logo && b.logo) { 148 | // return 1; 149 | // } 150 | return a.name.localeCompare(b.name); 151 | }) 152 | .filter((module: ILicense) => !this.onlyDirect || module.directDep) 153 | .map((module) => [ 154 | module.logoShield, 155 | [`[![${module.name}](${module.nameShield})](${module.homepage})`, 156 | `![](${ module.licenseShield })`, 157 | module.description].join('
') 158 | ]); 159 | return parsedModules.length ? 160 | [ 161 | ...(typeof key === 'string' ? [`# ${key} - License Summary\n`] : []), 162 | getMarkdownTable({ 163 | table: { 164 | head: ['Packages'], 165 | body: parsedModules 166 | }, 167 | alignment: [Align.Left, Align.Left] 168 | }) 169 | ].join('\n') : ''; 170 | }) 171 | .value() 172 | .join('\n\n'); 173 | } 174 | 175 | async getPackageExtraData(license: Partial): Promise { 176 | const packageDetails = readJsonSync(join(license.path, 'package.json')); 177 | const filesInFolder = readdirSync(license.path); 178 | const readmeFilename = filesInFolder.find((file) => file.toLowerCase().includes('readme')); 179 | const readmeFile = readmeFilename ? readFileSync(join(license.path, readmeFilename), 'utf8') : ''; 180 | license.homepage = packageDetails.homepage || license.repository; 181 | license.description = packageDetails.description; 182 | license.directDep = Object.keys(this.currentPackageData.dependencies).includes(packageDetails.name); 183 | license.licenseShield = this.shieldUrl('License', license.licenses); 184 | let nameSplit = license.name.split('@'); 185 | if (nameSplit.length > 2) { 186 | // join all but last since that's the version number 187 | nameSplit = [ 188 | nameSplit.slice(0, nameSplit.length - 1).join('@'), 189 | nameSplit[nameSplit.length - 1] 190 | ]; 191 | } 192 | license.nameShield = this.shieldUrl(nameSplit[0], nameSplit[1], 'npm', 'CB3837'); 193 | license.logo = await this.getPackageLogo(nameSplit[0], readmeFile); 194 | // remove everything before .com/ in repo url 195 | const repoFullName = license.repository 196 | .replace(/.*\.com\//g, ''); 197 | const owner = repoFullName.split('/')[0]; 198 | const repo = repoFullName.split('/')[1]; 199 | // TODO: fix right limit later and add this back as an option 200 | // license.fromGitHub = await this.getPackageLicenseFromGitHub(owner, repo); 201 | // console.log(license.fromGitHub); 202 | license.logoShield = license.logo ? 203 | `` : 204 | ''; 205 | } 206 | 207 | shieldUrl(label: string, message: string, logo?: string, color?: string): string { 208 | return [ 209 | 'https://img.shields.io/static/v1?', 210 | 'label=', encodeURIComponent(label), 211 | '&message=', encodeURIComponent(message), 212 | // '&style=for-the-badge', 213 | ...(logo ? [`&logo=${ logo }`] : []), 214 | `&color=${ color || 'blue' }` 215 | ].join(''); 216 | } 217 | 218 | async getPackageLogo(name: string, readmeFile: string): Promise { 219 | const logoRegex = new RegExp(`https://.*${ name }.*logo\\.(png|jpg|svg)`, 'gi'); 220 | const logoUrl = readmeFile.match(logoRegex)?.[0]; 221 | if (logoUrl) { 222 | return logoUrl; 223 | } 224 | const url = [ 225 | 'https://simpleicons.org/icons/', 226 | name.replace(/@/g, '-'), 227 | '.svg' 228 | ].join(''); 229 | 230 | // check icon exists 231 | try { 232 | const res = await axios.get(url); 233 | return url; 234 | } 235 | catch (e) { 236 | return null; 237 | } 238 | } 239 | 240 | async getPackageLicenseFromGitHub( 241 | owner: string, 242 | repo: string 243 | ) { 244 | const octokit = new Octokit(); 245 | const license = await octokit.rest.licenses.getForRepo({ 246 | owner, 247 | repo 248 | }); 249 | 250 | return { 251 | ...license.data, 252 | content: Buffer 253 | .from(license.data.content, 'base64') 254 | .toString('ascii') 255 | }; 256 | } 257 | } 258 | --------------------------------------------------------------------------------