├── .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 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
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 |
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 |
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.homepage})`,
156 | ``,
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 |
--------------------------------------------------------------------------------