├── .github ├── renovate.json └── workflows │ └── node.yml ├── .gitignore ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── bin ├── index.js ├── lib.js └── lib.test.js ├── demo.gif ├── invoke.js ├── mock_hooks └── COMMIT_EDITMSG ├── package-lock.json └── package.json /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:base"], 3 | "assignees": ["tjoskar"], 4 | "commitMessagePrefix": "⬆️", 5 | "prBodyNotes": ["{{#if isMajor}}:warning: MAJOR MAJOR MAJOR :warning:{{/if}}"], 6 | "reviewers": ["tjoskar"], 7 | "packageRules": [ 8 | { 9 | "depTypeList": ["devDependencies"], 10 | "automerge": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: ["push"] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Use Node.js v16 11 | uses: actions/setup-node@v2 12 | with: 13 | node-version: "16.x" 14 | - name: ⬇️ Install 15 | run: npm ci 16 | env: 17 | CI: true 18 | - name: ✅ Lint 19 | run: npm run lint 20 | - name: ⚙️ Test 21 | run: npm run test 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | mock_hooks/prepare-commit-msg 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["samverschueren.linter-xo"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tslint.enable": false, 3 | "eslint.enable": false, 4 | "editor.formatOnSave": true, 5 | "xo.enable": true, 6 | "xo.format.enable": true, 7 | "[javascript]": { 8 | "editor.defaultFormatter": "samverschueren.linter-xo" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Oskar Karlsson, Mathieu Santo Stefano--Féron 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gitmoji-commit-hook 2 | 3 | [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Ftjoskar%2Fgitmoji-commit-hook%2Fbadge&style=flat)](https://actions-badge.atrox.dev/tjoskar/gitmoji-commit-hook/goto) 4 | 5 | > Prepend the right emoji to your commit message from [Gitmoji](https://github.com/carloscuesta/gitmoji) 6 | 7 | ## Install 8 | 9 | - Install gitmoji-commit-hook package 10 | 11 | ``` 12 | $ npm install -g gitmoji-commit-hook 13 | ``` 14 | 15 | - Install the hook 16 | 17 | ``` 18 | $ cd any-git-initialized-directory 19 | $ gitmoji-commit-hook --init 20 | ``` 21 | 22 | ## Usage 23 | 24 | ![Demo](https://github.com/tjoskar/gitmoji-commit-hook/blob/master/demo.gif?raw=true) 25 | 26 | ## Config 27 | 28 | You can put unwanted emojis in a blacklist section by adding the name in a blacklist array in your `package.json`: 29 | 30 | ```json 31 | { 32 | "gitmoji": { 33 | "blacklist": [ 34 | "card-file-box", 35 | "beers" 36 | ] 37 | } 38 | } 39 | ``` 40 | 41 | ## Emoji Meanings 42 | 43 | A list of available emojis and their associated meanings can be found at [gitmoji.carloscuesta.me](https://gitmoji.carloscuesta.me/) 44 | 45 | ## KISS principle 46 | 47 | This package follow KISS principle, the only thing it does is to allow you 48 | to add an emoji from gitmojis list to your commit. 49 | 50 | If you're looking for some other cool feature like search in gitmojis list, 51 | please consider [gitmoji-cli](https://github.com/carloscuesta/gitmoji-cli) 52 | 53 | ## Develop 54 | 55 | To run the linter: `npm run lint` 56 | 57 | To run the unit test: `npm test` 58 | 59 | To dry run the script: 60 | ```bash 61 | node invoke.js --init # run the init setup 62 | 63 | node invoke.js mock_hooks/COMMIT_EDITMSG # simulate a git commit 64 | ``` 65 | 66 | ## License 67 | 68 | The code is available under the [MIT](https://github.com/tjoskar/gitmoji-commit-hook/blob/master/LICENSE) license. 69 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import process from 'node:process'; 4 | import {gitmojiCommitHook} from './lib.js'; 5 | 6 | gitmojiCommitHook(`${process.env.PWD}/.git/hooks`, process.argv[2]); 7 | -------------------------------------------------------------------------------- /bin/lib.js: -------------------------------------------------------------------------------- 1 | import {writeFile, readFile, chmod} from 'node:fs/promises'; 2 | import process from 'node:process'; 3 | import inquirer from 'inquirer'; 4 | import fetch from 'node-fetch'; 5 | import chalk from 'chalk'; 6 | import {pathExists} from 'path-exists'; 7 | import fileExists from 'file-exists'; 8 | 9 | const gitmojiUrl 10 | = 'https://raw.githubusercontent.com/carloscuesta/gitmoji/master/packages/gitmojis/src/gitmojis.json'; 11 | const prepareCommitMessageFileName = 'prepare-commit-msg'; 12 | 13 | const gitmojiCommitHookComand = `#!/bin/sh 14 | exec < /dev/tty 15 | gitmoji-commit-hook $1 16 | `; 17 | 18 | const errorMessage = { 19 | notGit: 'The directory is not a git repository.', 20 | commitHookExist: `A prepare-commit hook already exists, please remove the hook (rm .git/hooks/${prepareCommitMessageFileName}) or install gitmoji-commit-hook manually by adding the following content info .git/hooks/\n\n${prepareCommitMessageFileName}:${gitmojiCommitHookComand}`, 21 | gitmojiParse: `Could not find gitmojis at ${gitmojiUrl}.`, 22 | }; 23 | 24 | function errorHandler(error) { 25 | console.error(chalk.red(`🚨 ERROR: ${error}`)); 26 | process.exit(1); 27 | } 28 | 29 | function rejectIf(errorMessage_) { 30 | return value => (value ? Promise.reject(new Error(errorMessage_)) : value); 31 | } 32 | 33 | function rejectIfNot(errorMessage_) { 34 | return value => (value ? value : Promise.reject(new Error(errorMessage_))); 35 | } 36 | 37 | async function getGitmojiList() { 38 | try { 39 | const {gitmojis} = await fetch(gitmojiUrl).then(r => r.json()); 40 | if (!gitmojis) { 41 | throw new Error(errorMessage.gitmojiParse); 42 | } 43 | 44 | return gitmojis; 45 | } catch (error) { 46 | throw new Error(errorMessage.gitmojiParse + ` ${error.message}`); 47 | } 48 | } 49 | 50 | function assertGitRepository() { 51 | return pathExists('.git').then(rejectIfNot(errorMessage.notGit)); 52 | } 53 | 54 | function assertNoPrepareCommitHook(gitHookPath) { 55 | return () => 56 | fileExists(`${gitHookPath}/${prepareCommitMessageFileName}`).then( 57 | rejectIf(errorMessage.commitHookExist), 58 | ); 59 | } 60 | 61 | function initProject(gitHookPath) { 62 | return assertGitRepository() 63 | .then(assertNoPrepareCommitHook(gitHookPath)) 64 | .then(() => 65 | writeFile( 66 | `${gitHookPath}/${prepareCommitMessageFileName}`, 67 | gitmojiCommitHookComand, 68 | ), 69 | ) 70 | .then(() => chmod(`${gitHookPath}/${prepareCommitMessageFileName}`, '755')); 71 | } 72 | 73 | function prependMessage(getMessage, putMessage) { 74 | return filepath => message => 75 | getMessage(filepath) 76 | .then(fileContent => `${message} ${fileContent}`) 77 | .then(fileContent => putMessage(filepath, fileContent)); 78 | } 79 | 80 | const prependMessageToFile = prependMessage(readFile, writeFile); 81 | 82 | async function readJson(path) { 83 | return JSON.parse(await readFile(path)); 84 | } 85 | 86 | function getGitmojiExclude() { 87 | return fileExists(`${process.env.PWD}/package.json`) 88 | .then(exist => (exist ? readJson(`${process.env.PWD}/package.json`) : {})) 89 | .then(packageJson => packageJson.gitmoji || {}) 90 | .then(gitmoji => gitmoji.blacklist || []); 91 | } 92 | 93 | function seperateChoices(choices) { 94 | return exclude => { 95 | if (exclude.length === 0) { 96 | return choices; 97 | } 98 | 99 | return [ 100 | ...choices.filter(choice => !exclude.includes(choice.type)), 101 | new inquirer.Separator(), 102 | ...choices.filter(choice => exclude.includes(choice.type)), 103 | new inquirer.Separator(), 104 | ]; 105 | }; 106 | } 107 | 108 | function seperateExcludeEmojis(choices) { 109 | return getGitmojiExclude().then(seperateChoices(choices)); 110 | } 111 | 112 | function printInitSuccess() { 113 | console.log( 114 | `${chalk.green( 115 | '🎉 SUCCESS 🎉', 116 | )} gitmoji-commit-hook initialized with success.`, 117 | ); 118 | } 119 | 120 | function mapGitmojiItemToOption(gitmoji) { 121 | return { 122 | name: gitmoji.emoji + ' ' + gitmoji.description, 123 | value: gitmoji.emoji, 124 | type: gitmoji.name, 125 | }; 126 | } 127 | 128 | function createInquirerQuestion(emojis) { 129 | return [ 130 | { 131 | type: 'checkbox', 132 | name: 'emoji', 133 | message: 'Select emoji(s) for your commit', 134 | choices: emojis, 135 | }, 136 | ]; 137 | } 138 | 139 | const isCommitEditMessageFile = stringToTest => 140 | /COMMIT_EDITMSG/g.test(stringToTest); 141 | 142 | function gitmojiCommitHook(gitHookPath, commitFile) { 143 | if (commitFile === '--init') { 144 | initProject(gitHookPath).then(printInitSuccess).catch(errorHandler); 145 | } else if (isCommitEditMessageFile(commitFile)) { 146 | getGitmojiList() 147 | .then(emojis => emojis.map(emoji => mapGitmojiItemToOption(emoji))) 148 | .then(seperateExcludeEmojis) 149 | .then(createInquirerQuestion) 150 | .then(inquirer.prompt) 151 | .then(answers => answers.emoji) 152 | .then(answers => answers.join(' ')) 153 | .then(prependMessageToFile(commitFile)) 154 | .catch(errorHandler); 155 | } 156 | } 157 | 158 | export { 159 | rejectIf, 160 | rejectIfNot, 161 | gitmojiCommitHook, 162 | prependMessage, 163 | mapGitmojiItemToOption, 164 | createInquirerQuestion, 165 | seperateChoices, 166 | }; 167 | -------------------------------------------------------------------------------- /bin/lib.test.js: -------------------------------------------------------------------------------- 1 | import {describe, test, expect} from 'vitest'; 2 | import {spy} from 'simple-spy'; 3 | import { 4 | rejectIf, 5 | rejectIfNot, 6 | prependMessage, 7 | mapGitmojiItemToOption, 8 | createInquirerQuestion, 9 | seperateChoices, 10 | } from './lib.js'; 11 | 12 | describe('rejectIf', () => { 13 | test('reject', () => { 14 | const result = rejectIf('my error message')(true); 15 | 16 | return result.catch(error => { 17 | expect(error).toBeDefined(); 18 | }); 19 | }); 20 | 21 | test('do not reject', () => { 22 | const result = rejectIf('my error message')(false); 23 | 24 | expect(result).toBe(false); 25 | }); 26 | }); 27 | 28 | describe('rejectIfNot', () => { 29 | test('do not reject', () => { 30 | const result = rejectIfNot('my error message')(true); 31 | 32 | expect(result).toBe(true); 33 | }); 34 | 35 | test('reject', () => { 36 | const result = rejectIfNot('my error message')(false); 37 | 38 | return result.catch(error => { 39 | expect(error).toBeDefined(); 40 | }); 41 | }); 42 | }); 43 | 44 | test('Prepend a message', () => { 45 | const getMessage = spy(() => Promise.resolve('World')); 46 | const putMessage = spy(() => Promise.resolve()); 47 | const fileName = 'myfile.txt'; 48 | const message = 'Hello'; 49 | 50 | return prependMessage( 51 | getMessage, 52 | putMessage, 53 | )(fileName)(message).then(() => { 54 | expect(getMessage.args[0][0]).toBe(fileName); 55 | expect(putMessage.args[0][0]).toBe(fileName); 56 | expect(putMessage.args[0][1]).toBe('Hello World'); 57 | }); 58 | }); 59 | 60 | test('Map gitmoji item to an option', () => { 61 | const gitmoji = { 62 | emoji: 'a', 63 | description: 'Something awesome', 64 | }; 65 | 66 | const result = mapGitmojiItemToOption(gitmoji); 67 | 68 | expect(result.value).toEqual('a'); 69 | expect(result.name).toEqual('a Something awesome'); 70 | }); 71 | 72 | test('Create Inquirer question', () => { 73 | const choices = [ 74 | { 75 | name: 'Something', 76 | value: 's', 77 | }, 78 | ]; 79 | const result = createInquirerQuestion(choices); 80 | 81 | expect(result[0].choices).toBe(choices); 82 | }); 83 | 84 | test('Seperate choices', () => { 85 | const choices = [ 86 | { 87 | type: ':cat:', 88 | }, 89 | { 90 | type: ':dog:', 91 | }, 92 | ]; 93 | const exclude = [':dog:']; 94 | 95 | const result = seperateChoices(choices)(exclude); 96 | 97 | expect(result[0]).toEqual({type: ':cat:'}); 98 | expect(result[1].type).toEqual('separator'); 99 | expect(result[2]).toEqual({type: ':dog:'}); 100 | }); 101 | 102 | test('Return same list if no blacklist', () => { 103 | const choices = [ 104 | { 105 | type: ':cat:', 106 | }, 107 | { 108 | type: ':dog:', 109 | }, 110 | ]; 111 | const exclude = []; 112 | 113 | const result = seperateChoices(choices)(exclude); 114 | 115 | expect(result).toBe(choices); 116 | }); 117 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjoskar/gitmoji-commit-hook/78f854cd38d5283ff496859cb60820c040bb9df0/demo.gif -------------------------------------------------------------------------------- /invoke.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import {gitmojiCommitHook} from './bin/lib.js'; 3 | 4 | gitmojiCommitHook(`${process.env.PWD}/mock_hooks`, process.argv[2]); 5 | -------------------------------------------------------------------------------- /mock_hooks/COMMIT_EDITMSG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjoskar/gitmoji-commit-hook/78f854cd38d5283ff496859cb60820c040bb9df0/mock_hooks/COMMIT_EDITMSG -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitmoji-commit-hook", 3 | "version": "3.0.2", 4 | "description": "Start the commit message with an right emoji", 5 | "main": "bin/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "lint": "xo", 9 | "test": "vitest" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+ssh://git@github.com/tjoskar/emoji-commit-hook.git" 14 | }, 15 | "keywords": [ 16 | "git", 17 | "git-hook", 18 | "emoji", 19 | "gitmoji" 20 | ], 21 | "bin": { 22 | "gitmoji-commit-hook": "./bin/index.js" 23 | }, 24 | "author": "tjoskar ", 25 | "license": "MIT", 26 | "bugs": { 27 | "url": "https://github.com/tjoskar/emoji-commit-hook/issues" 28 | }, 29 | "homepage": "https://github.com/tjoskar/emoji-commit-hook#readme", 30 | "engines": { 31 | "node": ">=14.0.0" 32 | }, 33 | "dependencies": { 34 | "chalk": "5.0.1", 35 | "file-exists": "5.0.1", 36 | "inquirer": "9.0.2", 37 | "node-fetch": "3.2.10", 38 | "path-exists": "5.0.0" 39 | }, 40 | "devDependencies": { 41 | "simple-spy": "4.0.1", 42 | "vitest": "3.1.4", 43 | "xo": "0.52.4" 44 | }, 45 | "volta": { 46 | "node": "16.17.0" 47 | }, 48 | "wallaby": { 49 | "testFramework": "vitest" 50 | }, 51 | "xo": { 52 | "rules": { 53 | "unicorn/no-process-exit": "off" 54 | } 55 | } 56 | } 57 | --------------------------------------------------------------------------------