├── .npmignore ├── .gitignore ├── bin └── git-notify.js ├── docs ├── demo.gif ├── demo_animated.gif └── github-example.png ├── src ├── types.ts ├── git.ts ├── showNotifications.ts └── index.ts ├── .github └── workflows │ ├── size.yml │ └── main.yml ├── LICENSE ├── tsconfig.json ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | -------------------------------------------------------------------------------- /bin/git-notify.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../dist'); 3 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevakallio/git-notify/HEAD/docs/demo.gif -------------------------------------------------------------------------------- /docs/demo_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevakallio/git-notify/HEAD/docs/demo_animated.gif -------------------------------------------------------------------------------- /docs/github-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jevakallio/git-notify/HEAD/docs/github-example.png -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type Lifecycle = 'merge' | 'rewrite' | 'checkout' | 'since'; 2 | 3 | export type Flags = { 4 | simple: boolean | void; 5 | toast: boolean | void; 6 | prefix: string; 7 | color: string; 8 | }; 9 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: size 2 | on: [pull_request] 3 | jobs: 4 | size: 5 | runs-on: ubuntu-latest 6 | env: 7 | CI_JOB_NUMBER: 1 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: andresz1/size-limit-action@v1 11 | with: 12 | github_token: ${{ secrets.GITHUB_TOKEN }} 13 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push] 3 | jobs: 4 | build: 5 | name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }} 6 | 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | node: ['10.x', '12.x', '14.x'] 11 | os: [ubuntu-latest, windows-latest, macOS-latest] 12 | 13 | steps: 14 | - name: Checkout repo 15 | uses: actions/checkout@v2 16 | 17 | - name: Use Node ${{ matrix.node }} 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: ${{ matrix.node }} 21 | 22 | - name: Install deps and build (with cache) 23 | uses: bahmutov/npm-install@v1 24 | 25 | - name: Lint 26 | run: yarn lint 27 | 28 | - name: Test 29 | run: yarn test --ci --coverage --maxWorkers=2 30 | 31 | - name: Build 32 | run: yarn build 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jani Eväkallio 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/git.ts: -------------------------------------------------------------------------------- 1 | import SimpleGit from 'simple-git'; 2 | import log from 'git-raw-commits'; 3 | import { Lifecycle } from './types'; 4 | 5 | const git = SimpleGit(); 6 | 7 | /** 8 | * Gets the second item on the reflog. In post-merge hook, this 9 | * should be the HEAD of the branch before the merge. 10 | * 11 | * If this assumption turns out not to be correct, this may 12 | * under- or over-report notifications 13 | */ 14 | export function getLastRef(): Promise { 15 | return git 16 | .raw('reflog', '--pretty=format:"%h"', '--no-patch', '-1', 'HEAD@{1}') 17 | .then(value => value.replace(/"/g, '')); 18 | } 19 | 20 | export async function getLogStream(lifecycle: Lifecycle, args: string[]) { 21 | switch (lifecycle) { 22 | case 'merge': 23 | // all commits since the previous HEAD on this branch 24 | return log({ from: await getLastRef() }); 25 | 26 | case 'rewrite': 27 | // perhaps not the most accurate method, PRs welcome 28 | return log({ from: 'origin', to: 'HEAD' }); 29 | 30 | case 'checkout': 31 | // post-checkout hook receives old and new branch HEAD as args 32 | const [from, to] = args; 33 | return log({ from, to }); 34 | 35 | case 'since': 36 | // since is our own command, not called by any git hook 37 | return log({ from: args[0] }); 38 | 39 | default: 40 | throw new Error('[git-notify] unsupported git hook: ' + lifecycle); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // see https://www.typescriptlang.org/tsconfig to better understand tsconfigs 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "lib": ["dom", "esnext"], 7 | "importHelpers": true, 8 | // output .d.ts declaration files for consumers 9 | "declaration": true, 10 | // output .js.map sourcemap files for consumers 11 | "sourceMap": true, 12 | // match output dir to input dir. e.g. dist/index instead of dist/src/index 13 | "rootDir": "./src", 14 | // stricter type-checking for stronger correctness. Recommended by TS 15 | "strict": true, 16 | // linter checks for common issues 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": true, 19 | // noUnused* overlap with @typescript-eslint/no-unused-vars, can disable if duplicative 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | // use Node's module resolution algorithm, instead of the legacy TS one 23 | "moduleResolution": "node", 24 | // transpile JSX to React.createElement 25 | "jsx": "react", 26 | // interop between ESM and CJS modules. Recommended by TS 27 | "esModuleInterop": true, 28 | // significant perf increase by skipping checking .d.ts files, particularly those in node_modules. Recommended by TS 29 | "skipLibCheck": true, 30 | // error out if import and file system have a casing mismatch. Recommended by TS 31 | "forceConsistentCasingInFileNames": true, 32 | // `tsdx build` ignores this option, but it is commonly used when type-checking separately with `tsc` 33 | "noEmit": true, 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/showNotifications.ts: -------------------------------------------------------------------------------- 1 | import * as Stream from 'stream'; 2 | import { Flags } from './types'; 3 | import detectNewline from 'detect-newline'; 4 | import boxen from 'boxen'; 5 | import chalk from 'chalk'; 6 | 7 | /** 8 | * Reads given stream of git log messages and prints 9 | * out any lines prefixed with the git-notify tag 10 | */ 11 | export default async function showNotifications( 12 | messageStream: Stream.Readable, 13 | flags: Flags 14 | ) { 15 | let detectedNewline; 16 | 17 | // for each commit message 18 | for await (const chunk of messageStream) { 19 | // read message in stream 20 | const message: string = chunk.toString(); 21 | detectedNewline = detectedNewline || detectNewline(message); 22 | 23 | // for each paragraph in message 24 | const newline = detectedNewline || '\n'; 25 | const paragraphs = message.split(`${newline}${newline}`); 26 | for (const paragraph of paragraphs) { 27 | const notifications = paragraph.split(flags.prefix).slice(1); 28 | 29 | // for each notification on this paragraph 30 | notifications.forEach(notification => { 31 | // display notification 32 | notify(notification.trim(), flags); 33 | }); 34 | } 35 | } 36 | } 37 | 38 | function notify(message: string, flags: Flags) { 39 | const chalkPreset: chalk.ChalkFunction | void = (chalk as any)[flags.color]; 40 | const simpleColor = chalkPreset || chalk.hex(flags.color); 41 | const borderColor = chalkPreset 42 | ? flags.color 43 | : flags.color.startsWith('#') 44 | ? flags.color 45 | : `#${flags.color}`; 46 | 47 | const display = flags.simple 48 | ? simpleColor(message) 49 | : boxen(message, { 50 | padding: 1, 51 | margin: 1, 52 | borderStyle: 'doubleSingle', 53 | borderColor, 54 | float: 'center', 55 | align: 'center', 56 | }); 57 | 58 | // notify to console 59 | console.log(display); 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.3", 3 | "license": "MIT", 4 | "main": "dist/index.js", 5 | "typings": "dist/index.d.ts", 6 | "repository": "https://github.com/jevakallio/git-notify", 7 | "description": "Communicate important updates to your team via git commit messages", 8 | "contributors": [ 9 | "@jevakallio" 10 | ], 11 | "keywords": [ 12 | "git", 13 | "hook", 14 | "message", 15 | "notify", 16 | "husky" 17 | ], 18 | "files": [ 19 | "dist", 20 | "bin" 21 | ], 22 | "engines": { 23 | "node": ">=10" 24 | }, 25 | "bin": { 26 | "git-notify": "bin/git-notify.js" 27 | }, 28 | "scripts": { 29 | "start": "tsdx watch --target=node --format=cjs", 30 | "build": "tsdx build --target=node --format=cjs", 31 | "test": "echo tsdx text", 32 | "lint": "tsdx lint", 33 | "prepare": "yarn build", 34 | "size": "size-limit", 35 | "analyze": "size-limit --why" 36 | }, 37 | "dependencies": { 38 | "boxen": "^5.0.0", 39 | "chalk": "^4.1.0", 40 | "detect-newline": "^3.1.0", 41 | "git-raw-commits": "^2.0.10", 42 | "meow": "^9.0.0", 43 | "simple-git": "^2.35.2" 44 | }, 45 | "peerDependencies": {}, 46 | "husky": { 47 | "hooks": { 48 | "pre-commit": "tsdx lint" 49 | } 50 | }, 51 | "prettier": { 52 | "printWidth": 80, 53 | "semi": true, 54 | "singleQuote": true, 55 | "trailingComma": "es5" 56 | }, 57 | "name": "git-notify", 58 | "author": "Jani Eväkallio", 59 | "module": "dist/git-notify.esm.js", 60 | "size-limit": [ 61 | { 62 | "path": "dist/git-notify.cjs.production.min.js", 63 | "limit": "10 KB" 64 | }, 65 | { 66 | "path": "dist/git-notify.esm.js", 67 | "limit": "10 KB" 68 | } 69 | ], 70 | "devDependencies": { 71 | "@size-limit/preset-small-lib": "^4.9.2", 72 | "@types/git-raw-commits": "^2.0.0", 73 | "husky": "^5.1.1", 74 | "size-limit": "^4.9.2", 75 | "tsdx": "^0.14.1", 76 | "tslib": "^2.1.0", 77 | "typescript": "^4.2.2" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import meow from 'meow'; 2 | import showNotifications from './showNotifications'; 3 | import { getLogStream } from './git'; 4 | import { Lifecycle, Flags } from './types'; 5 | 6 | const cli = meow( 7 | ` 8 | Usage 9 | $ git-notify [options] $GIT_PARAMS 10 | 11 | Methods 12 | since show all notifications since commit 13 | merge run on git pull/merge 14 | rewrite run on git rebase 15 | checkout run on git checkout/switch 16 | 17 | Options 18 | --prefix, -p prefix to look for in commit messages (default: "git-notify:") 19 | --simple, -s show a plain, unboxed notification 20 | --color, -c color of displayed notification 21 | 22 | Examples 23 | $ git-notify since HEAD~5 24 | $ git-notify checkout $GIT_PARAMS --prefix "\@everyone:" 25 | $ git-notify merge $GIT_PARAMS --simple --color "\#ff6f6f" 26 | `, 27 | { 28 | flags: { 29 | toast: { 30 | type: 'boolean', 31 | alias: 't', 32 | }, 33 | simple: { 34 | type: 'boolean', 35 | alias: 's', 36 | }, 37 | prefix: { 38 | type: 'string', 39 | alias: 'p', 40 | default: 'git-notify:', 41 | }, 42 | color: { 43 | type: 'string', 44 | alias: 'c', 45 | default: '#ff6f6f', 46 | description: 47 | 'hex value like #ff0000, or one of: black, red, green, yellow, blue, magenta, cyan, white, gray', 48 | }, 49 | }, 50 | } 51 | ); 52 | 53 | async function hook(lifecycle: Lifecycle, args: string[], flags: Flags) { 54 | // get all commit messages for the relevant revision range 55 | // depending on which git hook / command was executed 56 | const logs = await getLogStream(lifecycle, args); 57 | 58 | // stream through logs and print any found notifications 59 | showNotifications(logs, flags); 60 | } 61 | 62 | // first argument is the git hook method 63 | const lifecycle = cli.input[0] as Lifecycle; 64 | 65 | // rest of the positional args come from the git hook 66 | const gitHookArgs = cli.input.slice(1); 67 | 68 | hook(lifecycle, gitHookArgs, cli.flags); 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git-notify 2 | 3 | _Communicate important updates to your team via git commit messages_. 4 | 5 | ## What is git-notify? 6 | 7 | Sometimes you need to communicate changes to other developers on your project. In a small team, a Slack message works okay, but in larger teams and distributed organizations (such as open source projects), reaching everyone can be a pain. 8 | 9 | `git-notify` allows you to embed announcements into your git commit messages: 10 | 11 | ```sh 12 | git commit -m 'git-notify: NEW DEVELOPMENT ENVIRONMENT ...' 13 | ``` 14 | 15 | And display them to another developer on a machine, far far away: 16 | 17 | Demo 18 | 19 | Simple as that. 20 | 21 | ## How to use git-notify? 22 | 23 | Just add `"git-notify:"` to your git commit message, and anything that follows will be displayed when another developer pulls that commit, or switches from a branch that does not contain that commit to one that does. 24 | 25 | If you're using a merge or squash commit strategy on GitHub, you can also add them to the extended commit message when landing a PR: 26 | 27 | GitHub PR flow example 28 | 29 | ## Getting Started 30 | 31 | Install `git-notify` to your `npm` (or `yarn`) based project as a devDependency: 32 | 33 | ```bash 34 | # using npm 35 | npm install --save-dev git-notify 36 | 37 | # using yarn 38 | yarn add -D git-notify 39 | ``` 40 | 41 | Next, we'll configure `git-notify` to run automatically when other developers pull commits that contain git messages. Below we show how to achieve this with the excellent [husky](https://github.com/typicode/husky) library. For other approaches, see the [Git Hooks](#git-hooks) section later in this document. 42 | 43 | ### Installing hooks with husky 44 | 45 | ``` 46 | # using npm 47 | npm install --save-dev husky@4 48 | 49 | # using yarn 50 | yarn add -D husky@4 51 | ``` 52 | 53 | Configure `git-notify` hooks by adding the following `husky` entries to your `package.json`: 54 | 55 | ```json 56 | { 57 | //...snip 58 | "husky": { 59 | "hooks": { 60 | "post-merge": "git-notify merge $HUSKY_GIT_PARAMS", 61 | "post-rewrite": "git-notify rewrite $HUSKY_GIT_PARAMS", 62 | "post-checkout": "git-notify checkout $HUSKY_GIT_PARAMS" 63 | } 64 | } 65 | } 66 | ``` 67 | 68 | _**Note:** The above instructions below are for [husky v4.x](https://github.com/typicode/husky/tree/master). Husky v5 has changed how hooks are configured, as well updated its licensing terms to be free only to other open source projects.See [husky's own documentation](https://dev.to/typicode/what-s-new-in-husky-5-32g5) for how to configure hooks in their latest version._ 69 | 70 | ## Configuration 71 | 72 | - `git-notify --prefix "@everyone"` 73 | - Change the prefix `git-notify` looks for in git commit messages 74 | - Default: `git-notify:` 75 | - `git-notify --color "#ff6f6f"` 76 | - Change the color of the banner or message 77 | - This can be one of the [`chalk` preset colors](https://www.npmjs.com/package/chalk#colors) or a hex value. Note that not all terminals support full hex color scales. 78 | - `git-notify --simple` 79 | - Instead of a fancy banner, displays a simple text message 80 | 81 | ### All parameters 82 | 83 | Run `npx git-notify --help` for an up to date list of parameters: 84 | 85 | ```sh 86 | npx git-notify --help 87 | 88 | Usage 89 | $ git-notify [options] $GIT_PARAMS 90 | 91 | Methods 92 | since show all notifications since commit 93 | merge run on git pull/merge 94 | rewrite run on git rebase 95 | checkout run on git checkout/switch 96 | 97 | Options 98 | --prefix, -p prefix to look for in commit messages (default: "git-notify:") 99 | --simple, -s show a plain, unboxed notification 100 | --color, -c color of displayed notification 101 | 102 | Examples 103 | $ git-notify since HEAD~5 104 | $ git-notify checkout $GIT_PARAMS 105 | ``` 106 | 107 | ## About formatting 108 | 109 | `git-notify` will display a message for every "git-notify:" prefix it finds in the commit log that was just pulled/merged/rebased/checked out. **The notification message will be the rest of the paragraph following the prefix.** 110 | 111 | For example, this commit message: 112 | 113 | ``` 114 | This change upgrades some of our dependencies. git-notify: Please run npm install 115 | ``` 116 | 117 | Will print: 118 | 119 | ``` 120 | ╒════════════════════════════╕ 121 | │ │ 122 | │ Please run npm install │ 123 | │ │ 124 | ╘════════════════════════════╛ 125 | ``` 126 | 127 | The message will run until the end of the paragraph, delimited by a double line break. Single line breaks and other whitespace will be preserved. So that: 128 | 129 | ``` 130 | Rewrite everything. 131 | 132 | git-notify:EVERYTHING HAS CHANGED 133 | This project has been rewritten 134 | from scratch. If something broke, 135 | please contact Jeff at dev@null.com. 136 | 137 | May god please forgive me. 138 | ``` 139 | 140 | Will display: 141 | 142 | ``` 143 | ╒══════════════════════════════════════════╕ 144 | │ │ 145 | │ EVERYTHING HAS CHANGED │ 146 | │ This project has been rewritten │ 147 | │ from scratch. If something broke, │ 148 | │ please contact Jeff at dev@null.com. │ 149 | │ │ 150 | ╘══════════════════════════════════════════╛ 151 | ``` 152 | 153 | You can run `git-notify since` to test configuration and dry-run the message you've just created locally. For example: 154 | 155 | ``` 156 | git commit -m '@team what's up??' 157 | npx git-notify since HEAD~1 --prefix "@team" 158 | ``` 159 | 160 | ### Can I group messages 161 | 162 | Not at the moment, but this should not be difficult to add. See [Contributing](#contributing) 163 | 164 | ## Git Hooks 165 | 166 | ### Installing with husky 167 | 168 | See [Installing hooks with husky](#installing-hooks-with-husky) in the Getting Started section. 169 | 170 | ### Installing hooks by any other means 171 | 172 | `git-notify` is agnostic to however you want to install your git hooks. 173 | 174 | The hooks you need to configure are: 175 | 176 | - **post-merge** (runs on `git pull` and `git merge`) 177 | - `npx git-notify merge $GIT_PARAMS` 178 | - **post-rewrite** (runs on `git rebase`) 179 | - `npx git-notify rewrite $GIT_PARAMS` 180 | - **post-checkout** (runs on `git checkout` -- optional, but useful) 181 | - `npx git-notify checkout $GIT_PARAMS` 182 | 183 | At the time of writing, `git-notify checkout` is the only hook that uses the arguments (`$GIT_PARAMS`) passed to the git hook, but ideally you should always pass the arguments to `git-notify`, in case we'll need to use them in a later version. 184 | 185 | See [githooks.com](https://githooks.com/) for more resources on the topic. Documentation for different approaches are welcome! 186 | 187 | ### Installing git-notify without npm 188 | 189 | At this time, `git-notify` is a node-based project. While I recognize it could be useful in other types of projects (ruby, python, rust, etc...), cross-platform scripting sucks, and this project is not interested in solving those problems at this time. 190 | 191 | However, the `git-notify` beviour has been implemented in other languages: 192 | 193 | - **PHP**: [Captain Hook](https://github.com/captainhookphp/captainhook) 194 | 195 | If you like this idea, feel free to steal it and implement your own version, and I'll add it here. 196 | 197 | ## Contributing 198 | 199 | This project is open to contributions. For anything that would radically change the nature of the project or increase its maintenance burden, please open an issue first to discuss. 200 | 201 | ### Local development 202 | 203 | This project is written in TypeScript and scaffolded using [tsdx](https://github.com/formium/tsdx). 204 | 205 | To run TSDX, use: 206 | 207 | ```bash 208 | yarn start 209 | ``` 210 | 211 | This builds to `/dist` and runs the project in watch mode so any edits you save inside `src` causes a rebuild to `/dist`. 212 | 213 | To do a one-off build, use `npm run build` or `yarn build`. 214 | 215 | To run tests, use `npm test` or `yarn test`. 216 | 217 | ## Thanks 218 | 219 | Special thanks to [Sindre Sorhus](https://github.com/sindresorhus), whose excellent [meow](https://github.com/sindresorhus/meow), [boxen](https://github.com/sindresorhus/boxen) and [chalk](https://github.com/chalk/chalk) libraries make developing Node CLIs a breeze. 220 | 221 | ## LICENSE 222 | 223 | [MIT](LICENSE) 224 | --------------------------------------------------------------------------------