├── .all-contributorsrc ├── .dockerignore ├── .editorconfig ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── canary-release.yml │ └── test-and-lint.yml ├── .gitignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── doc └── screenshot.png ├── jest.config-e2e.js ├── lerna.json ├── misc ├── example.gif ├── interactive.gif ├── logo.png └── package.sample.json ├── package.json ├── packages ├── gitmoji-changelog-cli │ ├── .npmignore │ ├── README.md │ ├── package.json │ └── src │ │ ├── cli.e2e.js │ │ ├── cli.js │ │ ├── cli.spec.js │ │ ├── index.js │ │ ├── interactiveMode.js │ │ ├── interactiveMode.spec.js │ │ ├── presets │ │ ├── cargo.js │ │ ├── generic.js │ │ ├── generic.spec.js │ │ ├── helm.js │ │ ├── helm.spec.js │ │ ├── maven.js │ │ ├── node.js │ │ ├── node.spec.js │ │ ├── python.js │ │ └── python.spec.js │ │ ├── repository.js │ │ └── repository.spec.js ├── gitmoji-changelog-core │ ├── .npmignore │ ├── package.json │ └── src │ │ ├── errors.js │ │ ├── fromGitFile.js │ │ ├── groupMapping.js │ │ ├── index.js │ │ ├── index.spec.js │ │ ├── logger.js │ │ ├── parser.js │ │ ├── parser.spec.js │ │ ├── utils.js │ │ └── utils.spec.js ├── gitmoji-changelog-documentation │ ├── .nojekyll │ ├── README.md │ ├── index.html │ └── package.json └── gitmoji-changelog-markdown │ ├── .npmignore │ ├── package.json │ └── src │ ├── index.js │ ├── index.spec.js │ └── template.md └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "gitmoji-changelog", 3 | "projectOwner": "frinyvonnick", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": false, 11 | "commitConvention": "gitmoji", 12 | "contributors": [ 13 | { 14 | "login": "frinyvonnick", 15 | "name": "Yvonnick FRIN", 16 | "avatar_url": "https://avatars0.githubusercontent.com/u/13099512?v=4", 17 | "profile": "https://yvonnickfrin.dev", 18 | "contributions": [ 19 | "code" 20 | ] 21 | }, 22 | { 23 | "login": "bpetetot", 24 | "name": "Benjamin Petetot", 25 | "avatar_url": "https://avatars3.githubusercontent.com/u/516360?v=4", 26 | "profile": "https://github.com/bpetetot", 27 | "contributions": [ 28 | "code" 29 | ] 30 | }, 31 | { 32 | "login": "fabienjuif", 33 | "name": "Fabien JUIF", 34 | "avatar_url": "https://avatars0.githubusercontent.com/u/17828231?v=4", 35 | "profile": "https://github.com/fabienjuif", 36 | "contributions": [ 37 | "code" 38 | ] 39 | }, 40 | { 41 | "login": "bgauduch", 42 | "name": "Baptiste Gauduchon", 43 | "avatar_url": "https://avatars0.githubusercontent.com/u/5585755?v=4", 44 | "profile": "https://github.com/bgauduch", 45 | "contributions": [ 46 | "code" 47 | ] 48 | }, 49 | { 50 | "login": "kefranabg", 51 | "name": "Franck Abgrall", 52 | "avatar_url": "https://avatars3.githubusercontent.com/u/9840435?v=4", 53 | "profile": "https://www.franck-abgrall.me/", 54 | "contributions": [ 55 | "code" 56 | ] 57 | }, 58 | { 59 | "login": "quentinncl", 60 | "name": "quentinncl", 61 | "avatar_url": "https://avatars0.githubusercontent.com/u/12430277?v=4", 62 | "profile": "https://github.com/quentinncl", 63 | "contributions": [ 64 | "code" 65 | ] 66 | }, 67 | { 68 | "login": "lhauspie", 69 | "name": "Logan HAUSPIE", 70 | "avatar_url": "https://avatars1.githubusercontent.com/u/25682509?v=4", 71 | "profile": "https://github.com/lhauspie", 72 | "contributions": [ 73 | "code" 74 | ] 75 | }, 76 | { 77 | "login": "gmembre-zenika", 78 | "name": "Guillaume Membré", 79 | "avatar_url": "https://avatars3.githubusercontent.com/u/7294764?v=4", 80 | "profile": "https://www.geekeries.fun", 81 | "contributions": [ 82 | "code" 83 | ] 84 | }, 85 | { 86 | "login": "yannbertrand", 87 | "name": "Yann Bertrand", 88 | "avatar_url": "https://avatars0.githubusercontent.com/u/5855339?v=4", 89 | "profile": "http://yann-bertrand.fr/", 90 | "contributions": [ 91 | "code" 92 | ] 93 | }, 94 | { 95 | "login": "snikic", 96 | "name": "s n", 97 | "avatar_url": "https://avatars3.githubusercontent.com/u/2975674?v=4", 98 | "profile": "https://github.com/snikic", 99 | "contributions": [ 100 | "code" 101 | ] 102 | }, 103 | { 104 | "login": "mathieutu", 105 | "name": "Mathieu TUDISCO", 106 | "avatar_url": "https://avatars3.githubusercontent.com/u/11351322?v=4", 107 | "profile": "https://mathieutu.dev", 108 | "contributions": [ 109 | "code" 110 | ] 111 | }, 112 | { 113 | "login": "charlyx", 114 | "name": "Charles-Henri GUERIN", 115 | "avatar_url": "https://avatars2.githubusercontent.com/u/481446?v=4", 116 | "profile": "http://charlyx.dev", 117 | "contributions": [ 118 | "code" 119 | ] 120 | }, 121 | { 122 | "login": "FBerthelot", 123 | "name": "Florent Berthelot", 124 | "avatar_url": "https://avatars1.githubusercontent.com/u/6927852?v=4", 125 | "profile": "https://github.com/FBerthelot", 126 | "contributions": [ 127 | "code" 128 | ] 129 | }, 130 | { 131 | "login": "EmmanuelDemey", 132 | "name": "Emmanuel DEMEY", 133 | "avatar_url": "https://avatars2.githubusercontent.com/u/555768?v=4", 134 | "profile": "http://gillespie59.github.io/", 135 | "contributions": [ 136 | "code" 137 | ] 138 | }, 139 | { 140 | "login": "christopherkade", 141 | "name": "Christopher Kade", 142 | "avatar_url": "https://avatars3.githubusercontent.com/u/15229355?v=4", 143 | "profile": "https://christopherkade.com", 144 | "contributions": [ 145 | "blog" 146 | ] 147 | }, 148 | { 149 | "login": "rudym", 150 | "name": "Rodion Martynov", 151 | "avatar_url": "https://avatars0.githubusercontent.com/u/863788?v=4", 152 | "profile": "http://numcom.herokuapp.com/", 153 | "contributions": [ 154 | "doc" 155 | ] 156 | }, 157 | { 158 | "login": "DanielTamkin", 159 | "name": "Daniel Tamkin", 160 | "avatar_url": "https://avatars1.githubusercontent.com/u/9532762?v=4", 161 | "profile": "http://danieltamkin.com", 162 | "contributions": [ 163 | "doc" 164 | ] 165 | }, 166 | { 167 | "login": "endormi", 168 | "name": "Erno Salo", 169 | "avatar_url": "https://avatars3.githubusercontent.com/u/39559256?v=4", 170 | "profile": "http://endormi.io", 171 | "contributions": [ 172 | "doc" 173 | ] 174 | }, 175 | { 176 | "login": "MarkLyck", 177 | "name": "Mark Lyck", 178 | "avatar_url": "https://avatars3.githubusercontent.com/u/6841110?v=4", 179 | "profile": "http://markdid.it", 180 | "contributions": [ 181 | "code" 182 | ] 183 | }, 184 | { 185 | "login": "horaklukas", 186 | "name": "Lukáš Horák", 187 | "avatar_url": "https://avatars.githubusercontent.com/u/996088?v=4", 188 | "profile": "https://github.com/horaklukas", 189 | "contributions": [ 190 | "code" 191 | ] 192 | }, 193 | { 194 | "login": "juwit", 195 | "name": "Julien WITTOUCK", 196 | "avatar_url": "https://avatars.githubusercontent.com/u/7531844?v=4", 197 | "profile": "https://github.com/juwit", 198 | "contributions": [ 199 | "code" 200 | ] 201 | } 202 | ], 203 | "contributorsPerLine": 7, 204 | "skipCi": true 205 | } 206 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | ** 2 | 3 | !Dockerfile 4 | !package.json 5 | !yarn.lock 6 | !packages/ 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base/legacy", 3 | "env": { 4 | "jest": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | "parserOptions": { 9 | "ecmaVersion": 2018 10 | }, 11 | "rules": { 12 | "semi": ["error", "never"], 13 | "no-use-before-define": ["error", { "functions": false }], 14 | "comma-dangle": ["error", "always-multiline"], 15 | "no-console": 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [frinyvonnick,bpetetot] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/canary-release.yml: -------------------------------------------------------------------------------- 1 | name: Canary release 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [12] 15 | 16 | steps: 17 | - name: Checkout project 18 | uses: actions/checkout@v1 19 | - name: Use Node.js ${{ matrix.node-version }} 20 | uses: actions/setup-node@v1 21 | with: 22 | node-version: ${{ matrix.node-version }} 23 | registry-url: 'https://registry.npmjs.org' 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | - name: Publish Canary version 27 | run: npx lerna publish --canary --yes --exact 28 | env: 29 | NODE_AUTH_TOKEN: ${{ secrets.NODE_AUTH_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/test-and-lint.yml: -------------------------------------------------------------------------------- 1 | name: Test and lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [12] 16 | 17 | steps: 18 | - name: Checkout project 19 | uses: actions/checkout@v1 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: Install dependencies 25 | run: yarn install --frozen-lockfile 26 | - name: Lint 27 | run: yarn lint 28 | - name: Unit tests 29 | run: yarn test 30 | - name: End to end tests 31 | run: | 32 | git config --global user.email "test@example.com" 33 | git config --global user.name "Test gitmoji" 34 | yarn test:e2e 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | CHANGELOG.json 64 | .vscode 65 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## 2.3.0 (2022-01-04) 5 | 6 | ### Added 7 | 8 | - ✨ Add Helm preset ([#229](https://github.com/frinyvonnick/gitmoji-changelog/issues/229)) [[8c16f72](https://github.com/frinyvonnick/gitmoji-changelog/commit/8c16f72302d44b20727d6e7b82adb68802afb72e)] 9 | 10 | ### Changed 11 | 12 | - ⬆️ Bump ssri from 6.0.1 to 6.0.2 ([#215](https://github.com/frinyvonnick/gitmoji-changelog/issues/215)) [[ed4a033](https://github.com/frinyvonnick/gitmoji-changelog/commit/ed4a03398242caa5f17dcd5cdda4f8b8815e6cc3)] 13 | - ⬆️ Bump normalize-url from 4.5.0 to 4.5.1 ([#219](https://github.com/frinyvonnick/gitmoji-changelog/issues/219)) [[9cb9e41](https://github.com/frinyvonnick/gitmoji-changelog/commit/9cb9e41ce0ab9e066616f9d60fa422094c2c1527)] 14 | - ⬆️ Bump y18n from 3.2.1 to 3.2.2 ([#220](https://github.com/frinyvonnick/gitmoji-changelog/issues/220)) [[0668d86](https://github.com/frinyvonnick/gitmoji-changelog/commit/0668d8604263776117a6052d29b33473b770c196)] 15 | - ⬆️ Upgrade dependencies using yarn upgrade ([#232](https://github.com/frinyvonnick/gitmoji-changelog/issues/232)) [[14f55bc](https://github.com/frinyvonnick/gitmoji-changelog/commit/14f55bc61cdff0863fc2c73af9086191cab93935)] 16 | - ⬆️ Upgrade vulnerable packages ([#230](https://github.com/frinyvonnick/gitmoji-changelog/issues/230)) [[27c53bd](https://github.com/frinyvonnick/gitmoji-changelog/commit/27c53bd253f17a94eac1c78b42a0630be3b299e9)] 17 | 18 | ### Miscellaneous 19 | 20 | - 👥 Add horaklukas as a contributor for code ([#234](https://github.com/frinyvonnick/gitmoji-changelog/issues/234)) [[1f94e98](https://github.com/frinyvonnick/gitmoji-changelog/commit/1f94e987d014bb5863eebf9811d5c3f01b71bb9b)] 21 | - 👥 Add juwit as a contributor for code ([#233](https://github.com/frinyvonnick/gitmoji-changelog/issues/233)) [[b643004](https://github.com/frinyvonnick/gitmoji-changelog/commit/b643004a1e5173d5387026530041b863bf930788)] 22 | 23 | 24 | 25 | ## 2.2.1 (2021-01-21) 26 | 27 | ### Changed 28 | 29 | - 📌 Fix yarn.lock not up to date ([#207](https://github.com/frinyvonnick/gitmoji-changelog/issues/207)) [[54d194f](https://github.com/frinyvonnick/gitmoji-changelog/commit/54d194f08ebb073c14626c80dc5ecfa494e5a3e2)] 30 | 31 | ### Fixed 32 | 33 | - 🐛 Fix unsupported key using read-pkg-up ([#206](https://github.com/frinyvonnick/gitmoji-changelog/issues/206)) [[7a2eb08](https://github.com/frinyvonnick/gitmoji-changelog/commit/7a2eb084c37e05300dd53fb940da1f24206f29f6)] 34 | 35 | ### Miscellaneous 36 | 37 | - 👥 Add MarkLyck as a contributor ([#208](https://github.com/frinyvonnick/gitmoji-changelog/issues/208)) [[134dfb9](https://github.com/frinyvonnick/gitmoji-changelog/commit/134dfb95ea360bd566d272256881443f30829c18)] 38 | 39 | 40 | 41 | ## 2.2.0 (2020-12-16) 42 | 43 | ### Added 44 | 45 | - ✨ Add Cargo preset ([#199](https://github.com/frinyvonnick/gitmoji-changelog/issues/199)) [[ceab8dc](https://github.com/frinyvonnick/gitmoji-changelog/commit/ceab8dcd86b3fbb1c91134bfad844a16f6f0f609)] 46 | 47 | ### Changed 48 | 49 | - 📌 Upgrade dependencies ([#191](https://github.com/frinyvonnick/gitmoji-changelog/issues/191)) [[cdf76f9](https://github.com/frinyvonnick/gitmoji-changelog/commit/cdf76f9a5f12a11703216de203f99a144436cfe9)] 50 | 51 | ### Fixed 52 | 53 | - 🐛 Fix init command ([#204](https://github.com/frinyvonnick/gitmoji-changelog/issues/204)) [[a9b1bfe](https://github.com/frinyvonnick/gitmoji-changelog/commit/a9b1bfe1642218de7005c8e6870e8a5cad31a4aa)] 54 | - 🐛 Fix a bug occuring when version is missing in configuration but passed as argument ([#190](https://github.com/frinyvonnick/gitmoji-changelog/issues/190)) [[20cf369](https://github.com/frinyvonnick/gitmoji-changelog/commit/20cf3690dd0acd0712f3068368c80ade1422cd5b)] 55 | 56 | ### Miscellaneous 57 | 58 | - 📝 Add the documentation site ([#194](https://github.com/frinyvonnick/gitmoji-changelog/issues/194)) [[5e0a971](https://github.com/frinyvonnick/gitmoji-changelog/commit/5e0a971775a7c986b11c7ee4fe9404ac4a2472cd)] 59 | - 🥅 Handle functional errors [[9a15406](https://github.com/frinyvonnick/gitmoji-changelog/commit/9a154061439a0e1e253e15b20ee5df6ae4b5388a)] 60 | - 📝 Improve documentation about presets ([#189](https://github.com/frinyvonnick/gitmoji-changelog/issues/189)) [[4e22239](https://github.com/frinyvonnick/gitmoji-changelog/commit/4e22239bd96e4b60eb966297e2e67e2070b03ea0)] 61 | - 📝 Improve Workflow section ([#152](https://github.com/frinyvonnick/gitmoji-changelog/issues/152)) [[1635963](https://github.com/frinyvonnick/gitmoji-changelog/commit/1635963b25fb6b611a563b918c5d78a5e11a3c16)] 62 | 63 | 64 | 65 | ## 2.1.0 (2020-01-21) 66 | 67 | ### Added 68 | 69 | - ✨ Add maven preset ([#172](https://github.com/frinyvonnick/gitmoji-changelog/issues/172)) [[1622775](https://github.com/frinyvonnick/gitmoji-changelog/commit/16227751f3c3a60192d6c277434ec85ab2d13b4f)] (by Guillaume Membré) 70 | - ✨ Generate a bug report when unexpected errors happens ([#137](https://github.com/frinyvonnick/gitmoji-changelog/issues/137)) [[6d68130](https://github.com/frinyvonnick/gitmoji-changelog/commit/6d6813032b735cb4832fd9020ee59527391e41b6)] (by Yvonnick FRIN) 71 | - ✨ Add a generic preset using configuration file ([#160](https://github.com/frinyvonnick/gitmoji-changelog/issues/160)) [[d41ebc6](https://github.com/frinyvonnick/gitmoji-changelog/commit/d41ebc64bb2ce8b11b4ba4e2f11540a0c48b815f)] (by Yvonnick FRIN) 72 | - ✨ Add configuration so users can customize commit mapping ([#155](https://github.com/frinyvonnick/gitmoji-changelog/issues/155)) [[be1fca2](https://github.com/frinyvonnick/gitmoji-changelog/commit/be1fca23df30c201956ca488c3bfa7b33a9c4381)] (by Yvonnick FRIN) 73 | 74 | ### Changed 75 | 76 | - ♻️ Make core independent from the git client. ([#171](https://github.com/frinyvonnick/gitmoji-changelog/issues/171)) [[4094bb9](https://github.com/frinyvonnick/gitmoji-changelog/commit/4094bb9abe01a731b0c20e0dcd006c8e08a66dc6)] (by Benjamin Petetot) 77 | - ♻️ Improve error messages for preset system ([#161](https://github.com/frinyvonnick/gitmoji-changelog/issues/161)) [[a51fee2](https://github.com/frinyvonnick/gitmoji-changelog/commit/a51fee25c633946b6aea9fe31f928e5e415a5171)] (by Yvonnick FRIN) 78 | 79 | ### Fixed 80 | 81 | - ✏️ Fix typo in CHANGELOG [[14662c4](https://github.com/frinyvonnick/gitmoji-changelog/commit/14662c46cc5fb9ad14f9f7350d01a7c00c1e972c)] (by FRIN Yvonnick) 82 | 83 | ### Miscellaneous 84 | 85 | - 📝 Correct sentence structure in documentation ([#173](https://github.com/frinyvonnick/gitmoji-changelog/issues/173)) [[36872a0](https://github.com/frinyvonnick/gitmoji-changelog/commit/36872a0fa8d02b264cab0d629869cb6538c126cb)] (by Daniel Tamkin) 86 | - 🐳 Upgrade Docker image version ([#167](https://github.com/frinyvonnick/gitmoji-changelog/issues/167)) [[8276d4a](https://github.com/frinyvonnick/gitmoji-changelog/commit/8276d4a9f8c67b6597116ba3a6472d44cdb29508)] (by Charles-Henri GUERIN) 87 | - 📝 Add documentation about generic preset ([#163](https://github.com/frinyvonnick/gitmoji-changelog/issues/163)) [[c940075](https://github.com/frinyvonnick/gitmoji-changelog/commit/c940075565d89c72c5d0114bc88e00133145f8be)] (by Yvonnick FRIN) 88 | - 📝 Fix presets path link ([#150](https://github.com/frinyvonnick/gitmoji-changelog/issues/150)) [[a27f818](https://github.com/frinyvonnick/gitmoji-changelog/commit/a27f8180ddc8f2a8280eb8b72acc8999ff66250d)] (by Rodion Martynov) 89 | 90 | 91 | 92 | ## 2.0.1 (2019-11-19) 93 | 94 | ### Added 95 | 96 | - 👷‍♂️ Add exact option to canary version ([#147](https://github.com/frinyvonnick/gitmoji-changelog/issues/147)) [[84c7086](https://github.com/frinyvonnick/gitmoji-changelog/commit/84c7086b94190d9259749262e997be8617e10345)] (by Benjamin Petetot) 97 | - 👷‍♂️ Publish Canary version on master push ([#141](https://github.com/frinyvonnick/gitmoji-changelog/issues/141)) [[84ab6f1](https://github.com/frinyvonnick/gitmoji-changelog/commit/84ab6f17a40831ca780f3ce60420ac3268624918)] (by Benjamin Petetot) 98 | - 👷‍♂️ Setup GitHub CI ([#140](https://github.com/frinyvonnick/gitmoji-changelog/issues/140)) [[a7f1228](https://github.com/frinyvonnick/gitmoji-changelog/commit/a7f1228ed313bbd507686e6cdcac5d9a5a8be46b)] (by Benjamin Petetot) 99 | 100 | ### Fixed 101 | 102 | - 🐛 Fix bug preventing generating changelog when previous version doesn't exist ([#145](https://github.com/frinyvonnick/gitmoji-changelog/issues/145)) [[4f04f45](https://github.com/frinyvonnick/gitmoji-changelog/commit/4f04f453af9c3fd82b388b082aa59b9a6f1d5374)] (by Yvonnick FRIN) 103 | 104 | 105 | ## 2.0.0 (2019-10-31) 106 | 107 | ### Added 108 | 109 | - ✨ Add a presets system to be independent from the package.json ([#128](https://github.com/frinyvonnick/gitmoji-changelog/issues/128)) [[24cead9](https://github.com/frinyvonnick/gitmoji-changelog/commit/24cead964ef3e9c1b5b7df9d53bfe357298a9191)] (by Yvonnick FRIN) 110 | - ✨ Add interactive option to manually select commits ([#81](https://github.com/frinyvonnick/gitmoji-changelog/issues/81)) [[d13ca2e](https://github.com/frinyvonnick/gitmoji-changelog/commit/d13ca2e1c77c3cd9694cde926442c303adb47fa3)] (by Franck Abgrall) 111 | 112 | ### Changed 113 | 114 | - ♻️ Change core function parameters from "mode" to "from" and "to" ([#127](https://github.com/frinyvonnick/gitmoji-changelog/issues/127)) [[42af500](https://github.com/frinyvonnick/gitmoji-changelog/commit/42af500df1b9b9d738bf9d47674537b50dd90870)] (by Yvonnick FRIN) 115 | - 🔧 Add editor config file ([#124](https://github.com/frinyvonnick/gitmoji-changelog/issues/124)) [[6c763a2](https://github.com/frinyvonnick/gitmoji-changelog/commit/6c763a2be93908232ffda7ca9d88499d38809c99)] (by quentinncl) 116 | 117 | ### Removed 118 | 119 | - 🔥 Remove some badges [[6d27a32](https://github.com/frinyvonnick/gitmoji-changelog/commit/6d27a32b900a1d1462f39a92d09b5ab1407be25b)] (by Yvonnick FRIN) 120 | 121 | ### Fixed 122 | 123 | - 🐛 Handle pre-release as latest version in markdown ([#126](https://github.com/frinyvonnick/gitmoji-changelog/issues/126)) [[d22699b](https://github.com/frinyvonnick/gitmoji-changelog/commit/d22699b6226cb03f634b22f65f0f825f980dc666)] (by Yvonnick FRIN) 124 | - 🐛 Fix updating changelog after tagging a version ([#115](https://github.com/frinyvonnick/gitmoji-changelog/issues/115)) [[8df6705](https://github.com/frinyvonnick/gitmoji-changelog/commit/8df6705bc369bbec16cc2dc1ff08c0ae55c20da3)] (by Yvonnick FRIN) 125 | - 🐛 Use last version from changelog file instead of previous git tag ([#82](https://github.com/frinyvonnick/gitmoji-changelog/issues/82)) [[d3c49d0](https://github.com/frinyvonnick/gitmoji-changelog/commit/d3c49d061cfbe2c271f9aa3739fae750dbf6327c)] (by Franck Abgrall) 126 | - 🐛 Fix dates sorting in commits ([#75](https://github.com/frinyvonnick/gitmoji-changelog/issues/75)) [[748e673](https://github.com/frinyvonnick/gitmoji-changelog/commit/748e6732a18f8bc5c529db12a558c0ffb458c8a1)] (by Mathieu TUDISCO) 127 | 128 | ### Miscellaneous 129 | 130 | - 📝 Add npx instruction on README ([#123](https://github.com/frinyvonnick/gitmoji-changelog/issues/123)) [[ffcbef5](https://github.com/frinyvonnick/gitmoji-changelog/commit/ffcbef55efd4558335f913d1c38411229a330a58)] (by quentinncl) 131 | - 📝 Add contributors to README ([#125](https://github.com/frinyvonnick/gitmoji-changelog/issues/125)) [[7580ff9](https://github.com/frinyvonnick/gitmoji-changelog/commit/7580ff9c6598b92aa3375836ce0b568e8378c65f)] (by Franck Abgrall) 132 | - 📝 Create CODE_OF_CONDUCT.md file [[51c1314](https://github.com/frinyvonnick/gitmoji-changelog/commit/51c1314fb0f6b2770bf2fdbdffc1912b8e06bbf7)] (by Yvonnick FRIN) 133 | - 📝 Propose to add badge to our users ([#103](https://github.com/frinyvonnick/gitmoji-changelog/issues/103)) [[f5981cb](https://github.com/frinyvonnick/gitmoji-changelog/commit/f5981cbeb102c8b6d36427c64f5e4f31932a938a)] (by Florent Berthelot) 134 | - 🐳 Add dependency in markdown package to fix docker image [[804a780](https://github.com/frinyvonnick/gitmoji-changelog/commit/804a780a957e2ae6364001331cd8ed818a9580a1)] (by FRIN Yvonnick) 135 | - 🐳 Improve Dockerfile and update docker image usage ([#109](https://github.com/frinyvonnick/gitmoji-changelog/issues/109)) [[55e24c2](https://github.com/frinyvonnick/gitmoji-changelog/commit/55e24c2bf8ae3ad2fd238b8480862498aa1a134e)] (by Baptiste Gauduchon) 136 | - 📝 Add docker usage section ([#99](https://github.com/frinyvonnick/gitmoji-changelog/issues/99)) [[53c96bc](https://github.com/frinyvonnick/gitmoji-changelog/commit/53c96bcd8c66f7a05e38739a1bde82585a3fee86)] (by Baptiste Gauduchon) 137 | - 🐳 Fix dependencies versions in Dockerfile ([#97](https://github.com/frinyvonnick/gitmoji-changelog/issues/97)) [[719541d](https://github.com/frinyvonnick/gitmoji-changelog/commit/719541dd414b62bba59178259a0830626ec70b2e)] (by Baptiste Gauduchon) 138 | - 📦 Add .npmignore file ([#73](https://github.com/frinyvonnick/gitmoji-changelog/issues/73)) [[b0a243f](https://github.com/frinyvonnick/gitmoji-changelog/commit/b0a243ffbfe5a84de4a8173f4aa79f19acc32b95)] (by Emmanuel DEMEY) 139 | - 🐳 Optimize context and image size [[a692310](https://github.com/frinyvonnick/gitmoji-changelog/commit/a692310da45654da5485622a189cc0e350684d0c)] (by Fabien JUIF) 140 | 141 | 142 | ## 1.1.0 (2018-11-10) 143 | 144 | ### Added 145 | 146 | - ✨ Check CLI version ([#52](https://github.com/frinyvonnick/gitmoji-changelog/issues/52)) [[18ead38](https://github.com/frinyvonnick/gitmoji-changelog/commit/18ead387032beda06f8097fbc3e8ad65d7268d42)] (by Fabien JUIF) 147 | - ✨ Do not override the top of the existing file ([#53](https://github.com/frinyvonnick/gitmoji-changelog/issues/53)) [[15edd6c](https://github.com/frinyvonnick/gitmoji-changelog/commit/15edd6cd49f672861e9b8195798d23746a39a7c1)] (by Fabien JUIF) 148 | - ✨ Add a breaking changes section ([#62](https://github.com/frinyvonnick/gitmoji-changelog/issues/62)) [[2a9904b](https://github.com/frinyvonnick/gitmoji-changelog/commit/2a9904b26843bb6721e2984049eb75d0914d4104)] (by Logan HAUSPIE) 149 | - ✨ Filter bookmark commit out ([#54](https://github.com/frinyvonnick/gitmoji-changelog/issues/54)) [[7858140](https://github.com/frinyvonnick/gitmoji-changelog/commit/78581402cde9f6ce4c3de6f558629a08941e9d0a)] (by Fabien JUIF) 150 | - ✨ Add the author in changelog lines ([#56](https://github.com/frinyvonnick/gitmoji-changelog/issues/56)) [[979da30](https://github.com/frinyvonnick/gitmoji-changelog/commit/979da30f5e52385b99bd4a58e1a946793bd1196d)] (by Benjamin Petetot) 151 | 152 | ### Fixed 153 | 154 | - 🐛 Remove empty versions ([#51](https://github.com/frinyvonnick/gitmoji-changelog/issues/51)) [[64ac5a7](https://github.com/frinyvonnick/gitmoji-changelog/commit/64ac5a72fe78877625a8b3cb77183a227a5fb881)] (by Fabien JUIF) 155 | - 🐛 Parse unicode emoji in commit ([#49](https://github.com/frinyvonnick/gitmoji-changelog/issues/49)) [[4d52747](https://github.com/frinyvonnick/gitmoji-changelog/commit/4d52747a585e46f118d39b4b1df199b41888b9c2)] (by Benjamin Petetot) 156 | - 🐛 Fix duplication with incremental update ([#60](https://github.com/frinyvonnick/gitmoji-changelog/issues/60)) [[c4ffc59](https://github.com/frinyvonnick/gitmoji-changelog/commit/c4ffc59c708452a5b9ed0d86f88031442ec44b82)] (by Benjamin Petetot) 157 | - 🐛 Fix emojies not identified in some commits ([#65](https://github.com/frinyvonnick/gitmoji-changelog/issues/65)) [[6a3b1b6](https://github.com/frinyvonnick/gitmoji-changelog/commit/6a3b1b642cb05c376079f4ad4a00a92445808722)] (by Benjamin Petetot) 158 | 159 | ### Miscellaneous 160 | 161 | - ⚗ Group similar commits together ([#50](https://github.com/frinyvonnick/gitmoji-changelog/issues/50)) [[06c8f3c](https://github.com/frinyvonnick/gitmoji-changelog/commit/06c8f3c5d9a28bbf2606cbe54b96f8932110fb10)] (by Fabien JUIF) 162 | - 🐳 Init dockerfile ([#61](https://github.com/frinyvonnick/gitmoji-changelog/issues/61)) [[afa7021](https://github.com/frinyvonnick/gitmoji-changelog/commit/afa70219878d39623f8337d479547a2cbadc4c4b)] (by Logan HAUSPIE) & ([#69](https://github.com/frinyvonnick/gitmoji-changelog/issues/69)) [[36c0e06](https://github.com/frinyvonnick/gitmoji-changelog/commit/36c0e061429e93b5ba6cb898f15ea874e4a60e67)] (by Charles-Henri GUERIN) 163 | 164 | 165 | 166 | ## 1.0.1 (2018-10-25) 167 | 168 | ### Fixed 169 | 170 | - 🐛 Prevent having right commits by version ([#30](https://github.com/frinyvonnick/gitmoji-changelog/issues/30)) ([de06319](https://github.com/frinyvonnick/gitmoji-changelog/commit/de063192baefebad16e05ce79061d815888a442f)) 171 | - 🐛 Get correct commit ranges from tags ([#32](https://github.com/frinyvonnick/gitmoji-changelog/issues/32)) ([573a2d0](https://github.com/frinyvonnick/gitmoji-changelog/commit/573a2d0b583b426d358c388af0ba1dc48a4e0ddf)) 172 | 173 | 174 | 175 | ## 1.0.0 (2018-10-25) 176 | 177 | ### Added 178 | 179 | - ✨ Generate full and partial changelog ([#23](https://github.com/frinyvonnick/gitmoji-changelog/issues/23)) ([e7c934a](https://github.com/frinyvonnick/gitmoji-changelog/commit/e7c934a1372344935ddd3739e32a9732d48dc0b8)) 180 | - ✨ Group commits by emojis group ([#11](https://github.com/frinyvonnick/gitmoji-changelog/issues/11)) ([7b8b49b](https://github.com/frinyvonnick/gitmoji-changelog/commit/7b8b49b366d3d51f0cc75f4ffb67efcd633cca16)) 181 | - ✨ Sort commits by emoji and date ([#24](https://github.com/frinyvonnick/gitmoji-changelog/issues/24)) ([08b2af3](https://github.com/frinyvonnick/gitmoji-changelog/commit/08b2af3241a13f6ebae9c24a3a51ef10b60f2879)) 182 | - ✨ Support commit's URL and issues autolink in changelog ([#16](https://github.com/frinyvonnick/gitmoji-changelog/issues/16)) ([871f164](https://github.com/frinyvonnick/gitmoji-changelog/commit/871f16499acee400863a94976e2520a4bdbc6cea)) 183 | - ✨ Add tag's date on version ([#22](https://github.com/frinyvonnick/gitmoji-changelog/issues/22)) ([ab3d9c6](https://github.com/frinyvonnick/gitmoji-changelog/commit/ab3d9c600e307dd0db16bc7abcbdf8a8a2c83ff5)) 184 | - ✨ Generate changelog JSON file ([86ca3ea](https://github.com/frinyvonnick/gitmoji-changelog/commit/86ca3eaefb18fd9c9b6bb4256ed2f6fa711aef59)) 185 | - ✨ Generate changelog markdown file ([b165f69](https://github.com/frinyvonnick/gitmoji-changelog/commit/b165f695f4c1a49ff16a5f03918545bfb36cf367)) 186 | 187 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at frin.yvonnick@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Develop and contribute 2 | 3 | ## Setup 4 | 5 | ```sh 6 | git clone git@github.com:frinyvonnick/gitmoji-changelog.git 7 | 8 | cd gitmoji-changelog && yarn 9 | ``` 10 | 11 | We are using lerna and yarn workspaces to split the library in modules: 12 | - [gitmoji-changelog-cli](https://github.com/frinyvonnick/gitmoji-changelog/tree/master/packages/gitmoji-changelog-cli) - the full-featured command line interface 13 | - [gitmoji-changelog-core](https://github.com/frinyvonnick/gitmoji-changelog/tree/master/packages/gitmoji-changelog-core) - the core lib generating changelog 14 | - [gitmoji-changelog-markdown](https://github.com/frinyvonnick/gitmoji-changelog/tree/master/packages/gitmoji-changelog-markdown) - the markdown changelog file writer 15 | 16 | ## Usage locally 17 | 18 | ```sh 19 | node [path-to-gitmoji-changelog-folder]/packages/gitmoji-changelog-cli/src/index.js 20 | ``` 21 | 22 | ## Run unit tests 23 | 24 | ```sh 25 | yarn test 26 | # or 27 | yarn test --watch 28 | ``` 29 | 30 | ## Run e2e tests 31 | 32 | ```sh 33 | yarn test:e2e 34 | ``` 35 | 36 | ## Run linter 37 | 38 | We are using [airbnb-base](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb-base) as linter: 39 | 40 | ```sh 41 | yarn lint 42 | ``` 43 | 44 | 45 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.14.1-alpine3.11 2 | ENV NODE_ENV=production 3 | 4 | # install dependencies 5 | RUN apk add --no-cache git=2.24.1-r0 6 | 7 | # build gitmoji-changelog from source 8 | WORKDIR /usr/src/gitmoji-changelog 9 | COPY . . 10 | RUN yarn --frozen-lockfile && yarn cache clean 11 | 12 | # run gitmoji-changelog on container startup 13 | RUN ln -s /usr/src/gitmoji-changelog/node_modules/.bin/gitmoji-changelog /usr/bin 14 | WORKDIR /app 15 | ENTRYPOINT ["gitmoji-changelog"] 16 | USER node 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 FRIN Yvonnick 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-changelog logo](https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/master/misc/logo.png) 2 | 3 |

4 | 5 | 6 | Gitmoji 7 | 8 | 9 | Twitter: YvonnickFrin 10 | 11 |

12 | 13 | > Generate changelog for repositories using [gitmoji](https://gitmoji.carloscuesta.me/) commits convention. 14 | 15 | 16 | ![gitmoji-changelog usage example](https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/master/misc/example.gif) 17 | 18 | ## 🚀 Usage 19 | 20 | Make sure you have [npx](https://www.npmjs.com/package/npx) installed (`npx` is shipped by default since npm `5.2.0`) 21 | 22 | Run the following command at the root of your project and answer questions. `gitmoji-changelog` uses a [preset system](https://docs.gitmoji-changelog.dev/#/?id=%e2%9a%99%ef%b8%8f-presets) to handle different type of project. The preset used by default is the Node.js one that look for project's information in the `package.json` file. 23 | 24 | with npx: 25 | ```sh 26 | npx gitmoji-changelog 27 | ``` 28 | 29 | with npm: 30 | ```sh 31 | npm install -g gitmoji-changelog 32 | 33 | cd my-project 34 | gitmoji-changelog 35 | ``` 36 | 37 | It exists a generic preset that works for every kind of project. It looks for information in a `.gitmoji-changelogrc` file at the root of your project. This file must contain three mandatory properties: `name`, `description` and `version`. 38 | 39 | .gitmoji-changelogrc: 40 | ```json 41 | { 42 | "project": { 43 | "name": "gitmoji-changelog", 44 | "description": "A changelog generator for gitmoji 😜", 45 | "version": "2.0.1" 46 | } 47 | } 48 | ``` 49 | 50 | You can change the preset used by `gitmoji-changelog` with the preset option. 51 | 52 | ```sh 53 | npx gitmoji-changelog --preset generic 54 | ``` 55 | 56 | ## 📖 Documentation 57 | 58 | :point_right: The full documentation is available [here](https://docs.gitmoji-changelog.dev). 59 | 60 | ## ✍ Author 61 | 62 | 👤 **Yvonnick FRIN (https://yvonnickfrin.dev)** 63 | 64 | * Twitter: [@YvonnickFrin](https://twitter.com/YvonnickFrin) 65 | * Github: [@frinyvonnick](https://github.com/frinyvonnick) 66 | 67 | ## 🤝 Contributing 68 | 69 | Contributions, issues and feature requests are welcome!
Feel free to check [issues page](https://github.com/frinyvonnick/gitmoji-changelog/issues). You can also take a look at our [contributing guide](CONTRIBUTING.md). 70 | 71 | ## 🙏 Show your support 72 | 73 | Give a ⭐️ if this project helped you! 74 | 75 | You also can add a badge in the README.md of your repository to promote `gitmoji-changelog`. All you need is to copy/past the code below: 76 | 77 | ```markdown 78 | [![gitmoji-changelog](https://img.shields.io/badge/Changelog-gitmoji-brightgreen.svg)](https://github.com/frinyvonnick/gitmoji-changelog) 79 | ``` 80 | 81 | It will add this badge: [![gitmoji-changelog](https://img.shields.io/badge/Changelog-gitmoji-brightgreen.svg)](https://github.com/frinyvonnick/gitmoji-changelog) 82 | 83 | 84 | ## ✨ Contributors 85 | 86 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 |

Yvonnick FRIN

💻

Benjamin Petetot

💻

Fabien JUIF

💻

Baptiste Gauduchon

💻

Franck Abgrall

💻

quentinncl

💻

Logan HAUSPIE

💻

Guillaume Membré

💻

Yann Bertrand

💻

s n

💻

Mathieu TUDISCO

💻

Charles-Henri GUERIN

💻

Florent Berthelot

💻

Emmanuel DEMEY

💻

Christopher Kade

📝

Rodion Martynov

📖

Daniel Tamkin

📖

Erno Salo

📖

Mark Lyck

💻

Lukáš Horák

💻

Julien WITTOUCK

💻
120 | 121 | 122 | 123 | 124 | 125 | 126 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 127 | 128 | ## 📝 License 129 | 130 | Copyright © 2020 [Yvonnick FRIN (https://github.com/frinyvonnick)](https://github.com/frinyvonnick).
131 | This project is [MIT](https://github.com/frinyvonnick/gitmoji-changelog/blob/master/LICENSE) licensed. 132 | 133 | *** 134 | _This README was generated with ❤️ by [readme-md-generator](https://github.com/kefranabg/readme-md-generator)_ 135 | -------------------------------------------------------------------------------- /doc/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/d0edd29b861584808f6c0ba0edf7c391d3ddcb33/doc/screenshot.png -------------------------------------------------------------------------------- /jest.config-e2e.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(e2e).[jt]s?(x)"], 3 | } 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "npmClient": "yarn", 4 | "useWorkspaces": true, 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "version": "2.3.0", 9 | "command": { 10 | "version": { 11 | "message": ":bookmark: Release %s" 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /misc/example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/d0edd29b861584808f6c0ba0edf7c391d3ddcb33/misc/example.gif -------------------------------------------------------------------------------- /misc/interactive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/d0edd29b861584808f6c0ba0edf7c391d3ddcb33/misc/interactive.gif -------------------------------------------------------------------------------- /misc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/d0edd29b861584808f6c0ba0edf7c391d3ddcb33/misc/logo.png -------------------------------------------------------------------------------- /misc/package.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitmoji-changelog", 3 | "version": "0.0.1", 4 | "description": "Some description", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT" 11 | } 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitmoji-changelog/workspace", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "Tools for generating changelog based on gitmoji convention https://gitmoji.carloscuesta.me/", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/frinyvonnick/gitmoji-changelog.git" 9 | }, 10 | "author": "Yvonnick FRIN (https://github.com/frinyvonnick)", 11 | "license": "MIT", 12 | "bugs": { 13 | "url": "https://github.com/frinyvonnick/gitmoji-changelog/issues" 14 | }, 15 | "homepage": "https://github.com/frinyvonnick/gitmoji-changelog#readme", 16 | "scripts": { 17 | "test": "jest", 18 | "test:e2e": "jest --config ./jest.config-e2e.js", 19 | "lint": "eslint packages/**/src" 20 | }, 21 | "devDependencies": { 22 | "all-contributors-cli": "^6.9.1", 23 | "eslint": "^5.4.0", 24 | "eslint-config-airbnb-base": "^13.1.0", 25 | "eslint-plugin-import": "^2.14.0", 26 | "jest": "^26.6.3", 27 | "lerna": "^4.0.0" 28 | }, 29 | "workspaces": [ 30 | "packages/*" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/README.md: -------------------------------------------------------------------------------- 1 | # gitmoji-changelog 2 | 3 | > Generate changelog for repositories using [gitmoji](https://gitmoji.carloscuesta.me/) commits convention. 4 | 5 | **Prerequisites:** 6 | - use [gitmoji](https://gitmoji.carloscuesta.me/) for commits convention 7 | - use [semver](https://semver.org/) for versions and tags convention 8 | 9 | ## Quick start 10 | 11 | ```bash 12 | npm install -g gitmoji-changelog 13 | 14 | cd my-project 15 | 16 | gitmoji-changelog 17 | ``` 18 | 19 | If `CHANGELOG.md` file doesn't exist, it will generate all previous changelog based on semver tags of your repo. 20 | 21 | If `CHANGELOG.md` file already exists, _this will not overwrite any previous changelog_, it will generate a changelog based on commits since the last semver tag that match. 22 | 23 | All available commands and parameters can be listed using: `gitmoji-changelog --help` 24 | 25 | **Here an example output:** [CHANGELOG.md](https://github.com/frinyvonnick/gitmoji-changelog/blob/master/CHANGELOG.md) 26 | 27 | ## Workflow 28 | 29 | Here the recommended workflow to generate your changelog file using `gitmoji-changelog`: 30 | 31 | **Important:** Before generating, be sure to have all tags locally (e.g. `git fetch origin`) 32 | 33 | 1. Make changes and commit: `git commit -m ":sparkles: my awesome feature"` 34 | 2. Bump version (ex: `1.0.0`) in `package.json` using [semver](https://semver.org/) convention 35 | 3. Run `gitmoji-changelog`, then the file `CHANGELOG.md` is created or updated with all changes 36 | 4. You can freely edit the new release in the changelog file, it will not be overwrite with the next generation 37 | 5. Commit `package.json` and `CHANGELOG.md` file 38 | 6. Tag your release: `git tag -a v1.0.0 -m "v1.0.0"` (or create a Github release) 39 | 7. Push to the remote `git push` 40 | 41 | ## Usage 42 | 43 | ```js 44 | gitmoji-changelog --help 45 | ``` 46 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gitmoji-changelog", 3 | "version": "2.3.0", 4 | "keywords": [ 5 | "gitmoji", 6 | "generate", 7 | "changelog", 8 | "semver", 9 | "cli", 10 | "nantes", 11 | "version", 12 | "release", 13 | "markdown" 14 | ], 15 | "description": "Gitmoji Changelog CLI", 16 | "main": "src/index.js", 17 | "engines": { 18 | "node": ">=10" 19 | }, 20 | "bin": { 21 | "gitmoji-changelog": "./src/index.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/frinyvonnick/gitmoji-changelog.git" 26 | }, 27 | "author": "Yvonnick FRIN (https://github.com/frinyvonnick)", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/frinyvonnick/gitmoji-changelog/issues" 31 | }, 32 | "homepage": "https://github.com/frinyvonnick/gitmoji-changelog#readme", 33 | "dependencies": { 34 | "@gitmoji-changelog/core": "^2.2.0", 35 | "@gitmoji-changelog/markdown": "^2.2.0", 36 | "hosted-git-info": "^3.0.2", 37 | "immutadot": "^1.0.0", 38 | "inquirer": "^6.3.1", 39 | "issue-reporter": "^0.2.0", 40 | "libnpm": "^3.0.1", 41 | "lodash": "^4.17.11", 42 | "pom-parser": "^1.2.0", 43 | "rc": "^1.2.8", 44 | "read-pkg-up": "^7.0.1", 45 | "semver": "^5.6.0", 46 | "semver-compare": "^1.0.0", 47 | "simple-git": "^1.113.0", 48 | "toml": "^3.0.0", 49 | "yaml": "^1.10.2", 50 | "yargs": "^17.3.1" 51 | }, 52 | "publishConfig": { 53 | "access": "public", 54 | "registry": "https://registry.npmjs.org/" 55 | }, 56 | "devDependencies": { 57 | "fs-extra": "^8.0.1" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/cli.e2e.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const fs = require('fs') 3 | const { removeSync } = require('fs-extra') 4 | const path = require('path') 5 | const simpleGit = require('simple-git/promise') 6 | const childProcess = require('child_process') 7 | 8 | expect.extend({ 9 | includes(str, items) { 10 | const pass = items.every(item => str.includes(item)) 11 | return { 12 | pass, 13 | message: () => pass 14 | ? `Expected ${str} to not includes ${items.join(', ')}` 15 | : `Expected ${str} to includes ${items.join(', ')}`, 16 | } 17 | }, 18 | }) 19 | 20 | expect.extend({ 21 | toDisplayError(str, message = '') { 22 | const pass = str.includes('Error') && str.includes(message) 23 | return { 24 | pass, 25 | message: () => pass ? 'It passes' : `Expected ${str} to includes Error and "${message}"`, 26 | } 27 | }, 28 | }) 29 | 30 | describe('generate changelog', () => { 31 | let testDir 32 | let repo 33 | 34 | beforeEach(async () => { 35 | testDir = path.join(os.tmpdir(), 'gitmoji-changelog') 36 | if (fs.existsSync(testDir)) removeSync(testDir) 37 | fs.mkdirSync(testDir) 38 | repo = simpleGit(testDir) 39 | await repo.init() 40 | fs.copyFileSync( 41 | path.join(__dirname, '..', '..', '..', 'misc', 'package.sample.json'), 42 | path.join(testDir, 'package.json'), 43 | ) 44 | }) 45 | 46 | afterEach(() => { 47 | removeSync(testDir) 48 | }) 49 | 50 | describe('presets', () => { 51 | it('should throw an Error if the preset could not find configuration', () => { 52 | fs.unlinkSync(path.join(testDir, 'package.json')) 53 | 54 | const output = gitmojiChangelog() 55 | 56 | expect(output.toString('utf8')).includes(['Cannot retrieve configuration']) 57 | }) 58 | 59 | it('should throw an Error if the preset did not return a version', () => { 60 | const packageJson = path.join(testDir, 'package.json') 61 | // eslint-disable-next-line global-require 62 | const content = JSON.parse(fs.readFileSync(packageJson).toString('utf8')) 63 | delete content.version 64 | fs.writeFileSync(packageJson, JSON.stringify(content)) 65 | 66 | const output = gitmojiChangelog() 67 | console.log(output.toString('utf8')) 68 | 69 | expect(output.toString('utf8')).includes(['Cannot retrieve the version from your configuration']) 70 | }) 71 | }) 72 | 73 | describe('init', () => { 74 | it("should get a 1.0.0 version while initializing changelog by calling cli without arguments and having package.json's version set to 1.0.0", async () => { 75 | await makeChanges('file1') 76 | await commit(':sparkles: Add some file') 77 | await bumpVersion('1.0.0') 78 | gitmojiChangelog() 79 | await commit(':bookmark: Version 1.0.0') 80 | 81 | expect(getChangelog()).includes(['1.0.0']) 82 | }) 83 | 84 | it("should get a 1.0.0 version while initializing changelog by calling cli with explicit init argument and having package.json's version set to 1.0.0", async () => { 85 | await makeChanges('file1') 86 | await commit(':sparkles: Add some file') 87 | await bumpVersion('1.0.0') 88 | gitmojiChangelog(['init']) 89 | await commit(':bookmark: Version 1.0.0') 90 | 91 | expect(getChangelog()).includes(['1.0.0']) 92 | }) 93 | 94 | it('should use a custom commit mapping to create commit categories', async () => { 95 | makeCustomConfig({ 96 | commitMapping: [ 97 | { group: 'style', label: 'Style', emojis: ['lipstick'] }, 98 | { group: 'changed', label: 'Changed', emojis: [] }, 99 | ], 100 | }) 101 | await makeChanges('file1') 102 | await commit(':sparkles: Add some file') 103 | await makeChanges('file2') 104 | await commit(':lipstick: Add some style file') 105 | await bumpVersion('1.0.0') 106 | gitmojiChangelog() 107 | await commit(':bookmark: Version 1.0.0') 108 | 109 | expect(getChangelog()).includes(['### Style']) 110 | expect(getChangelog()).not.includes(['### Changed']) 111 | }) 112 | 113 | it("should get a 1.0.0 version while initializing changelog by calling cli with 1.0.0 and having package.json's version set to 0.0.1", async () => { 114 | await makeChanges('file1') 115 | await commit(':sparkles: Add some file') 116 | gitmojiChangelog('1.0.0') 117 | await commit(':bookmark: Version 1.0.0') 118 | 119 | expect(getChangelog()).includes(['1.0.0']) 120 | }) 121 | 122 | it("should get a next version while initializing changelog by calling cli without arguments and having package.json's version set to 1.0.0", async () => { 123 | await makeChanges('file1') 124 | await commit(':sparkles: Add some file') 125 | await bumpVersion('1.0.0') 126 | await commit(':bookmark: Version 1.0.0') 127 | await tag('1.0.0') 128 | 129 | await makeChanges('file2') 130 | await commit(':sparkles: Add another file') 131 | gitmojiChangelog() 132 | 133 | expect(getChangelog()).includes(['1.0.0', 'next']) 134 | }) 135 | 136 | it("should get two versions 1.0.0 and 2.0.0 while initializing changelog by calling cli without and having package.json's version set to 1.0.0", async () => { 137 | await makeChanges('file1') 138 | await commit(':sparkles: Add some file') 139 | await bumpVersion('1.0.0') 140 | await commit(':bookmark: Version 1.0.0') 141 | await tag('1.0.0') 142 | 143 | await makeChanges('file2') 144 | await commit(':sparkles: Add another file') 145 | await bumpVersion('2.0.0') 146 | gitmojiChangelog() 147 | await commit(':bookmark: Version 2.0.0') 148 | 149 | expect(getChangelog()).includes(['1.0.0', '2.0.0']) 150 | }) 151 | }) 152 | 153 | describe('update', () => { 154 | it("should get a 1.0.0 version while initializing changelog by calling cli without arguments two times and having package.json's version set to 1.0.0", async () => { 155 | await bumpVersion('1.0.0') 156 | await makeChanges('file1') 157 | await commit(':sparkles: Add some file') 158 | gitmojiChangelog() 159 | await makeChanges('file2') 160 | await commit(':sparkles: Add a second file') 161 | gitmojiChangelog() 162 | 163 | expect(getChangelog()).includes(['second file']) 164 | }) 165 | 166 | it('should use a custom commit mapping to create commit categories', async () => { 167 | makeCustomConfig({ 168 | commitMapping: [ 169 | { group: 'style', label: 'Style', emojis: ['lipstick'] }, 170 | { group: 'changed', label: 'Changed', emojis: [] }, 171 | ], 172 | }) 173 | await bumpVersion('1.0.0') 174 | await makeChanges('file1') 175 | await commit(':sparkles: Add some file') 176 | await makeChanges('file3') 177 | await commit(':lipstick: Add some file') 178 | gitmojiChangelog() 179 | await makeChanges('file2') 180 | await commit(':sparkles: Add a second file') 181 | await makeChanges('file4') 182 | await commit(':lipstick: Add some file') 183 | gitmojiChangelog() 184 | 185 | expect(getChangelog()).includes(['### Style']) 186 | expect(getChangelog()).not.includes(['### Changed']) 187 | }) 188 | 189 | it("should get two versions 1.0.0 and next while updating changelog by calling cli without arguments and having package.json's version set to 1.0.0", async () => { 190 | await makeChanges('file1') 191 | await commit(':sparkles: Add some file') 192 | await bumpVersion('1.0.0') 193 | gitmojiChangelog() 194 | await commit(':bookmark: Version 1.0.0') 195 | await tag('1.0.0') 196 | 197 | await makeChanges('file2') 198 | await commit(':sparkles: Add another file') 199 | gitmojiChangelog() 200 | 201 | expect(getChangelog()).includes(['1.0.0', 'next']) 202 | }) 203 | it("should get two versions 1.0.0 and 2.0.0 while updating changelog by calling cli without arguments and having package.json's version set to 2.0.0", async () => { 204 | await makeChanges('file1') 205 | await commit(':sparkles: Add some file') 206 | await bumpVersion('1.0.0') 207 | gitmojiChangelog() 208 | await commit(':bookmark: Version 1.0.0') 209 | await tag('1.0.0') 210 | 211 | await makeChanges('file2') 212 | await commit(':sparkles: Add another file') 213 | await bumpVersion('2.0.0') 214 | gitmojiChangelog() 215 | 216 | expect(getChangelog()).includes(['1.0.0', '2.0.0']) 217 | }) 218 | 219 | it('should get two versions 1.0.0 and 2.0.0 while updating changelog after tagging a version 2.0.0', async () => { 220 | await makeChanges('file1') 221 | await commit(':sparkles: Add some file') 222 | await bumpVersion('1.0.0') 223 | gitmojiChangelog() 224 | await commit(':bookmark: Version 1.0.0') 225 | await tag('1.0.0') 226 | 227 | await makeChanges('file2') 228 | await commit(':sparkles: Add another file') 229 | await bumpVersion('2.0.0') 230 | await tag('2.0.0') 231 | gitmojiChangelog() 232 | 233 | expect(getChangelog()).includes(['1.0.0', '2.0.0']) 234 | }) 235 | 236 | it('should get two versions 1.0.0-alpha.1 and 1.0.0-alpha.2 while updating changelog after tagging a version 1.0.0-alpha.2', async () => { 237 | await makeChanges('file1') 238 | await commit(':sparkles: Add some file') 239 | await bumpVersion('1.0.0-alpha.1') 240 | gitmojiChangelog() 241 | await commit(':bookmark: Version 1.0.0-alpha.1') 242 | await tag('1.0.0-alpha.1') 243 | 244 | await makeChanges('file2') 245 | await commit(':sparkles: Add another file') 246 | await bumpVersion('1.0.0-alpha.2') 247 | await tag('1.0.0-alpha.2') 248 | gitmojiChangelog() 249 | 250 | expect(getChangelog()).includes(['1.0.0-alpha.1', '1.0.0-alpha.2']) 251 | }) 252 | 253 | it("should get two versions 1.0.0 and 2.0.0 while updating changelog by calling cli with 2.0.0 and having package.json's version set to 1.0.0", async () => { 254 | await makeChanges('file1') 255 | await commit(':sparkles: Add some file') 256 | await bumpVersion('1.0.0') 257 | gitmojiChangelog() 258 | await commit(':bookmark: Version 1.0.0') 259 | await tag('1.0.0') 260 | 261 | await makeChanges('file2') 262 | await commit(':sparkles: Add another file') 263 | gitmojiChangelog('2.0.0') 264 | 265 | expect(getChangelog()).includes(['1.0.0', '2.0.0']) 266 | }) 267 | it('should get three versions 1.0.0, 2.0.0, 3.0.0 while updating changelog by calling cli without arguments and skipping two tags creation 2.0.0 and 3.0.0', async () => { 268 | await makeChanges('file1') 269 | await commit(':sparkles: Add some file') 270 | await bumpVersion('1.0.0') 271 | gitmojiChangelog() 272 | await commit(':bookmark: Version 1.0.0') 273 | await tag('1.0.0') 274 | 275 | await makeChanges('file2') 276 | await commit(':sparkles: Add another file') 277 | await bumpVersion('2.0.0') 278 | await commit(':bookmark: Version 2.0.0') 279 | await tag('2.0.0') 280 | 281 | await makeChanges('file3') 282 | await commit(':sparkles: Add a third file') 283 | await bumpVersion('3.0.0') 284 | gitmojiChangelog() 285 | await commit(':bookmark: Version 3.0.0') 286 | await tag('3.0.0') 287 | 288 | expect(getChangelog()).includes(['1.0.0', '2.0.0', '3.0.0']) 289 | }) 290 | 291 | it('should get three versions 1.0.0, 2.0.0, 3.0.0 and next while updating changelog by calling cli without arguments and skipping two tags creation 2.0.0 and 3.0.0', async () => { 292 | await makeChanges('file1') 293 | await commit(':sparkles: Add some file') 294 | await bumpVersion('1.0.0') 295 | gitmojiChangelog() 296 | await commit(':bookmark: Version 1.0.0') 297 | await tag('1.0.0') 298 | 299 | await makeChanges('file2') 300 | await commit(':sparkles: Add another file') 301 | await bumpVersion('2.0.0') 302 | await commit(':bookmark: Version 2.0.0') 303 | await tag('2.0.0') 304 | 305 | await makeChanges('file3') 306 | await commit(':sparkles: Add a third file') 307 | await bumpVersion('3.0.0') 308 | await commit(':bookmark: Version 3.0.0') 309 | await tag('3.0.0') 310 | 311 | await makeChanges('file4') 312 | await commit(':sparkles: Add a fourth file') 313 | gitmojiChangelog() 314 | 315 | expect(getChangelog()).includes(['1.0.0', '2.0.0', '3.0.0', 'next']) 316 | }) 317 | 318 | it('should get two versions 1.0.0, 2.0.0 and next while updating changelog by calling cli without arguments', async () => { 319 | await makeChanges('file1') 320 | await commit(':sparkles: Add some file') 321 | await bumpVersion('1.0.0') 322 | gitmojiChangelog() 323 | await commit(':bookmark: Version 1.0.0') 324 | await tag('1.0.0') 325 | 326 | await makeChanges('file2') 327 | await commit(':sparkles: Add another file') 328 | await bumpVersion('2.0.0') 329 | gitmojiChangelog() 330 | await commit(':bookmark: Version 2.0.0') 331 | await tag('2.0.0') 332 | 333 | await makeChanges('file4') 334 | await commit(':sparkles: Add a fourth file') 335 | gitmojiChangelog() 336 | 337 | expect(getChangelog()).includes(['1.0.0', '2.0.0', 'next']) 338 | }) 339 | 340 | it('shouldn\'t generate changelog when gimoji-changelog if there isn\'t any changes', async () => { 341 | await makeChanges('file1') 342 | await commit(':sparkles: Add some file') 343 | await bumpVersion('1.0.0') 344 | gitmojiChangelog() 345 | await commit(':bookmark: Version 1.0.0') 346 | await tag('1.0.0') 347 | const output = gitmojiChangelog() 348 | 349 | expect(getChangelog()).includes(['1.0.0']) 350 | expect(output.toString('utf8')).toDisplayError('No changes found.') 351 | }) 352 | 353 | it('should get two versions 1.0.0 and next after two generation while updating changelog by calling cli without arguments', async () => { 354 | await makeChanges('file1') 355 | await commit(':sparkles: Add some file') 356 | await bumpVersion('1.0.0') 357 | gitmojiChangelog() 358 | await commit(':bookmark: Version 1.0.0') 359 | await tag('1.0.0') 360 | 361 | await makeChanges('file2') 362 | await commit(':sparkles: Add another file') 363 | gitmojiChangelog() 364 | 365 | await makeChanges('file4') 366 | await commit(':sparkles: Add a fourth file') 367 | gitmojiChangelog() 368 | 369 | expect(getChangelog()).includes(['1.0.0', 'next']) 370 | }) 371 | 372 | it('should get two versions 1.0.0 and 1.1.0 after three generations while updating changelog by calling cli with version 1.1.0', async () => { 373 | await makeChanges('file1') 374 | await commit(':sparkles: Add some file') 375 | await bumpVersion('1.0.0') 376 | gitmojiChangelog() 377 | await commit(':bookmark: Version 1.0.0') 378 | await tag('1.0.0') 379 | 380 | await makeChanges('file2') 381 | await commit(':sparkles: Add another file') 382 | gitmojiChangelog() 383 | 384 | await makeChanges('file4') 385 | await commit(':sparkles: Add a fourth file') 386 | gitmojiChangelog('1.1.0') 387 | 388 | expect(getChangelog()).includes(['1.0.0', '1.1.0']) 389 | }) 390 | 391 | it('should get two versions 1.0.0 and next after three generations while updating changelog by calling cli without arguments', async () => { 392 | await makeChanges('file1') 393 | await commit(':sparkles: Add some file') 394 | await bumpVersion('1.0.0') 395 | gitmojiChangelog() 396 | await commit(':bookmark: Version 1.0.0') 397 | await tag('1.0.0') 398 | 399 | await makeChanges('file2') 400 | await commit(':sparkles: Add another file') 401 | gitmojiChangelog('1.1.0') 402 | gitmojiChangelog() 403 | 404 | expect(getChangelog()).not.includes(['1.1.0']) 405 | expect(getChangelog()).includes(['1.0.0', 'next']) 406 | }) 407 | 408 | it('should get two versions 1.0.0 and 1.2.0 after three generations while updating changelog by calling cli without arguments', async () => { 409 | await makeChanges('file1') 410 | await commit(':sparkles: Add some file') 411 | await bumpVersion('1.0.0') 412 | gitmojiChangelog() 413 | await commit(':bookmark: Version 1.0.0') 414 | await tag('1.0.0') 415 | 416 | await makeChanges('file2') 417 | await commit(':sparkles: Add another file') 418 | gitmojiChangelog('1.1.0') 419 | gitmojiChangelog('1.2.0') 420 | 421 | expect(getChangelog()).not.includes(['1.1.0']) 422 | expect(getChangelog()).includes(['1.0.0', '1.2.0']) 423 | }) 424 | 425 | it('should work by passing release as argument without package.json or configuration file', async () => { 426 | const packageJson = path.join(testDir, 'package.json') 427 | const { version, ...content } = JSON.parse(fs.readFileSync(packageJson).toString('utf8')) 428 | fs.writeFileSync(packageJson, JSON.stringify(content)) 429 | 430 | await makeChanges('file1') 431 | await commit(':sparkles: Add some file') 432 | const output = gitmojiChangelog('1.0.0') 433 | console.log(output.toString('utf8')) 434 | 435 | expect(getChangelog()).includes(['1.0.0']) 436 | }) 437 | 438 | it('should display an error if requested version isn\'t semver', async () => { 439 | const output = gitmojiChangelog('awesomeversion') 440 | 441 | expect(output.toString('utf8')).toDisplayError() 442 | }) 443 | }) 444 | 445 | async function makeChanges(fileName) { 446 | fs.writeFileSync(path.join(testDir, fileName), '') 447 | } 448 | 449 | async function makeCustomConfig(config) { 450 | fs.writeFileSync(path.join(testDir, '.gitmoji-changelogrc'), JSON.stringify(config, undefined, 2)) 451 | } 452 | 453 | async function commit(message) { 454 | await repo.add('.') 455 | await repo.commit(message) 456 | } 457 | 458 | async function tag(version) { 459 | await repo.addTag(`v${version}`) 460 | } 461 | 462 | function gitmojiChangelog(args = []) { 463 | if (!Array.isArray(args)) { 464 | // eslint-disable-next-line no-param-reassign 465 | args = [args] 466 | } 467 | return childProcess.execFileSync('node', [path.join(__dirname, 'index.js'), ...args], { cwd: testDir }) 468 | } 469 | 470 | function getChangelog() { 471 | return fs.readFileSync(path.join(testDir, 'CHANGELOG.md')).toString('utf8') 472 | } 473 | 474 | function bumpVersion(to) { 475 | const packageJson = path.join(testDir, 'package.json') 476 | // eslint-disable-next-line global-require 477 | const content = fs.readFileSync(packageJson).toString('utf8') 478 | const { version } = JSON.parse(content) 479 | const updatedContent = content.replace(version, to) 480 | fs.writeFileSync(packageJson, updatedContent) 481 | } 482 | 483 | /* 484 | * This function is useful to print cli ouput when debugging tests 485 | */ 486 | // eslint-disable-next-line no-unused-vars 487 | function logOutput(ouput) { 488 | console.log(ouput.toString('utf8')) 489 | } 490 | }) 491 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/cli.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const { get } = require('lodash') 3 | const { set } = require('immutadot') 4 | const libnpm = require('libnpm') 5 | const semver = require('semver') 6 | const semverCompare = require('semver-compare') 7 | const rc = require('rc') 8 | 9 | const { generateChangelog, logger, FunctionalError } = require('@gitmoji-changelog/core') 10 | const { buildMarkdownFile, getLatestVersion } = require('@gitmoji-changelog/markdown') 11 | 12 | const issueReporter = require('issue-reporter') 13 | 14 | const { executeInteractiveMode } = require('./interactiveMode') 15 | const getRepositoryInfo = require('./repository') 16 | const packageJson = require('../package.json') 17 | 18 | async function getGitmojiChangelogLatestVersion() { 19 | const watchdog = new Promise(resolve => { 20 | setTimeout(resolve, 500, { version: packageJson.version }) 21 | }) 22 | const request = libnpm.manifest('gitmoji-changelog@latest') 23 | 24 | const { version } = await Promise.race([watchdog, request]) 25 | 26 | return version 27 | } 28 | 29 | async function main(options = {}) { 30 | logger.start(`gitmoji-changelog v${packageJson.version}`) 31 | logger.info(`${options.mode} ${options.output}`) 32 | 33 | const customConfiguration = rc('gitmoji-changelog') 34 | if (customConfiguration.configs) { 35 | logger.info('Custom configuration found') 36 | } else { 37 | logger.info('No custom configuration found') 38 | } 39 | 40 | try { 41 | const latestVersion = await getGitmojiChangelogLatestVersion() 42 | if (semverCompare(latestVersion, packageJson.version) > 0) { 43 | logger.warn(`You got an outdated version of gitmoji-changelog, please update! (yours: ${packageJson.version}, latest: ${latestVersion})`) 44 | logger.warn('Just do the following npm command to update it:') 45 | logger.warn('\t> npm install -g gitmoji-changelog@latest') 46 | } 47 | } catch (e) { /* ignore error */ } 48 | 49 | let projectInfo 50 | try { 51 | logger.info(`use preset ${options.preset}`) 52 | 53 | // eslint-disable-next-line global-require 54 | const loadProjectInfo = require(`./presets/${options.preset}.js`) 55 | projectInfo = await loadProjectInfo() 56 | 57 | if (!projectInfo) { 58 | throw Error(`Cannot retrieve configuration for preset ${options.preset}.`) 59 | } 60 | 61 | if (!projectInfo.version && options.release === 'from-package') { 62 | throw Error('Cannot retrieve the version from your configuration. Check it or you can do "gitmoji-changelog ".') 63 | } 64 | } catch (e) { 65 | logger.error(e) 66 | // Force quit if the requested preset doesn't exist 67 | return process.exit(0) 68 | } 69 | 70 | if (options.groupSimilarCommits) { 71 | logger.warn('⚗️ You are using a beta feature - may not working as expected') 72 | logger.warn('Feel free to open issues or PR into gitmoji-changelog') 73 | logger.warn('\t> https://github.com/frinyvonnick/gitmoji-changelog') 74 | } 75 | 76 | try { 77 | switch (options.format) { 78 | case 'json': { 79 | const changelog = await getChangelog(options, projectInfo) 80 | 81 | logMetaData(changelog) 82 | 83 | fs.writeFileSync(options.output, JSON.stringify(changelog)) 84 | break 85 | } 86 | default: { 87 | const lastVersion = await getLatestVersion(options.output) 88 | const newOptions = set(options, 'meta.lastVersion', lastVersion) 89 | 90 | // Handle the case where changelog file exist but there isn't a previous version 91 | if (options.mode === 'update' && !lastVersion) { 92 | newOptions.mode = 'init' 93 | 94 | fs.unlinkSync(options.output) 95 | } 96 | 97 | const changelog = await getChangelog(newOptions, projectInfo) 98 | 99 | logMetaData(changelog) 100 | await buildMarkdownFile(changelog, newOptions) 101 | } 102 | } 103 | logger.success(`changelog updated into ${options.output}`) 104 | } catch (e) { 105 | if (e.name !== 'FunctionalError') { 106 | const repository = await getRepositoryInfo() 107 | await issueReporter({ 108 | error: e, 109 | user: 'frinyvonnick', 110 | repo: 'gitmoji-changelog', 111 | sections: [ 112 | { 113 | title: 'CLI options', 114 | content: options, 115 | }, 116 | { 117 | title: 'Project info', 118 | content: projectInfo, 119 | }, 120 | { 121 | title: 'Repository info', 122 | content: repository, 123 | }, 124 | ], 125 | }) 126 | } else { 127 | logger.error(e) 128 | } 129 | } 130 | 131 | // force quit (if the latest version request is pending, we don't wait for it) 132 | return process.exit(0) 133 | } 134 | 135 | async function getChangelog(options, projectInfo) { 136 | const repository = await getRepositoryInfo() 137 | 138 | const release = options.release === 'from-package' ? projectInfo.version : options.release 139 | 140 | if (!semver.valid(release)) { 141 | throw new FunctionalError(`${release} is not a valid semver version.`) 142 | } 143 | 144 | const enhancedOptions = { 145 | ...options, 146 | release, 147 | } 148 | 149 | let changelog 150 | if (options.mode === 'init') { 151 | changelog = await generateChangelog('', release, enhancedOptions) 152 | } else { 153 | const { meta } = options 154 | const lastVersion = get(meta, 'lastVersion') 155 | 156 | changelog = await generateChangelog(lastVersion, release, enhancedOptions) 157 | } 158 | 159 | if (options.interactive) { 160 | changelog = await executeInteractiveMode(changelog) 161 | } 162 | 163 | changelog.meta.project = projectInfo 164 | changelog.meta.repository = repository 165 | 166 | return changelog 167 | } 168 | 169 | function logMetaData(changelog) { 170 | if (changelog.meta.project) { 171 | const { name, version } = changelog.meta.project 172 | logger.info(`${name} v${version}`) 173 | } 174 | if (changelog.meta.repository) { 175 | logger.info(changelog.meta.repository.url) 176 | } 177 | } 178 | 179 | module.exports = { main } 180 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/cli.spec.js: -------------------------------------------------------------------------------- 1 | const { generateChangelog, logger } = require('@gitmoji-changelog/core') 2 | const { manifest } = require('libnpm') 3 | const { main } = require('./cli') 4 | const issueReporter = require('issue-reporter') 5 | 6 | describe('cli', () => { 7 | const realExitFunction = process.exit 8 | const options = { preset: 'node' } 9 | beforeEach(() => { 10 | process.exit = jest.fn(() => {}) 11 | jest.clearAllMocks() 12 | }) 13 | afterEach(() => { 14 | process.exit = realExitFunction 15 | }) 16 | 17 | it('should throw an error if changelog generation fails', async () => { 18 | generateChangelog.mockRejectedValue(new Error()) 19 | 20 | await main(options) 21 | 22 | expect(issueReporter).toHaveBeenCalled() 23 | }) 24 | 25 | it('should call process.exit explicitly so promises are not waited for', async () => { 26 | await main(options) 27 | 28 | expect(process.exit).toHaveBeenCalledTimes(1) 29 | }) 30 | 31 | it('should call process.exit if preset does not exist', async () => { 32 | await main() 33 | 34 | expect(process.exit).toHaveBeenCalledTimes(1) 35 | }) 36 | 37 | describe('version control', () => { 38 | const findOutdatedMessage = () => logger.warn.mock.calls.find(([message]) => message.includes('outdated')) 39 | 40 | it('should print a warning about a new version', async () => { 41 | manifest.mockReturnValueOnce(Promise.resolve({ version: '2.0.0' })) 42 | await main(options) 43 | 44 | expect(findOutdatedMessage()).toBeTruthy() 45 | }) 46 | 47 | it('should NOT print a warning about a new version', async () => { 48 | // older version in npm registry 49 | manifest.mockReturnValueOnce(Promise.resolve({ version: '0.5.0' })) 50 | await main(options) 51 | 52 | // same version in npm registry 53 | manifest.mockReturnValueOnce(Promise.resolve({ version: '1.0.0' })) 54 | await main(options) 55 | 56 | expect(manifest).toHaveBeenCalledTimes(2) 57 | expect(findOutdatedMessage()).toBeFalsy() 58 | }) 59 | 60 | it('should NOT print a warning about a new version when request took to much time', async () => { 61 | manifest.mockImplementationOnce(() => new Promise((resolve) => { setTimeout(resolve, 1000, { version: '2.0.0' }) })) 62 | await main(options) 63 | 64 | expect(findOutdatedMessage()).toBeFalsy() 65 | }) 66 | 67 | it('should NOT print a warning about a new version when request is on error', async () => { 68 | manifest.mockReturnValueOnce(Promise.reject(new Error('faked error (manifest)'))) 69 | await main(options) 70 | 71 | expect(findOutdatedMessage()).toBeFalsy() 72 | }) 73 | }) 74 | }) 75 | 76 | jest.mock('@gitmoji-changelog/core', () => ({ 77 | generateChangelog: jest.fn(), 78 | logger: { 79 | start: jest.fn(), 80 | error: jest.fn(), 81 | success: jest.fn(), 82 | info: jest.fn(), 83 | warn: jest.fn(), 84 | }, 85 | })) 86 | 87 | jest.mock('../package.json', () => ({ 88 | version: '1.0.0', 89 | })) 90 | 91 | jest.mock('libnpm', () => ({ 92 | manifest: jest.fn(), 93 | })) 94 | 95 | jest.mock('issue-reporter') 96 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/index.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | const fs = require('fs') 3 | const yargs = require('yargs') 4 | const { noop } = require('lodash') 5 | 6 | const { homepage } = require('../package.json') 7 | const { main } = require('./cli') 8 | 9 | function getOutputFile({ output, format }) { 10 | if (output) { 11 | return output 12 | } 13 | if (format === 'json') { 14 | return './CHANGELOG.json' 15 | } 16 | return './CHANGELOG.md' 17 | } 18 | 19 | const execute = mode => argv => main({ 20 | ...argv, 21 | mode, 22 | output: getOutputFile(argv), 23 | }) 24 | 25 | yargs 26 | .usage('Usage: $0 [options]') 27 | 28 | .command('$0 [release]', 'Generate changelog', (command) => { 29 | command.positional('release', { 30 | desc: 'Next version (from-package, next)', 31 | default: 'from-package', 32 | }) 33 | }, (argv) => { 34 | const output = getOutputFile(argv) 35 | const existsOuput = fs.existsSync(output) 36 | const mode = existsOuput ? 'update' : 'init' 37 | execute(mode)(argv) 38 | }) 39 | 40 | .command('init', 'Initialize changelog from tags', noop, (argv) => execute('init')({ ...argv, release: 'from-package' })) 41 | 42 | .command('update [release]', 'Update changelog with a new version', (command) => { 43 | command.positional('release', { 44 | desc: 'Next version (from-package, next)', 45 | default: 'from-package', 46 | }) 47 | }, execute('update')) 48 | 49 | .option('format', { default: 'markdown', desc: 'changelog format (markdown, json)' }) 50 | .option('preset', { default: 'node', desc: 'define preset mode', choices: ['node', 'generic', 'maven', 'cargo', 'helm', 'python'] }) 51 | .option('output', { desc: 'output changelog file' }) 52 | .option('group-similar-commits', { desc: '[⚗️ - beta] try to group similar commits', default: false }) 53 | .option('author', { default: false, desc: 'add the author in changelog lines' }) 54 | .option('interactive', { default: false, desc: 'select commits manually', alias: 'i' }) 55 | 56 | .help('help') 57 | .epilog(`For more information visit: ${homepage}`) 58 | .parse() 59 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/interactiveMode.js: -------------------------------------------------------------------------------- 1 | const { get, isEmpty, cloneDeep } = require('lodash') 2 | const inquirer = require('inquirer') 3 | const { set } = require('immutadot') 4 | 5 | const interactiveMode = { 6 | buildFormattedChoices, 7 | getFilteredChangelog, 8 | executeInteractiveMode, 9 | } 10 | 11 | function buildFormattedChoices(changelog) { 12 | const formattedChoices = [] 13 | 14 | changelog.changes 15 | .forEach(change => { 16 | change.groups 17 | .forEach(group => { 18 | formattedChoices.push(new inquirer.Separator(`${group.label} (version ${change.version})`)) 19 | 20 | group.commits.forEach(commit => { 21 | formattedChoices.push({ 22 | name: `${get(commit, 'emoji', '')} ${commit.message}`, 23 | value: commit.hash, 24 | checked: true, 25 | }) 26 | }) 27 | }) 28 | }) 29 | 30 | return formattedChoices 31 | } 32 | 33 | function getFilteredChangelog(changelog, selectedCommitsHashes) { 34 | const changes = changelog.changes.map(change => { 35 | const groups = change.groups.map(group => { 36 | const filteredCommits = group.commits.filter(commit => { 37 | return selectedCommitsHashes.find(hash => commit.hash === hash) 38 | }) 39 | 40 | return set(group, 'commits', filteredCommits) 41 | }).filter(group => !isEmpty(group.commits)) 42 | 43 | return set(change, 'groups', groups) 44 | }).filter(change => !isEmpty(change.groups)) 45 | 46 | return { 47 | ...changelog, 48 | changes, 49 | } 50 | } 51 | 52 | async function executeInteractiveMode(initialChangelog) { 53 | const initialChangelogCopy = cloneDeep(initialChangelog) 54 | const formattedChoices = interactiveMode.buildFormattedChoices(initialChangelogCopy) 55 | const prompt = inquirer.createPromptModule() 56 | const question = { 57 | type: 'checkbox', 58 | name: 'selectedCommitsHashes', 59 | message: 'Select commits', 60 | choices: formattedChoices, 61 | pageSize: 10, 62 | } 63 | 64 | const { selectedCommitsHashes } = await prompt(question) 65 | 66 | return interactiveMode.getFilteredChangelog(initialChangelogCopy, selectedCommitsHashes) 67 | } 68 | 69 | module.exports = interactiveMode 70 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/interactiveMode.spec.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer') 2 | const interactiveMode = require('./interactiveMode') 3 | 4 | describe('interactiveMode', () => { 5 | const commitAddProcess = { 6 | hash: '00c90b7844c3d030e967721eafea1b436ee51a6b', 7 | author: 'Franck', 8 | date: '2019-05-19T10:57:22+02:00', 9 | subject: ':sparkles: Add interactive process', 10 | emojiCode: 'sparkles', 11 | emoji: '✨', 12 | message: 'Add interactive process', 13 | group: 'added', 14 | siblings: [], 15 | body: '', 16 | } 17 | 18 | const commitAddOption = { 19 | hash: '3092ffd56e35fff7e35e8a9fcb7fff53005eac8a', 20 | author: 'Franck', 21 | date: '2019-05-18T18:39:44+02:00', 22 | subject: ':sparkles: Add interactive option', 23 | emojiCode: 'sparkles', 24 | emoji: '✨', 25 | message: 'Add interactive option', 26 | group: 'added', 27 | siblings: [], 28 | body: '', 29 | } 30 | 31 | const commitUpgradeDeps = { 32 | hash: 'b77199f96c8570b827dfcb11907d6f4edac98823', 33 | author: 's n', 34 | date: '2019-04-23T15:55:19+02:00', 35 | subject: ':arrow_up: Update handlebar to 4.0.14 (#78)', 36 | emojiCode: 'arrow_up', 37 | emoji: '⬆️', 38 | message: 'Update handlebar to 4.0.14 (#78)', 39 | group: 'changed', 40 | siblings: [], 41 | body: '', 42 | } 43 | 44 | const commitAddAuthor = { 45 | hash: '979da30f5e52385b99bd4a58e1a946793bd1196d', 46 | author: 'Benjamin Petetot', 47 | date: '2018-10-30T09:33:52+01:00', 48 | subject: ':sparkles: Add the author in changelog lines (#56)', 49 | emojiCode: 'sparkles', 50 | emoji: '✨', 51 | message: 'Add the author in changelog lines (#56)', 52 | group: 'added', 53 | siblings: [], 54 | body: '', 55 | } 56 | 57 | const groupAddedVersionNext = { 58 | group: 'added', 59 | label: 'Added', 60 | commits: [ 61 | commitAddProcess, 62 | commitAddOption, 63 | ], 64 | } 65 | 66 | const groupChangedVersionNext = { 67 | group: 'changed', 68 | label: 'Changed', 69 | commits: [ 70 | commitUpgradeDeps, 71 | ], 72 | } 73 | 74 | const groupAddedVersionOne = { 75 | group: 'added', 76 | label: 'Added', 77 | commits: [ 78 | commitAddAuthor, 79 | ], 80 | } 81 | 82 | const versionNext = { 83 | version: 'next', 84 | groups: [ 85 | groupAddedVersionNext, 86 | groupChangedVersionNext, 87 | ], 88 | } 89 | 90 | const versionOne = { 91 | version: '1.1.0', 92 | date: '2018-11-15', 93 | groups: [ 94 | groupAddedVersionOne, 95 | ], 96 | } 97 | 98 | const initialChangelog = { 99 | changes: [ 100 | versionNext, 101 | versionOne, 102 | ], 103 | } 104 | 105 | beforeEach(() => { 106 | jest.clearAllMocks() 107 | }) 108 | 109 | describe('buildFormattedChoices', () => { 110 | it('should return a list of formatted choices from the given changelog', () => { 111 | const result = interactiveMode.buildFormattedChoices(initialChangelog) 112 | 113 | const expectedResult = [ 114 | new inquirer.Separator(`${groupAddedVersionNext.label} (version ${versionNext.version})`), 115 | { 116 | name: `${commitAddProcess.emoji} ${commitAddProcess.message}`, 117 | value: commitAddProcess.hash, 118 | checked: true, 119 | }, 120 | { 121 | name: `${commitAddOption.emoji} ${commitAddOption.message}`, 122 | value: commitAddOption.hash, 123 | checked: true, 124 | }, 125 | new inquirer.Separator(`${groupChangedVersionNext.label} (version ${versionNext.version})`), 126 | { 127 | name: `${commitUpgradeDeps.emoji} ${commitUpgradeDeps.message}`, 128 | value: commitUpgradeDeps.hash, 129 | checked: true, 130 | }, 131 | new inquirer.Separator(`${groupAddedVersionOne.label} (version ${versionOne.version})`), 132 | { 133 | name: `${commitAddAuthor.emoji} ${commitAddAuthor.message}`, 134 | value: commitAddAuthor.hash, 135 | checked: true, 136 | }, 137 | ] 138 | 139 | expect(result).toEqual(expectedResult) 140 | }) 141 | }) 142 | 143 | describe('getFilteredChangelog', () => { 144 | it('should return a new filtered changelog from the given inital changelog and selected commits', () => { 145 | const selectedCommitsHashes = [commitAddProcess.hash, commitAddAuthor.hash] 146 | 147 | const result = interactiveMode.getFilteredChangelog(initialChangelog, selectedCommitsHashes) 148 | 149 | const expectedResult = { 150 | changes: [ 151 | { 152 | version: 'next', 153 | groups: [{ 154 | group: 'added', 155 | label: 'Added', 156 | commits: [ 157 | commitAddProcess, 158 | ], 159 | }], 160 | }, 161 | { 162 | version: '1.1.0', 163 | date: '2018-11-15', 164 | groups: [ 165 | groupAddedVersionOne, 166 | ], 167 | }, 168 | ], 169 | } 170 | 171 | expect(result).toEqual(expectedResult) 172 | }) 173 | }) 174 | 175 | describe('executeInteractiveMode', () => { 176 | it('should call buildFormattedChoices, createPromptModule and getFilteredChangelog with correct parameters', async () => { 177 | const formattedChoices = [{ 178 | name: `${commitAddProcess.emoji} ${commitAddProcess.message}`, 179 | value: commitAddProcess.hash, 180 | checked: true, 181 | }] 182 | const selectedCommitsHashes = [commitAddProcess.hash, commitAddOption.hash] 183 | const prompt = jest.fn(() => Promise.resolve({ selectedCommitsHashes })) 184 | 185 | interactiveMode.buildFormattedChoices = jest.fn(() => formattedChoices) 186 | interactiveMode.getFilteredChangelog = jest.fn() 187 | inquirer.createPromptModule = jest.fn() 188 | inquirer.createPromptModule.mockReturnValueOnce(prompt) 189 | 190 | await interactiveMode.executeInteractiveMode(initialChangelog) 191 | 192 | expect(interactiveMode.buildFormattedChoices).toHaveBeenNthCalledWith(1, initialChangelog) 193 | expect(inquirer.createPromptModule).toHaveBeenCalledTimes(1) 194 | expect(prompt).toHaveBeenNthCalledWith(1, { 195 | type: 'checkbox', 196 | name: 'selectedCommitsHashes', 197 | message: 'Select commits', 198 | choices: formattedChoices, 199 | pageSize: 10, 200 | }) 201 | expect(interactiveMode.getFilteredChangelog) 202 | .toHaveBeenNthCalledWith(1, initialChangelog, selectedCommitsHashes) 203 | }) 204 | }) 205 | }) 206 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/cargo.js: -------------------------------------------------------------------------------- 1 | const toml = require('toml') 2 | const fs = require('fs') 3 | 4 | module.exports = async () => { 5 | try { 6 | const cargoPromise = new Promise((resolve, reject) => { 7 | try { 8 | resolve(toml.parse(fs.readFileSync('Cargo.toml', 'utf-8'))) 9 | } catch (err) { 10 | reject(err) 11 | } 12 | }) 13 | 14 | const { 15 | package: { 16 | name, 17 | version, 18 | description, 19 | }, 20 | } = await cargoPromise 21 | return { 22 | name: name, 23 | version: version, 24 | description: description, 25 | } 26 | } catch (e) { 27 | return null 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/generic.js: -------------------------------------------------------------------------------- 1 | const rc = require('rc') 2 | 3 | module.exports = () => { 4 | try { 5 | const customConfiguration = rc('gitmoji-changelog') 6 | if (!customConfiguration.configs) throw Error('Configuration not found') 7 | 8 | return customConfiguration.project 9 | } catch (e) { 10 | return null 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/generic.spec.js: -------------------------------------------------------------------------------- 1 | const rc = require('rc') 2 | 3 | const loadProjectInfo = require('./generic.js') 4 | 5 | describe('getPackageInfo', () => { 6 | it('should extract github repo info from configuration file', async () => { 7 | rc.mockImplementationOnce(() => ({ 8 | project: { 9 | name: 'gitmoji-changelog', 10 | version: '0.0.1', 11 | description: 'Gitmoji Changelog CLI', 12 | }, 13 | configs: [], 14 | })) 15 | 16 | const result = await loadProjectInfo() 17 | 18 | expect(result).toEqual({ 19 | name: 'gitmoji-changelog', 20 | version: '0.0.1', 21 | description: 'Gitmoji Changelog CLI', 22 | }) 23 | }) 24 | }) 25 | 26 | jest.mock('rc') 27 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/helm.js: -------------------------------------------------------------------------------- 1 | const yaml = require('yaml') 2 | const fs = require('fs') 3 | 4 | module.exports = async () => { 5 | try { 6 | const chartYamlPromise = new Promise((resolve, reject) => { 7 | try { 8 | resolve(yaml.parse(fs.readFileSync('Chart.yaml', 'utf-8'))) 9 | } catch (err) { 10 | reject(err) 11 | } 12 | }) 13 | 14 | const { 15 | name, 16 | version, 17 | description, 18 | } = await chartYamlPromise 19 | return { 20 | name: name, 21 | version: version, 22 | description: description, 23 | } 24 | } catch (e) { 25 | return null 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/helm.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const loadProjectInfo = require('./helm.js') 4 | 5 | describe('getPackageInfo', () => { 6 | it('should extract info from Chart.yaml', async () => { 7 | fs.readFileSync.mockReturnValue(` 8 | name: chart-name 9 | version: 0.1.1 10 | description: Description of the chart 11 | `) 12 | 13 | const result = await loadProjectInfo() 14 | 15 | expect(result).toEqual({ 16 | name: 'chart-name', 17 | version: '0.1.1', 18 | description: 'Description of the chart', 19 | }) 20 | }) 21 | }) 22 | 23 | jest.mock('fs') 24 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/maven.js: -------------------------------------------------------------------------------- 1 | const pomParser = require('pom-parser') 2 | 3 | module.exports = async () => { 4 | try { 5 | const opts = { 6 | filePath: 'pom.xml', 7 | } 8 | const pomPromise = new Promise((resolve, reject) => { 9 | pomParser.parse(opts, (err, pomResponse) => { 10 | if (err) { 11 | reject(err) 12 | return 13 | } 14 | 15 | resolve(pomResponse.pomObject) 16 | }) 17 | }) 18 | 19 | const { 20 | project: { 21 | groupid, artifactid, version, description, 22 | }, 23 | } = await pomPromise 24 | return { 25 | name: `${groupid}.${artifactid}`, 26 | version: version, 27 | description: description, 28 | } 29 | } catch (e) { 30 | return null 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/node.js: -------------------------------------------------------------------------------- 1 | const readPkgUp = require('read-pkg-up') 2 | 3 | module.exports = async () => { 4 | try { 5 | const packageInfo = await readPkgUp() 6 | 7 | if (!packageInfo.packageJson) throw Error('Empty package.json') 8 | 9 | return { 10 | name: packageInfo.packageJson.name, 11 | version: packageInfo.packageJson.version, 12 | description: packageInfo.packageJson.description, 13 | } 14 | } catch (e) { 15 | return null 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/node.spec.js: -------------------------------------------------------------------------------- 1 | const readPkgUp = require('read-pkg-up') 2 | 3 | const loadProjectInfo = require('./node.js') 4 | 5 | describe('getPackageInfo', () => { 6 | it('should extract github repo info from package.json', async () => { 7 | readPkgUp.mockImplementationOnce(() => Promise.resolve({ 8 | packageJson: { 9 | name: 'gitmoji-changelog', 10 | version: '0.0.1', 11 | description: 'Gitmoji Changelog CLI', 12 | }, 13 | })) 14 | 15 | const result = await loadProjectInfo() 16 | 17 | expect(result).toEqual({ 18 | name: 'gitmoji-changelog', 19 | version: '0.0.1', 20 | description: 'Gitmoji Changelog CLI', 21 | }) 22 | }) 23 | }) 24 | 25 | jest.mock('read-pkg-up') 26 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/python.js: -------------------------------------------------------------------------------- 1 | const toml = require('toml') 2 | const fs = require('fs') 3 | 4 | module.exports = async () => { 5 | try { 6 | const pyprojectPromise = new Promise((resolve, reject) => { 7 | try { 8 | resolve(toml.parse(fs.readFileSync('pyproject.toml', 'utf-8'))) 9 | } catch (err) { 10 | reject(err) 11 | } 12 | }) 13 | 14 | const projectFile = await pyprojectPromise 15 | const name = recursiveKeySearch('name', projectFile)[0] 16 | const version = recursiveKeySearch('version', projectFile)[0] 17 | let description = recursiveKeySearch('description', projectFile)[0] 18 | 19 | if (!name) { 20 | throw new Error('Could not find name metadata in pyproject.toml') 21 | } 22 | if (!version) { 23 | throw new Error('Could not find version metadata in pyproject.toml') 24 | } 25 | if (!description) { 26 | description = '' 27 | } 28 | 29 | return { 30 | name, 31 | version, 32 | description, 33 | } 34 | } catch (e) { 35 | return null 36 | } 37 | } 38 | 39 | 40 | function recursiveKeySearch(key, data) { 41 | // https://codereview.stackexchange.com/a/143914 42 | if (data === null) { 43 | return [] 44 | } 45 | 46 | if (data !== Object(data)) { 47 | return [] 48 | } 49 | 50 | let results = [] 51 | 52 | if (data.constructor === Array) { 53 | for (let i = 0, len = data.length; i < len; i += 1) { 54 | results = results.concat(recursiveKeySearch(key, data[i])) 55 | } 56 | return results 57 | } 58 | 59 | for (let i = 0; i < Object.keys(data).length; i += 1) { 60 | const dataKey = Object.keys(data)[i] 61 | if (key === dataKey) { 62 | results.push(data[key]) 63 | } 64 | results = results.concat(recursiveKeySearch(key, data[dataKey])) 65 | } 66 | 67 | return results 68 | } 69 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/presets/python.spec.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | 3 | const loadProjectInfo = require('./python.js') 4 | 5 | describe('getPackageInfo', () => { 6 | it('should extract metadata from a pyproject.toml made by poetry', async () =>{ 7 | // Note the TOML section is distinct for poetry 8 | fs.readFileSync.mockReturnValue(` 9 | [tool.poetry] 10 | name = "poetry-package-name" 11 | version = "0.1.0" 12 | description = "Description of the poetry package" 13 | `) 14 | 15 | const result = await loadProjectInfo() 16 | 17 | expect(result).toEqual({ 18 | name: 'poetry-package-name', 19 | version: '0.1.0', 20 | description: 'Description of the poetry package', 21 | }) 22 | }) 23 | 24 | it('should extract metadata from the PEP621 example pyproject.toml', async () =>{ 25 | // [project] is the usual TOML section for the metadata 26 | fs.readFileSync.mockReturnValue(` 27 | [project] 28 | name = "spam" 29 | version = "2020.0.0" 30 | description = "Lovely Spam! Wonderful Spam!" 31 | readme = "README.rst" 32 | requires-python = ">=3.8" 33 | license = {file = "LICENSE.txt"} 34 | keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"] 35 | authors = [ 36 | {email = "hi@pradyunsg.me"}, 37 | {name = "Tzu-Ping Chung"} 38 | ] 39 | maintainers = [ 40 | {name = "Brett Cannon", email = "brett@python.org"} 41 | ] 42 | classifiers = [ 43 | "Development Status :: 4 - Beta", 44 | "Programming Language :: Python" 45 | ] 46 | 47 | dependencies = [ 48 | "httpx", 49 | "gidgethub[httpx]>4.0.0", 50 | "django>2.1; os_name != 'nt'", 51 | "django>2.0; os_name == 'nt'" 52 | ] 53 | 54 | [project.optional-dependencies] 55 | test = [ 56 | "pytest < 5.0.0", 57 | "pytest-cov[all]" 58 | ] 59 | 60 | [project.urls] 61 | homepage = "example.com" 62 | documentation = "readthedocs.org" 63 | repository = "github.com" 64 | changelog = "github.com/me/spam/blob/master/CHANGELOG.md" 65 | 66 | [project.scripts] 67 | spam-cli = "spam:main_cli" 68 | 69 | [project.gui-scripts] 70 | spam-gui = "spam:main_gui" 71 | 72 | [project.entry-points."spam.magical"] 73 | tomatoes = "spam:main_tomatoes" 74 | `) 75 | 76 | const result = await loadProjectInfo() 77 | 78 | expect(result).toEqual({ 79 | name: 'spam', 80 | version: '2020.0.0', 81 | description: 'Lovely Spam! Wonderful Spam!', 82 | }) 83 | }) 84 | 85 | it('should extract metadata despite a missing description', async () =>{ 86 | // The description metadata is optional. 87 | fs.readFileSync.mockReturnValue(` 88 | [project] 89 | name = "no-description" 90 | version = "0.0.1" 91 | readme = "README.rst" 92 | `) 93 | 94 | const result = await loadProjectInfo() 95 | 96 | expect(result).toEqual({ 97 | name: 'no-description', 98 | version: '0.0.1', 99 | description: '', 100 | }) 101 | }) 102 | 103 | it('should use the first metadata value found from the top', async () =>{ 104 | // Only the first occurance of the expected key names are taken. 105 | fs.readFileSync.mockReturnValue(` 106 | [other.section] 107 | somebody = "once told me the" 108 | world = "is gonna roll me" 109 | 110 | [project] 111 | name = "project-1" 112 | version = "0.0.1" 113 | description = "Project 1 Description" 114 | 115 | [tool.poetry] 116 | name = "project-2" 117 | version = "0.0.2" 118 | description = "Project 2 Description" 119 | 120 | [tool.something.else] 121 | name = "project-3" 122 | version = "0.0.3" 123 | description = "Project 3 Description" 124 | `) 125 | 126 | const result = await loadProjectInfo() 127 | 128 | expect(result).toEqual({ 129 | name: 'project-1', 130 | version: '0.0.1', 131 | description: 'Project 1 Description', 132 | }) 133 | }) 134 | }) 135 | 136 | 137 | jest.mock('fs') 138 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/repository.js: -------------------------------------------------------------------------------- 1 | const gitRemoteOriginUrl = require('git-remote-origin-url') 2 | const hostedGitInfo = require('hosted-git-info') 3 | 4 | const { isEmpty } = require('lodash') 5 | 6 | async function getRepositoryInfo() { 7 | try { 8 | const url = await gitRemoteOriginUrl() 9 | const repo = hostedGitInfo.fromUrl(url) 10 | 11 | if (isEmpty(repo)) return null 12 | 13 | return { 14 | type: repo.type, 15 | domain: repo.domain, 16 | user: repo.user, 17 | project: repo.project, 18 | url: repo.browse(), 19 | bugsUrl: repo.bugs(), 20 | } 21 | } catch (e) { 22 | return null 23 | } 24 | } 25 | 26 | module.exports = getRepositoryInfo 27 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-cli/src/repository.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const gitRemoteOriginUrl = require('git-remote-origin-url') 3 | 4 | const getRepositoryInfo = require('./repository') 5 | 6 | describe('getRepositoryInfo', () => { 7 | it('should return null if no git info found', async () => { 8 | gitRemoteOriginUrl.mockImplementationOnce(() => Promise.resolve(null)) 9 | 10 | const result = await getRepositoryInfo() 11 | 12 | expect(result).toBeNull() 13 | }) 14 | 15 | it('should extract GitHub info', async () => { 16 | gitRemoteOriginUrl.mockImplementationOnce(() => Promise.resolve('git+https://github.com/frinyvonnick/gitmoji-changelog.git')) 17 | 18 | const result = await getRepositoryInfo() 19 | 20 | expect(result).toEqual({ 21 | type: 'github', 22 | domain: 'github.com', 23 | user: 'frinyvonnick', 24 | project: 'gitmoji-changelog', 25 | url: 'https://github.com/frinyvonnick/gitmoji-changelog', 26 | bugsUrl: 'https://github.com/frinyvonnick/gitmoji-changelog/issues', 27 | }) 28 | }) 29 | 30 | it('should extract gitlab repo info', async () => { 31 | gitRemoteOriginUrl.mockImplementationOnce(() => Promise.resolve('git+https://gitlab.com/gitlab-user/gitlab-project.git')) 32 | const result = await getRepositoryInfo() 33 | 34 | expect(result).toEqual({ 35 | type: 'gitlab', 36 | domain: 'gitlab.com', 37 | user: 'gitlab-user', 38 | project: 'gitlab-project', 39 | url: 'https://gitlab.com/gitlab-user/gitlab-project', 40 | bugsUrl: 'https://gitlab.com/gitlab-user/gitlab-project/issues', 41 | }) 42 | }) 43 | 44 | it('should extract bitbucket repo info', async () => { 45 | gitRemoteOriginUrl.mockImplementationOnce(() => Promise.resolve('https://username@bitbucket.org/bitbucket-account/bitbucket-project.git')) 46 | const result = await getRepositoryInfo() 47 | 48 | expect(result).toEqual({ 49 | type: 'bitbucket', 50 | domain: 'bitbucket.org', 51 | user: 'bitbucket-account', 52 | project: 'bitbucket-project', 53 | url: 'https://bitbucket.org/bitbucket-account/bitbucket-project', 54 | bugsUrl: undefined, 55 | }) 56 | }) 57 | }) 58 | 59 | jest.mock('git-remote-origin-url') 60 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitmoji-changelog/core", 3 | "version": "2.3.0", 4 | "description": "Core tool that transform raw commits into a nice json structure", 5 | "main": "src/index.js", 6 | "engines": { 7 | "node": ">=10" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/frinyvonnick/gitmoji-changelog.git" 12 | }, 13 | "author": "Yvonnick FRIN (https://github.com/frinyvonnick)", 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/frinyvonnick/gitmoji-changelog/issues" 17 | }, 18 | "homepage": "https://github.com/frinyvonnick/gitmoji-changelog#readme", 19 | "dependencies": { 20 | "concat-stream": "^1.6.2", 21 | "fast-levenshtein": "^2.0.6", 22 | "git-raw-commits": "^2.0.0", 23 | "git-remote-origin-url": "^2.0.0", 24 | "git-semver-tags": "^4.1.1", 25 | "lodash": "^4.17.11", 26 | "node-emoji": "^1.8.1", 27 | "normalize-package-data": "^2.4.0", 28 | "read-pkg-up": "^7.0.1", 29 | "semver": "^5.6.0", 30 | "semver-compare": "^1.0.0", 31 | "signale": "^1.3.0", 32 | "split-lines": "^2.0.0", 33 | "through2": "^2.0.3" 34 | }, 35 | "publishConfig": { 36 | "access": "public", 37 | "registry": "https://registry.npmjs.org/" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/errors.js: -------------------------------------------------------------------------------- 1 | class FunctionalError extends Error { 2 | constructor(message) { 3 | super(message) 4 | this.name = 'FunctionalError' 5 | } 6 | } 7 | 8 | module.exports = { 9 | FunctionalError, 10 | } 11 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/fromGitFile.js: -------------------------------------------------------------------------------- 1 | const gitRawCommits = require('git-raw-commits') 2 | const splitLines = require('split-lines') 3 | const { promisify } = require('util') 4 | const gitSemverTags = require('git-semver-tags') 5 | const through = require('through2') 6 | const concat = require('concat-stream') 7 | 8 | const COMMIT_FORMAT = '%n%H%n%an%n%cI%n%s%n%b' 9 | 10 | function parseCommit(commit) { 11 | const lines = splitLines(commit) 12 | const [hash, author, date, subject, ...body] = lines.splice( 13 | 1, 14 | lines.length - 2 15 | ) 16 | return { 17 | hash, author, date, subject, body, 18 | } 19 | } 20 | 21 | function getCommits(from, to) { 22 | return new Promise(resolve => { 23 | gitRawCommits({ 24 | format: COMMIT_FORMAT, 25 | from, 26 | to, 27 | }) 28 | .pipe( 29 | through.obj((data, enc, next) => { 30 | next(null, parseCommit(data.toString())) 31 | }) 32 | ) 33 | .pipe( 34 | concat(data => { 35 | resolve(data) 36 | }) 37 | ) 38 | }) 39 | } 40 | 41 | const getTags = promisify(gitSemverTags) 42 | 43 | module.exports = { 44 | getTags, 45 | getCommits, 46 | } 47 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/groupMapping.js: -------------------------------------------------------------------------------- 1 | module.exports = [ 2 | { 3 | group: 'added', 4 | label: 'Added', 5 | emojis: [ 6 | 'sparkles', 7 | 'tada', 8 | 'white_check_mark', 9 | 'construction_worker', 10 | 'chart_with_upwards_trend', 11 | 'heavy_plus_sign', 12 | 'loud_sound', 13 | ], 14 | }, 15 | { 16 | group: 'changed', 17 | label: 'Changed', 18 | emojis: [ 19 | 'art', 20 | 'zap', 21 | 'lipstick', 22 | 'rotating_light', 23 | 'arrow_down', 24 | 'arrow_up', 25 | 'pushpin', 26 | 'recycle', 27 | 'wrench', 28 | 'rewind', 29 | 'alien', 30 | 'truck', 31 | 'bento', 32 | 'wheelchair', 33 | 'speech_balloon', 34 | 'card_file_box', 35 | 'children_crossing', 36 | 'building_construction', 37 | 'iphone', 38 | ], 39 | }, 40 | { 41 | group: 'breaking_changes', 42 | label: 'Breaking changes', 43 | emojis: [ 44 | 'boom', 45 | ], 46 | }, 47 | { 48 | group: 'deprecated', 49 | label: 'Deprecated', 50 | emojis: [], 51 | }, 52 | { 53 | group: 'removed', 54 | label: 'Removed', 55 | emojis: ['fire', 'heavy_minus_sign', 'mute'], 56 | }, 57 | { 58 | group: 'fixed', 59 | label: 'Fixed', 60 | emojis: [ 61 | 'bug', 62 | 'ambulance', 63 | 'apple', 64 | 'penguin', 65 | 'checkered_flag', 66 | 'robot', 67 | 'green_apple', 68 | 'green_heart', 69 | 'pencil2', 70 | ], 71 | }, 72 | { 73 | group: 'security', 74 | label: 'Security', 75 | emojis: ['lock'], 76 | }, 77 | { 78 | group: 'useless', 79 | label: 'Useless', 80 | emojis: [ 81 | 'bookmark', 82 | ], 83 | }, 84 | { 85 | group: 'misc', 86 | label: 'Miscellaneous', 87 | emojis: [], 88 | }, 89 | ] 90 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/index.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const semverCompare = require('semver-compare') 3 | 4 | const { isEmpty } = require('lodash') 5 | 6 | const { parseCommit, getMergedGroupMapping } = require('./parser') 7 | const logger = require('./logger') 8 | const { groupSentencesByDistance } = require('./utils') 9 | const { FunctionalError } = require('./errors') 10 | const fromGitFileClient = require('./fromGitFile') 11 | 12 | const HEAD = '' 13 | const TAIL = '' 14 | 15 | function makeGroups(commits) { 16 | if (isEmpty(commits)) return [] 17 | 18 | const mapCommits = groups => { 19 | return groups 20 | .map(({ group, label }) => ({ 21 | group, 22 | label, 23 | commits: commits 24 | .filter(commit => commit.group === group) 25 | .sort((first, second) => second.date.localeCompare(first.date)), 26 | })) 27 | .filter(group => group.commits.length) 28 | } 29 | 30 | const mergedCommitMapping = getMergedGroupMapping() 31 | return mapCommits(mergedCommitMapping) 32 | } 33 | 34 | function sanitizeVersion(version) { 35 | try { 36 | return semver.valid(version, { 37 | loose: false, 38 | includePrerelease: true, 39 | }) 40 | } catch (e) { 41 | if (e.name === 'TypeError') { 42 | throw FunctionalError(e.message) 43 | } 44 | throw e 45 | } 46 | } 47 | 48 | function filterCommits(commits) { 49 | return commits 50 | .filter(commit => commit.group !== 'useless') 51 | } 52 | 53 | async function generateVersion(options) { 54 | const { 55 | from, 56 | to, 57 | version, 58 | groupSimilarCommits, 59 | client, 60 | } = options 61 | 62 | const rawCommits = await client.getCommits(from, to) 63 | 64 | let commits = filterCommits(rawCommits.map(parseCommit)) 65 | 66 | if (groupSimilarCommits) { 67 | commits = groupSentencesByDistance(commits.map(commit => commit.message)) 68 | .map(indexes => indexes.map(index => commits[index])) 69 | .map(([first, ...siblings]) => ({ 70 | ...first, 71 | siblings, 72 | })) 73 | } 74 | 75 | const result = { 76 | version, 77 | groups: makeGroups(commits), 78 | } 79 | 80 | if (version !== 'next') { 81 | result.date = getLastCommitDate(commits) 82 | } 83 | 84 | return result 85 | } 86 | 87 | function sortVersions(c1, c2) { 88 | if (c1.version === 'next') return -1 89 | if (c2.version === 'next') return 1 90 | return semverCompare(c2.version, c1.version) 91 | } 92 | 93 | function hasNextVersion(tags, release) { 94 | if (!release || release === 'next') return true 95 | try { 96 | return tags.some(tag => semver.eq(tag, release)) 97 | } catch (e) { 98 | if (e.name === 'TypeError') { 99 | throw FunctionalError(e.message) 100 | } 101 | throw e 102 | } 103 | } 104 | 105 | async function generateVersions({ 106 | tags, 107 | hasNext, 108 | release, 109 | groupSimilarCommits, 110 | client, 111 | }) { 112 | let nextTag = HEAD 113 | const targetVersion = hasNext ? 'next' : sanitizeVersion(release) 114 | const changes = await Promise.all(tags.map(async tag => { 115 | const version = sanitizeVersion(nextTag) || targetVersion 116 | const from = tag 117 | const to = nextTag 118 | nextTag = tag 119 | return generateVersion({ 120 | from, to, version, groupSimilarCommits, client, 121 | }) 122 | })) 123 | .then(versions => versions.sort(sortVersions)) 124 | 125 | return changes 126 | } 127 | 128 | async function generateChangelog(from, to, { 129 | groupSimilarCommits, client = fromGitFileClient, 130 | } = {}) { 131 | const gitTags = await client.getTags() 132 | let tagsToProcess = [...gitTags] 133 | const hasNext = hasNextVersion(gitTags, to) 134 | 135 | if (from === TAIL) { 136 | tagsToProcess = [...tagsToProcess, TAIL] 137 | } else { 138 | try { 139 | const fromIndex = tagsToProcess.findIndex(tag => semver.eq(tag, from)) 140 | tagsToProcess.splice(fromIndex + 1) 141 | 142 | if (hasNext && isEmpty(tagsToProcess)) { 143 | tagsToProcess.push(HEAD) 144 | } 145 | } catch (e) { 146 | if (e.name === 'TypeError') { 147 | throw FunctionalError(e.message) 148 | } 149 | throw e 150 | } 151 | } 152 | 153 | const changes = await generateVersions({ 154 | tags: tagsToProcess, 155 | hasNext, 156 | release: to, 157 | groupSimilarCommits, 158 | client, 159 | }) 160 | 161 | if (from !== TAIL && changes.length === 1 && isEmpty(changes[0].groups)) { 162 | throw new FunctionalError('No changes found. You may need to fetch or pull the last changes.') 163 | } 164 | 165 | return { 166 | meta: { 167 | lastVersion: sanitizeVersion(from), 168 | }, 169 | changes: changes.filter(({ groups }) => groups.length), 170 | } 171 | } 172 | 173 | function getLastCommitDate(commits) { 174 | if (isEmpty(commits)) return null 175 | 176 | return commits 177 | .map((commit) => new Date(commit.date)) 178 | .reduce((lastCommitDate, currentCommitDate) => { 179 | if (currentCommitDate > lastCommitDate) { 180 | return currentCommitDate 181 | } 182 | return lastCommitDate 183 | }) 184 | .toISOString().split('T')[0] 185 | } 186 | 187 | module.exports = { 188 | generateChangelog, 189 | logger, 190 | FunctionalError, 191 | } 192 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const gitRawCommits = require('git-raw-commits') 3 | const gitSemverTags = require('git-semver-tags') 4 | const rc = require('rc') 5 | 6 | const { generateChangelog } = require('./index') 7 | 8 | const TAIL = '' 9 | const HEAD = '' 10 | 11 | const uselessCommit = { 12 | hash: '460b79497ae7e791bc8ba8475bda8f0b93630dd3', 13 | date: '2018-09-14T21:00:18+02:00', 14 | subject: ':bookmark: Bump version to 1.9.2', 15 | body: 'Yes!', 16 | emoji: '🔖', 17 | emojiCode: 'bookmark', 18 | group: 'useless', 19 | message: 'Bump version to 1.9.2', 20 | } 21 | 22 | const lockCommit = { 23 | hash: '460b79497ae7e791bc8ba8475bda8f0b93630dd9', 24 | author: 'John Doe', 25 | date: '2018-09-14T22:00:18+02:00', 26 | subject: ':lock: Improve security', 27 | body: 'Yes!', 28 | emoji: '🔒', 29 | emojiCode: 'lock', 30 | group: 'security', 31 | message: 'Improve security', 32 | siblings: [], 33 | } 34 | 35 | const sparklesCommit = { 36 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23a', 37 | author: 'John Doe', 38 | date: '2018-08-28T10:06:00+02:00', 39 | subject: ':sparkles: Add a brand new feature', 40 | body: 'Waouh this is awesome 2', 41 | emoji: '✨', 42 | emojiCode: 'sparkles', 43 | group: 'added', 44 | message: 'Add a brand new feature', 45 | siblings: [], 46 | } 47 | 48 | const recycleCommit = { 49 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c', 50 | author: 'John Doe', 51 | date: '2018-08-01T10:07:00+02:00', 52 | subject: ':recycle: Make some reworking on code', 53 | body: 'Waouh this is awesome 3', 54 | emoji: '♻️', 55 | emojiCode: 'recycle', 56 | group: 'changed', 57 | message: 'Make some reworking on code', 58 | siblings: [], 59 | } 60 | 61 | const secondRecycleCommit = { 62 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23d', 63 | author: 'John Doe', 64 | date: '2018-08-30T10:07:00+02:00', 65 | subject: ':recycle: Upgrade another brand new feature', 66 | body: 'Waouh this is awesome 4', 67 | emoji: '♻️', 68 | emojiCode: 'recycle', 69 | group: 'changed', 70 | message: 'Upgrade another brand new feature', 71 | siblings: [], 72 | } 73 | 74 | const lipstickCommit = { 75 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23e', 76 | author: 'John Doe', 77 | date: '2018-08-10T10:07:00+02:00', 78 | subject: ':lipstick: Change graphics for a feature', 79 | body: 'Waouh this is awesome 5', 80 | emoji: '💄', 81 | emojiCode: 'lipstick', 82 | group: 'changed', 83 | message: 'Change graphics for a feature', 84 | siblings: [], 85 | } 86 | 87 | const secondLipstickCommit = { 88 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23f', 89 | author: 'John Doe', 90 | date: '2018-08-18T10:07:00+02:00', 91 | subject: ':lipstick: Change more graphics for a feature', 92 | body: 'Waouh this is awesome 6', 93 | emoji: '💄', 94 | emojiCode: 'lipstick', 95 | group: 'changed', 96 | message: 'Change more graphics for a feature', 97 | siblings: [], 98 | } 99 | 100 | describe('changelog', () => { 101 | beforeEach(() => { 102 | rc.mockImplementation(() => ({})) 103 | }) 104 | 105 | it('should generate changelog for next release on init', async () => { 106 | mockGroup([sparklesCommit]) 107 | 108 | gitSemverTags.mockImplementation(cb => cb(null, [])) 109 | 110 | const { changes } = await generateChangelog(TAIL, 'next') 111 | 112 | expect(changes).toEqual([ 113 | { 114 | version: 'next', 115 | groups: [ 116 | { 117 | group: 'added', 118 | label: 'Added', 119 | commits: expect.arrayContaining([expect.objectContaining(sparklesCommit)]), 120 | }, 121 | ], 122 | }, 123 | ]) 124 | }) 125 | 126 | it('should generate changelog using custom commit mapping', async () => { 127 | const customConfiguration = { 128 | commitMapping: [ 129 | { group: 'added', label: 'Added', emojis: ['sparkles'] }, 130 | { group: 'style', label: 'Style', emojis: ['lipstick'] }, 131 | { group: 'changed', label: 'Changed', emojis: [] }, 132 | ], 133 | } 134 | 135 | rc.mockImplementation(() => customConfiguration) 136 | mockGroup([sparklesCommit, lipstickCommit, lockCommit]) 137 | 138 | gitSemverTags.mockImplementation(cb => cb(null, [])) 139 | 140 | const { changes } = await generateChangelog(TAIL, 'next') 141 | 142 | expect(changes).toEqual([ 143 | { 144 | version: 'next', 145 | groups: [ 146 | { 147 | group: 'added', 148 | label: 'Added', 149 | commits: expect.arrayContaining([expect.objectContaining(sparklesCommit)]), 150 | }, 151 | { 152 | group: 'security', 153 | label: 'Security', 154 | commits: expect.arrayContaining([expect.objectContaining(lockCommit)]), 155 | }, 156 | { 157 | group: 'style', 158 | label: 'Style', 159 | commits: expect.arrayContaining([expect.objectContaining({ ...lipstickCommit, group: 'style' })]), 160 | }, 161 | ], 162 | }, 163 | ]) 164 | }) 165 | 166 | it('should generate changelog for next release', async () => { 167 | mockGroup([sparklesCommit]) 168 | 169 | gitSemverTags.mockImplementation(cb => cb(null, [])) 170 | 171 | const { changes } = await generateChangelog('v1.0.0', 'next') 172 | 173 | expect(changes).toEqual([ 174 | { 175 | version: 'next', 176 | groups: [ 177 | { 178 | group: 'added', 179 | label: 'Added', 180 | commits: expect.arrayContaining([expect.objectContaining(sparklesCommit)]), 181 | }, 182 | ], 183 | }, 184 | ]) 185 | }) 186 | 187 | it('should generate changelog for all tags', async () => { 188 | mockGroup([recycleCommit, secondRecycleCommit, lipstickCommit, secondLipstickCommit]) 189 | mockGroup([sparklesCommit]) 190 | 191 | gitSemverTags.mockImplementation(cb => cb(null, ['v1.0.0'])) 192 | 193 | const { changes } = await generateChangelog(TAIL, HEAD) 194 | 195 | expect(changes).toEqual([ 196 | { 197 | version: 'next', 198 | groups: [ 199 | { 200 | group: 'changed', 201 | label: 'Changed', 202 | commits: [ 203 | expect.objectContaining({ subject: secondRecycleCommit.subject }), 204 | expect.objectContaining({ subject: secondLipstickCommit.subject }), 205 | expect.objectContaining({ subject: lipstickCommit.subject }), 206 | expect.objectContaining({ subject: recycleCommit.subject }), 207 | ], 208 | }, 209 | ], 210 | }, 211 | { 212 | version: '1.0.0', 213 | date: '2018-08-28', 214 | groups: [ 215 | { 216 | group: 'added', 217 | label: 'Added', 218 | commits: [ 219 | expect.objectContaining({ subject: sparklesCommit.subject }), 220 | ], 221 | }, 222 | ], 223 | }, 224 | ]) 225 | }) 226 | 227 | it('should generate a changelog with only next since lastVersion is provided to v1.0.0', async () => { 228 | mockGroup([recycleCommit, secondRecycleCommit, lipstickCommit, secondLipstickCommit]) 229 | 230 | gitSemverTags.mockImplementation(cb => cb(null, ['v1.0.0'])) 231 | 232 | const { changes } = await generateChangelog('v1.0.0', 'next') 233 | 234 | expect(changes).toEqual([ 235 | { 236 | version: 'next', 237 | groups: [ 238 | { 239 | group: 'changed', 240 | label: 'Changed', 241 | commits: [ 242 | expect.objectContaining({ subject: secondRecycleCommit.subject }), 243 | expect.objectContaining({ subject: secondLipstickCommit.subject }), 244 | expect.objectContaining({ subject: lipstickCommit.subject }), 245 | expect.objectContaining({ subject: recycleCommit.subject }), 246 | ], 247 | }, 248 | ], 249 | }, 250 | ]) 251 | }) 252 | 253 | it('should group similar commits', async () => { 254 | mockGroup([]) 255 | mockGroup([recycleCommit, secondRecycleCommit, lipstickCommit, secondLipstickCommit]) 256 | 257 | gitSemverTags.mockImplementation(cb => cb(null, ['v1.0.0'])) 258 | 259 | const { changes } = await generateChangelog(TAIL, HEAD, { groupSimilarCommits: true }) 260 | 261 | expect(changes[0].groups[0].commits).toEqual([ 262 | secondRecycleCommit, 263 | { 264 | ...lipstickCommit, 265 | siblings: [secondLipstickCommit], 266 | }, 267 | recycleCommit, 268 | ]) 269 | }) 270 | 271 | it('should filter some commits out', async () => { 272 | gitRawCommits.mockReset() 273 | mockGroup([uselessCommit, lipstickCommit]) 274 | 275 | gitSemverTags.mockImplementation(cb => cb(null, [])) 276 | 277 | const { changes } = await generateChangelog(TAIL, HEAD) 278 | 279 | expect(changes).toEqual([ 280 | expect.objectContaining({ 281 | groups: [ 282 | expect.objectContaining({ 283 | commits: [lipstickCommit], 284 | }), 285 | ], 286 | }), 287 | ]) 288 | }) 289 | 290 | it('should throw an error if no commits', async () => { 291 | mockNoCommits() 292 | 293 | gitSemverTags.mockImplementation(cb => cb(null, [])) 294 | 295 | let message = false 296 | try { 297 | await generateChangelog('v1.0.0', 'next') 298 | } catch (e) { 299 | message = e.message 300 | } 301 | expect(message).toBeTruthy() 302 | }) 303 | 304 | it('should get previous tag in from', async () => { 305 | mockGroup([]) 306 | mockGroup([]) 307 | mockGroup([]) 308 | 309 | gitSemverTags.mockImplementation(cb => cb(null, ['v1.0.1', 'v1.0.0'])) 310 | 311 | await generateChangelog(TAIL, HEAD) 312 | 313 | expect(gitRawCommits).toHaveBeenCalledWith(expect.objectContaining({ from: 'v1.0.1', to: '' })) 314 | expect(gitRawCommits).toHaveBeenCalledWith( 315 | expect.objectContaining({ from: 'v1.0.0', to: 'v1.0.1' }) 316 | ) 317 | expect(gitRawCommits).toHaveBeenCalledWith(expect.objectContaining({ from: '', to: 'v1.0.0' })) 318 | }) 319 | 320 | it('should filter empty groups out', async () => { 321 | mockGroup([sparklesCommit]) 322 | mockGroup([recycleCommit]) 323 | mockGroup([]) // empty group 324 | mockGroup([lipstickCommit]) 325 | 326 | gitSemverTags.mockImplementation(cb => cb(null, ['v1.0.0', '1.0.0', 'v1.1.1'])) 327 | 328 | const { changes } = await generateChangelog(TAIL, HEAD) 329 | 330 | // inputs has 4 group (4 versions) 331 | // but output should only has 3, since the 3rd is empty 332 | expect(changes).toHaveLength(3) 333 | }) 334 | 335 | it('should sort commits by date', async () => { 336 | mockGroup([ 337 | { date: '2019-02-02T00:00:00+00:00', body: '3' }, 338 | { date: '2019-02-01T01:01:00+00:00', body: '7' }, 339 | { date: '2019-02-03T00:00:00+00:00', body: '2' }, 340 | { date: '2019-02-04T00:00:00+00:00', body: '1' }, 341 | { date: '2019-02-01T01:01:01+01:01', body: '4' }, 342 | { date: '2019-02-01T00:00:00+00:00', body: '9' }, 343 | { date: '2019-02-01T01:01:01+01:00', body: '5' }, 344 | { date: '2019-02-01T01:00:00+00:00', body: '8' }, 345 | { date: '2019-02-01T01:01:01+00:00', body: '6' }, 346 | ]) 347 | 348 | gitSemverTags.mockImplementation(cb => cb(null, [])) 349 | 350 | const { changes } = await generateChangelog(TAIL, HEAD) 351 | 352 | expect(changes[0].groups[0].commits.map(({ date, body }) => ({ date, body }))) 353 | .toEqual([ 354 | { date: '2019-02-04T00:00:00+00:00', body: '1' }, 355 | { date: '2019-02-03T00:00:00+00:00', body: '2' }, 356 | { date: '2019-02-02T00:00:00+00:00', body: '3' }, 357 | { date: '2019-02-01T01:01:01+01:01', body: '4' }, 358 | { date: '2019-02-01T01:01:01+01:00', body: '5' }, 359 | { date: '2019-02-01T01:01:01+00:00', body: '6' }, 360 | { date: '2019-02-01T01:01:00+00:00', body: '7' }, 361 | { date: '2019-02-01T01:00:00+00:00', body: '8' }, 362 | { date: '2019-02-01T00:00:00+00:00', body: '9' }, 363 | ]) 364 | }) 365 | }) 366 | 367 | jest.mock('git-raw-commits') 368 | jest.mock('git-semver-tags') 369 | jest.mock('rc') 370 | 371 | function mockGroup(commits) { 372 | gitRawCommits.mockImplementationOnce(() => { 373 | const stream = require('stream') 374 | const readable = new stream.Readable() 375 | commits.forEach(commit => { 376 | const { 377 | hash, author, date, subject, body, 378 | } = commit 379 | readable.push(`\n${hash}\n${author}\n${date}\n${subject}\n${body}\n`) 380 | }) 381 | readable.push(null) 382 | readable.emit('close') 383 | return readable 384 | }) 385 | } 386 | 387 | function mockNoCommits() { 388 | gitRawCommits.mockReset() 389 | 390 | gitRawCommits.mockImplementationOnce(() => { 391 | const stream = require('stream') 392 | const readable = new stream.Readable() 393 | readable.push(null) 394 | readable.emit('close') 395 | return readable 396 | }) 397 | } 398 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/logger.js: -------------------------------------------------------------------------------- 1 | const { Signale } = require('signale') 2 | 3 | const options = { 4 | disabled: false, 5 | interactive: false, 6 | stream: process.stdout, 7 | } 8 | 9 | const signale = new Signale(options) 10 | 11 | module.exports = signale 12 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/parser.js: -------------------------------------------------------------------------------- 1 | const nodeEmoji = require('node-emoji') 2 | const groupMapping = require('./groupMapping') 3 | const rc = require('rc') 4 | 5 | function parseSubject(subject) { 6 | if (!subject) return {} 7 | 8 | const unemojified = nodeEmoji.unemojify(subject) 9 | 10 | const matches = unemojified.match(/:(\w*):(.*)/) 11 | 12 | if (matches) { 13 | const [, emojiCode, message] = matches 14 | 15 | if (nodeEmoji.hasEmoji(emojiCode)) { 16 | return { 17 | emojiCode, 18 | emoji: nodeEmoji.get(emojiCode), 19 | message: nodeEmoji.emojify(message.trim()), 20 | } 21 | } 22 | } 23 | 24 | return { 25 | message: subject, 26 | } 27 | } 28 | 29 | function getMergedGroupMapping() { 30 | const customConfiguration = rc('gitmoji-changelog') 31 | const customGroupMapping = customConfiguration ? customConfiguration.commitMapping : undefined 32 | if (!customGroupMapping) return groupMapping 33 | const newCategories = customGroupMapping.filter(cg => { 34 | return !groupMapping.some(g => g.group === cg.group) 35 | }) 36 | 37 | const overridedCategories = groupMapping.map(group => { 38 | const customGroup = customGroupMapping.find(cg => cg.group === group.group) 39 | return customGroup || group 40 | }) 41 | 42 | const miscellaneousIndex = overridedCategories.findIndex(g => g.group === 'misc') 43 | const miscellaneousCategory = overridedCategories.splice(miscellaneousIndex, 1)[0] 44 | 45 | return [ 46 | ...overridedCategories, 47 | ...newCategories, 48 | miscellaneousCategory, 49 | ] 50 | } 51 | 52 | function getCommitGroup(emojiCode) { 53 | const group = getMergedGroupMapping() 54 | .find(({ emojis }) => emojis.includes(emojiCode)) 55 | if (!group) return 'misc' 56 | return group.group 57 | } 58 | 59 | function parseCommit({ 60 | hash, author, date, subject = '', body = '', 61 | }) { 62 | const { emoji, emojiCode, message } = parseSubject(subject) 63 | const group = getCommitGroup(emojiCode) 64 | 65 | return { 66 | hash, 67 | author, 68 | date, 69 | subject, 70 | emojiCode, 71 | emoji, 72 | message, 73 | group, 74 | siblings: [], 75 | body: Array.isArray(body) ? body.join('\n') : body, 76 | } 77 | } 78 | 79 | module.exports = { 80 | parseCommit, 81 | getCommitGroup, 82 | getMergedGroupMapping, 83 | } 84 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/parser.spec.js: -------------------------------------------------------------------------------- 1 | const { getMergedGroupMapping, parseCommit } = require('./parser.js') 2 | const rc = require('rc') 3 | 4 | const sparklesCommit = { 5 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23f', 6 | author: 'John Doe', 7 | date: '2018-08-28T10:06:00+02:00', 8 | subject: ':sparkles: Upgrade brand new feature', 9 | body: 'Waouh this is awesome 2', 10 | } 11 | 12 | describe('group mapping', () => { 13 | it('should place miscellaneous category at the end', () => { 14 | const mergeGroupMapping = getMergedGroupMapping([ 15 | { group: 'added', emojis: [] }, 16 | { group: 'custom', emojis: ['sparkles'] }, 17 | ]) 18 | 19 | expect(mergeGroupMapping.pop()).toEqual(expect.objectContaining({ group: 'misc' })) 20 | }) 21 | }) 22 | 23 | describe('commits parser', () => { 24 | it('should parse a single commit', () => { 25 | expect(parseCommit(sparklesCommit)).toEqual(expect.objectContaining(sparklesCommit)) 26 | }) 27 | 28 | it('should parse a unicode emoji', () => { 29 | const parsed = parseCommit({ 30 | ...sparklesCommit, 31 | subject: '✨ Upgrade brand new feature', 32 | }) 33 | expect(parsed.emoji).toEqual('✨') 34 | expect(parsed.emojiCode).toEqual('sparkles') 35 | expect(parsed.message).toEqual('Upgrade brand new feature') 36 | }) 37 | 38 | it('should parse a single commit without a body', () => { 39 | const parsed = parseCommit({ 40 | ...sparklesCommit, 41 | body: '', 42 | }) 43 | 44 | expect(parseCommit(parsed)).toEqual(expect.objectContaining({ 45 | ...sparklesCommit, 46 | body: '', 47 | })) 48 | }) 49 | 50 | it('should parse a single commit without a subject', () => { 51 | const { 52 | hash, 53 | author, 54 | date, 55 | } = sparklesCommit 56 | expect(parseCommit({ 57 | hash, 58 | author, 59 | date, 60 | })).toEqual(expect.objectContaining({ 61 | ...sparklesCommit, 62 | subject: '', 63 | body: '', 64 | })) 65 | }) 66 | 67 | it('should add the group to a commit', () => { 68 | expect(parseCommit(sparklesCommit)).toEqual(expect.objectContaining({ group: 'added' })) 69 | }) 70 | 71 | it('should handle custom commit mapping', () => { 72 | const customConfiguration = { 73 | commitMapping: [ 74 | { group: 'added', emojis: [] }, 75 | { group: 'custom', emojis: ['sparkles'] }, 76 | ], 77 | } 78 | rc.mockImplementation(() => customConfiguration) 79 | 80 | expect(parseCommit(sparklesCommit)).toEqual(expect.objectContaining({ group: 'custom' })) 81 | }) 82 | }) 83 | 84 | jest.mock('rc') 85 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/utils.js: -------------------------------------------------------------------------------- 1 | const { deburr } = require('lodash') 2 | const levenshtein = require('fast-levenshtein') 3 | 4 | // this is a magic number, this comes from various testing 5 | // feel free to tweak it 6 | const MAX_DISTANCE_PERCENT = 0.30 7 | 8 | function groupSentencesByDistance(texts = []) { 9 | const textsWithSortedWords = texts 10 | .map(text => ( 11 | // to basic latin characters 12 | deburr(text) 13 | // replace specials characters by a filler 14 | .replace(/[^\w\s]/gi, '▩') 15 | // split words 16 | .split(' ') 17 | // little words are replaces by fillers 18 | // this way -> we remove useless word like (a, of, etc) 19 | // we keep the string length for the algorithm 20 | .map(word => word.length < 4 ? Array.from({ length: word.length }).join('▩') : word) 21 | // we sort words 22 | .sort() 23 | // we make them a sentence 24 | .join('') 25 | )) 26 | 27 | const alreadyProcessedWords = new Set() 28 | const keyGroups = [] 29 | 30 | for ( 31 | let indexesFromStart = 0; 32 | indexesFromStart < textsWithSortedWords.length; 33 | indexesFromStart += 1 34 | ) { 35 | if (!alreadyProcessedWords.has(indexesFromStart)) { 36 | alreadyProcessedWords.add(indexesFromStart) 37 | const group = [indexesFromStart] 38 | keyGroups.push(group) 39 | 40 | for ( 41 | let indexesFromNext = indexesFromStart + 1; 42 | indexesFromNext < textsWithSortedWords.length; 43 | indexesFromNext += 1 44 | ) { 45 | const textA = textsWithSortedWords[indexesFromStart] 46 | const textB = textsWithSortedWords[indexesFromNext] 47 | const distance = levenshtein.get(textA, textB) 48 | const textAverageLength = (textA.length + textB.length) / 2 49 | 50 | if ( 51 | // close distance 52 | (textAverageLength * MAX_DISTANCE_PERCENT) >= distance 53 | // not already in a group 54 | && !alreadyProcessedWords.has(indexesFromNext) 55 | ) { 56 | group.push(indexesFromNext) 57 | alreadyProcessedWords.add(indexesFromNext) 58 | } 59 | } 60 | } 61 | } 62 | 63 | return keyGroups 64 | } 65 | 66 | module.exports = { 67 | groupSentencesByDistance, 68 | } 69 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-core/src/utils.spec.js: -------------------------------------------------------------------------------- 1 | const { groupSentencesByDistance } = require('./utils') 2 | 3 | describe('utils', () => { 4 | describe('groupSentencesByDistance', () => { 5 | it('should group values together', () => { 6 | const messages = [ 7 | 'add levenshtein', // 0 - group1 8 | 'fix a bug about failures graph', // 1 - group2 9 | 'levenshtein', // 2 - group1 10 | 'fix levenshtein', // 3 - group1 11 | 'nothing to group with me', 12 | 'fix a graph of failures bug', // 5 - group2 13 | ] 14 | 15 | expect(groupSentencesByDistance(messages)).toEqual([[0, 2, 3], [1, 5], [4]]) 16 | }) 17 | }) 18 | }) 19 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-documentation/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/d0edd29b861584808f6c0ba0edf7c391d3ddcb33/packages/gitmoji-changelog-documentation/.nojekyll -------------------------------------------------------------------------------- /packages/gitmoji-changelog-documentation/README.md: -------------------------------------------------------------------------------- 1 | ![gitmoji-changelog logo](https://raw.githubusercontent.com/frinyvonnick/gitmoji-changelog/master/misc/logo.png) 2 | 3 | ## 🚀 Usage 4 | 5 | Make sure you have [npx](https://www.npmjs.com/package/npx) installed (`npx` is shipped by default since npm `5.2.0`) 6 | 7 | Run the following command at the root of your project and answer questions. `gitmoji-changelog` uses a [preset system](?id=⚙%ef%b8%8f-presets) to handle different type of project. The preset used by default is the Node.js one that look for project's information in the `package.json` file. 8 | 9 | with npx: 10 | ```sh 11 | npx gitmoji-changelog 12 | ``` 13 | 14 | with npm: 15 | ```sh 16 | npm install -g gitmoji-changelog 17 | 18 | cd my-project 19 | gitmoji-changelog 20 | ``` 21 | 22 | It exists a generic preset that works for every kind of project. It looks for information in a `.gitmoji-changelogrc` file at the root of your project. This file must contain three mandatory properties: `name`, `description` and `version`. 23 | 24 | .gitmoji-changelogrc: 25 | ```json 26 | { 27 | "project": { 28 | "name": "gitmoji-changelog", 29 | "description": "A changelog generator for gitmoji 😜", 30 | "version": "2.0.1" 31 | } 32 | } 33 | ``` 34 | 35 | You can change the preset used by `gitmoji-changelog` with the preset option. 36 | 37 | ```sh 38 | npx gitmoji-changelog --preset generic 39 | ``` 40 | 41 | 42 | ### Available commands 43 | 44 | ```sh 45 | gitmoji-changelog [release] 46 | gitmoji-changelog init 47 | gitmoji-changelog update [release] 48 | ``` 49 | 50 | The first command listed above is the idiomatic usage of `gitmoji-changelog` (read the `How it works` for more information). 51 | 52 | `release` argument is by default valued to `from-package`. It will retrieve the version from the selected `preset` (read `Presets` for more information). You can overwrite it with the tag you want to generate in the changelog. 53 | 54 | ### Options 55 | 56 | | option | description | default value | 57 | |-------------------------|-----------------------------------------|------------------------------------| 58 | | --version | display version | | 59 | | --format | changelog format (markdown, json) | markdown | 60 | | --preset | define preset mode | node | 61 | | --output | output file path | ./CHANGELOG.md or ./CHANGELOG.json | 62 | | --group-similar-commits | [⚗️,- beta] try to group similar commits | false | 63 | | --author | add the author in changelog lines | false | 64 | | --interactive -i | select commits manually | false | 65 | | --help | display help | | 66 | 67 | ### Example 68 | 69 | **Here an example output:** [CHANGELOG.md](https://github.com/frinyvonnick/gitmoji-changelog/blob/master/CHANGELOG.md) 70 | 71 | ## 📚 How it works 72 | 73 | ### Behavior 74 | 75 | `CHANGELOG.md` doesn't exist: 76 | 77 | The CLI will generate all previous changelog based on semver tags of your repo. 78 | 79 | `CHANGELOG.md` exists: 80 | 81 | _All previous semvers tags remain unchanged_. The CLI will add each tag since the last semver tag found in the `CHANGELOG.md` file. 82 | 83 | **By default when you generate your changelog with `gitmoji-changelog`, the following mapping is used to group commits: [groupMapping.js](https://github.com/frinyvonnick/gitmoji-changelog/blob/master/packages/gitmoji-changelog-core/src/groupMapping.js)** 84 | 85 | ### Workflow 86 | 87 | Here the recommended workflow to generate your changelog file using `gitmoji-changelog`: 88 | 89 | **Important:** Before generating, be sure to have all tags locally (e.g. `git fetch origin`) 90 | 91 | 1. Make changes and commit: `git commit -m ":sparkles: my awesome feature"` 92 | 2. Bump version (ex: `1.0.0`) in `package.json` using [semver](https://semver.org/) convention 93 | 3. Run `gitmoji-changelog`, then the file `CHANGELOG.md` is created or updated with all changes 94 | 4. You can freely edit the new release in the changelog file, it will not be overwritten with the next generation 95 | 5. Commit `package.json` and `CHANGELOG.md` file 96 | 6. Tag your release: `git tag -a v1.0.0 -m "v1.0.0"` (or create a Github release) 97 | 7. Push to the remote `git push --follow-tags` 98 | 99 | _This workflow is related to the `node` preset but can be adapted to your own technology._ 100 | 101 | ## ⚙️ Presets 102 | 103 | `gitmoji-changelog` use presets to get project metadata useful for its smooth operation. Here is the list of available presets: 104 | 105 | - node (default preset) 106 | - generic 107 | - maven 108 | - cargo 109 | - helm 110 | 111 | Didn't see the preset you need in the list? Consider adding it. Presets are stored in a [presets](https://github.com/frinyvonnick/gitmoji-changelog/blob/master/packages/gitmoji-changelog-cli/src/presets) folder in the `cli` package. 112 | 113 | ### Existing presets 114 | 115 | #### Node 116 | 117 | The node preset looks for a `package.json` file. 118 | 119 | ```json 120 | { 121 | "name": "project-name", 122 | "version": "0.0.1", 123 | "description": "Some description", 124 | } 125 | ``` 126 | 127 | #### Generic 128 | 129 | The generic preset looks a `gitmoji-changelogrc` file. 130 | 131 | ```json 132 | { 133 | "project": { 134 | "name": "yvonnickfrin.dev", 135 | "description": "My blog", 136 | "version": "1.1.0" 137 | } 138 | } 139 | ``` 140 | 141 | It must contain all mandatory properties in a `project` property. The `gitmoji-changelogrc` file can contain other configuration properties like a custom commit categorization. 142 | 143 | #### Maven 144 | 145 | The maven preset looks for 4 properties in you `pom.xml`: 146 | 147 | - groupid 148 | - artifactid 149 | - version 150 | - description 151 | 152 | #### Cargo 153 | 154 | The cargo preset looks for 3 properties in your `Cargo.toml`: 155 | 156 | - name 157 | - version 158 | - description 159 | 160 | #### Helm 161 | 162 | The helm preset looks for 3 properties in your `Chart.yaml`: 163 | 164 | - name 165 | - version 166 | - description 167 | 168 | #### Python 169 | 170 | The python preset looks for 3 properties in your `pyproject.toml`: 171 | 172 | - name 173 | - version 174 | - description 175 | 176 | (The value taken is the first one found in your `pyproject.toml` that matches the expected key name given above.) 177 | 178 | ### Add a preset 179 | 180 | A preset need to export a function. When called this function must return three mandatory information about the project in which the cli has been called. The name of the project, a short description of it and its current version. 181 | 182 | Let's dissect the `node` preset to see how it works. First of all the module must export a function. In case something went wrong return `null`. The cli will tell the user a problem occurred. 183 | 184 | ```js 185 | module.exports = () => { 186 | return null 187 | } 188 | ``` 189 | 190 | There is a package called `read-pkg-up` to get the first `package.json` in the parent folder structure. It returns its content as a JavaScript object. If we can't find a `package.json` or it is empty we return `null`. 191 | 192 | ```js 193 | const readPkgUp = require('read-pkg-up') 194 | 195 | module.exports = async () => { 196 | try { 197 | const packageInfo = await readPkgUp() 198 | 199 | if (!packageInfo.packageJson) { 200 | throw Error('Empty package.json') 201 | } 202 | } catch (e) { 203 | return null 204 | } 205 | } 206 | ``` 207 | 208 | If everything went fine we return the three mandatory information (name, description, version). 209 | 210 | ```js 211 | const readPkgUp = require('read-pkg-up') 212 | 213 | module.exports = async () => { 214 | try { 215 | const packageInfo = await readPkgUp() 216 | 217 | if (!packageInfo.packageJson) { 218 | throw Error('Empty package.json') 219 | } 220 | 221 | return { 222 | name: packageInfo.packageJson.name, 223 | description: packageInfo.packageJson.description, 224 | version: packageInfo.packageJson.version, 225 | } 226 | } catch (e) { 227 | return null 228 | } 229 | } 230 | ``` 231 | 232 | That's it. Feel free to open an issue to ask for a new preset or open a pull request to add one. 233 | 234 | All preset needs at least 3 pieces of informations to work: 235 | 236 | - A project name 237 | - A current version 238 | - A description 239 | 240 | They have their own way to get these. 241 | 242 | 243 | ## 🐳 Using Docker image 244 | 245 | Launch `gitmoji-changelog` using the [official Docker image](https://hub.docker.com/r/yvonnick/gitmoji-changelog): 246 | ```sh 247 | docker container run -it -v $(pwd):/app --rm yvonnick/gitmoji-changelog:latest 248 | ``` 249 | 250 | > `/app` is the directory where gitmoji-changelog expect your project in the container. 251 | 252 | You can also build the image locally and use it directly: 253 | ```sh 254 | # build the image: 255 | docker image build -t yvonnick/gitmoji-changelog:dev . 256 | # use it: 257 | docker container run -it -v $(pwd):/app --rm yvonnick/gitmoji-changelog:dev 258 | ``` 259 | 260 | ### Supported tags and respective Dockerfile links 261 | 262 | * [yvonnick/gitmoji-changelog:latest](https://github.com/frinyvonnick/gitmoji-changelog/blob/master/Dockerfile) 263 | 264 | 265 | ## 🐥 Canary version 266 | 267 | If you want to test the incoming release of gitmoji-changelog, you can use or install the canary version. 268 | Be aware, it's a development in progress version, feel free to report any bugs plus give feedback. 269 | 270 | with npx: 271 | ```sh 272 | npx gitmoji-changelog@canary --version 273 | ``` 274 | 275 | with npm: 276 | ```sh 277 | npm install -g gitmoji-changelog@canary 278 | ``` 279 | 280 | ## 📝 License 281 | 282 | Copyright © 2020 [Yvonnick FRIN (https://github.com/frinyvonnick)](https://github.com/frinyvonnick).
283 | This project is [MIT](https://github.com/frinyvonnick/gitmoji-changelog/blob/master/LICENSE) licensed. 284 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-documentation/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | gitmoji-changelog | docs 6 | 7 | 8 | 9 | 10 | 11 | 12 |
Loading...
13 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-documentation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": "true", 3 | "name": "@gitmoji-changelog/documentation", 4 | "version": "2.2.0", 5 | "main": "index.html", 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "docsify serve" 9 | }, 10 | "devDependencies": { 11 | "docsify-cli": "^4.4.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-markdown/.npmignore: -------------------------------------------------------------------------------- 1 | *.spec.js -------------------------------------------------------------------------------- /packages/gitmoji-changelog-markdown/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gitmoji-changelog/markdown", 3 | "version": "2.3.0", 4 | "description": "Gitmoji Changelog markdown formatter", 5 | "main": "src/index.js", 6 | "bin": { 7 | "gitmoji-changelog": "./src/index.js" 8 | }, 9 | "engines": { 10 | "node": ">=10" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/frinyvonnick/gitmoji-changelog.git" 15 | }, 16 | "author": "Yvonnick FRIN (https://github.com/frinyvonnick)", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/frinyvonnick/gitmoji-changelog/issues" 20 | }, 21 | "homepage": "https://github.com/frinyvonnick/gitmoji-changelog#readme", 22 | "dependencies": { 23 | "git-semver-tags": "^4.1.1", 24 | "handlebars": "^4.0.14", 25 | "immutadot": "^1.0.0", 26 | "lodash": "^4.17.11", 27 | "semver": "^5.6.0" 28 | }, 29 | "publishConfig": { 30 | "access": "public", 31 | "registry": "https://registry.npmjs.org/" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-markdown/src/index.js: -------------------------------------------------------------------------------- 1 | const semver = require('semver') 2 | const { promisify } = require('util') 3 | const fs = require('fs') 4 | const path = require('path') 5 | const { Transform } = require('stream') 6 | const handlebars = require('handlebars') 7 | const { update } = require('immutadot') 8 | const { isEmpty } = require('lodash') 9 | const gitSemverTags = require('git-semver-tags') 10 | 11 | const gitSemverTagsAsync = promisify(gitSemverTags) 12 | 13 | const MARKDOWN_TEMPLATE = path.join(__dirname, 'template.md') 14 | 15 | function buildMarkdownFile(changelog = {}, options = {}) { 16 | if (options.mode === 'init') { 17 | return markdownFromScratch(changelog, options) 18 | } 19 | return markdownIncremental(changelog, options) 20 | } 21 | 22 | function mapCommit(meta, options) { 23 | const { author } = options 24 | 25 | return commit => ({ 26 | ...commit, 27 | hash: getShortHash(commit.hash, meta.repository), 28 | subject: autolink(commit.subject, meta.repository), 29 | message: autolink(commit.message, meta.repository), 30 | body: autolink(commit.body, meta.repository), 31 | author: author ? commit.author : null, 32 | siblings: commit.siblings.map(mapCommit(meta, options)), 33 | }) 34 | } 35 | 36 | function toMarkdown({ meta, changes }, options) { 37 | const template = fs.readFileSync(MARKDOWN_TEMPLATE, 'utf-8') 38 | const compileTemplate = handlebars.compile(template) 39 | const changelog = update(changes, '[:].groups[:].commits[:]', mapCommit(meta, options)) 40 | 41 | return compileTemplate({ changelog }) 42 | } 43 | 44 | function markdownFromScratch({ meta, changes }, options) { 45 | return promisify(fs.writeFile)(options.output, `# Changelog\n\n${toMarkdown({ meta, changes }, options)}`) 46 | } 47 | 48 | function markdownIncremental({ meta, changes }, options) { 49 | const { lastVersion } = meta 50 | const { output } = options 51 | 52 | const tempFile = `${output}.tmp` 53 | 54 | return new Promise((resolve, reject) => { 55 | const readStream = fs.createReadStream(output, { encoding: 'utf-8' }) 56 | const writeStream = fs.createWriteStream(tempFile, { encoding: 'utf-8' }) 57 | 58 | let previousNextFound = false 59 | let previousVersionFound = false 60 | let nextVersionWritten = false 61 | 62 | readStream 63 | .pipe(new Transform({ 64 | transform(chunk, encoding, callback) { 65 | const string = chunk.toString() 66 | 67 | callback( 68 | null, 69 | string.split('\n') 70 | .reduce( 71 | (content, nextLine, index, array) => { 72 | previousVersionFound = matchVersionBreakpoint(nextLine, lastVersion) 73 | previousNextFound = previousNextFound || matchVersionBreakpoint(nextLine) 74 | 75 | // Remove old release (next version) 76 | if (previousNextFound && !previousVersionFound && !nextVersionWritten) { 77 | return content 78 | } 79 | 80 | // Rewrite the release (next version) 81 | if (previousVersionFound && !nextVersionWritten) { 82 | nextVersionWritten = true 83 | return `${content}${toMarkdown({ meta, changes }, options)}${nextLine}\n` 84 | } 85 | 86 | // Just push the line without changing anything 87 | if (index !== array.length - 1) { 88 | return `${content}${nextLine}\n` 89 | } 90 | return `${content}${nextLine}` 91 | }, 92 | '', 93 | ) 94 | ) 95 | }, 96 | })) 97 | .pipe(writeStream) 98 | .on('error', reject) 99 | .on('close', () => { 100 | fs.rename(tempFile, output, resolve) 101 | }) 102 | }) 103 | } 104 | 105 | function matchVersionBreakpoint(tested, version = '.*') { 106 | const regex = new RegExp(``) 107 | return regex.test(tested) 108 | } 109 | 110 | async function getLatestVersion(markdownFile) { 111 | let markdownContent 112 | 113 | try { 114 | markdownContent = fs.readFileSync(markdownFile, 'utf-8') 115 | } catch (err) { 116 | return null 117 | } 118 | 119 | const versions = markdownContent.match(/<\/a>/g) 120 | 121 | if (!versions) return null 122 | 123 | const [lastVersion, previousVersion] = versions 124 | 125 | const tags = await gitSemverTagsAsync() 126 | const result = lastVersion.match(/<\/a>/) 127 | const isNext = result[1] === 'next' || !tags.some(tag => semver.eq(tag, result[1])) 128 | if (!isNext) return result[1] 129 | 130 | if (!previousVersion) return null 131 | 132 | const previousResult = previousVersion.match(/<\/a>/) 133 | return previousResult[1] 134 | } 135 | 136 | function getShortHash(hash, repository) { 137 | if (!hash) return null 138 | 139 | const shortHash = hash.slice(0, 7) 140 | 141 | if (isEmpty(repository) || !repository.url) return shortHash 142 | 143 | return `[${shortHash}](${repository.url}/commit/${hash})` 144 | } 145 | 146 | const ISSUE_REGEXP = /#{1}(\d+)/gm 147 | 148 | function autolink(message, repository) { 149 | if (!message) return null 150 | 151 | if (isEmpty(repository) || !repository.bugsUrl) return message 152 | 153 | const matches = message.match(ISSUE_REGEXP) 154 | if (!matches) return message 155 | 156 | return message.replace(ISSUE_REGEXP, `[#$1](${repository.bugsUrl}/$1)`) 157 | } 158 | 159 | module.exports = { 160 | buildMarkdownFile, 161 | matchVersionBreakpoint, 162 | getShortHash, 163 | autolink, 164 | getLatestVersion, 165 | } 166 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-markdown/src/index.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable global-require */ 2 | const fs = require('fs') 3 | const { Writable, Readable } = require('stream') 4 | 5 | const { 6 | buildMarkdownFile, 7 | matchVersionBreakpoint, 8 | autolink, 9 | getShortHash, 10 | } = require('./index') 11 | 12 | describe('Markdown converter', () => { 13 | it('should generate full changelog into markdown from scratch', async () => { 14 | fs.writeFile = jest.fn((path, content, cb) => cb(null, 'done')) 15 | 16 | const changelog = { 17 | meta: { 18 | repository: { 19 | type: 'github', 20 | domain: 'github.com', 21 | user: 'frinyvonnick', 22 | project: 'gitmoji-changelog', 23 | url: 'https://github.com/frinyvonnick/gitmoji-changelog', 24 | bugsUrl: 'https://github.com/frinyvonnick/gitmoji-changelog/issues', 25 | }, 26 | }, 27 | changes: [ 28 | { 29 | version: 'next', 30 | groups: [ 31 | { 32 | group: 'changed', 33 | label: 'Changed', 34 | commits: [ 35 | { 36 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c', 37 | author: 'John Doe', 38 | date: '2018-08-28T10:07:00+02:00', 39 | subject: ':recycle: Upgrade brand new feature', 40 | emoji: '♻️', 41 | message: 'Upgrade brand new feature', 42 | body: 'Waouh this is awesome 3', 43 | siblings: [], 44 | }, 45 | ], 46 | }, 47 | ], 48 | }, 49 | { 50 | version: '1.0.0', 51 | date: '2018-08-28', 52 | groups: [ 53 | { 54 | group: 'added', 55 | label: 'Added', 56 | commits: [ 57 | { 58 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23f', 59 | author: 'John Doe', 60 | date: '2018-08-28T10:06:00+02:00', 61 | subject: ':sparkles: Upgrade brand new feature', 62 | emoji: '✨', 63 | message: 'Upgrade brand new feature', 64 | body: 'Waouh this is awesome 2', 65 | siblings: [], 66 | }, 67 | ], 68 | }, 69 | ], 70 | }, 71 | ], 72 | } 73 | 74 | await buildMarkdownFile(changelog, { mode: 'init', output: './CHANGELOG.md' }) 75 | 76 | expect(fs.writeFile).toHaveBeenCalledTimes(1) 77 | expect(fs.writeFile.mock.calls[0][0]).toBe('./CHANGELOG.md') 78 | expect(fs.writeFile.mock.calls[0][1]).toEqual(`# Changelog 79 | 80 | 81 | ## next 82 | 83 | ### Changed 84 | 85 | - ♻️ Upgrade brand new feature [[c40ee86](https://github.com/frinyvonnick/gitmoji-changelog/commit/c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c)] 86 | 87 | 88 | 89 | ## 1.0.0 (2018-08-28) 90 | 91 | ### Added 92 | 93 | - ✨ Upgrade brand new feature [[c40ee86](https://github.com/frinyvonnick/gitmoji-changelog/commit/c40ee8669ba7ea5151adc2942fa8a7fc98d9e23f)] 94 | 95 | 96 | `) 97 | }) 98 | 99 | it('should generate full changelog into markdown from scratch with author', async () => { 100 | fs.writeFile = jest.fn((path, content, cb) => cb(null, 'done')) 101 | 102 | const changelog = { 103 | meta: { 104 | repository: { 105 | type: 'github', 106 | domain: 'github.com', 107 | user: 'frinyvonnick', 108 | project: 'gitmoji-changelog', 109 | url: 'https://github.com/frinyvonnick/gitmoji-changelog', 110 | bugsUrl: 'https://github.com/frinyvonnick/gitmoji-changelog/issues', 111 | }, 112 | }, 113 | changes: [ 114 | { 115 | version: 'next', 116 | groups: [ 117 | { 118 | group: 'changed', 119 | label: 'Changed', 120 | commits: [ 121 | { 122 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c', 123 | author: 'John Doe', 124 | date: '2018-08-28T10:07:00+02:00', 125 | subject: ':recycle: Upgrade brand new feature', 126 | emoji: '♻️', 127 | message: 'Upgrade brand new feature', 128 | body: 'Waouh this is awesome 3', 129 | siblings: [], 130 | }, 131 | ], 132 | }, 133 | ], 134 | }, 135 | ], 136 | } 137 | 138 | await buildMarkdownFile(changelog, { mode: 'init', output: './CHANGELOG.md', author: true }) 139 | 140 | expect(fs.writeFile).toHaveBeenCalledTimes(1) 141 | expect(fs.writeFile.mock.calls[0][0]).toBe('./CHANGELOG.md') 142 | expect(fs.writeFile.mock.calls[0][1]).toEqual(`# Changelog 143 | 144 | 145 | ## next 146 | 147 | ### Changed 148 | 149 | - ♻️ Upgrade brand new feature [[c40ee86](https://github.com/frinyvonnick/gitmoji-changelog/commit/c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c)] (by John Doe) 150 | 151 | 152 | `) 153 | }) 154 | 155 | it('should generate incremental markdown changelog', async () => { 156 | let result 157 | fs.createWriteStream = jest.fn(() => new Writable({ 158 | write(chunk, encoding, callback) { 159 | result = chunk.toString() 160 | callback() 161 | this.emit('close') 162 | }, 163 | })) 164 | fs.createReadStream = jest.fn(() => { 165 | let index = 0 166 | return new Readable({ 167 | read() { 168 | if (index === 0) { 169 | this.push(`# Changelog 170 | > Custom header 171 | 172 | 173 | ## 1.0.0 174 | I am the last version 175 | `) 176 | index += 1 177 | return 178 | } 179 | 180 | this.push(null) 181 | }, 182 | }) 183 | }) 184 | fs.rename = jest.fn((fileA, fileB, cb) => cb(null, 'done')) 185 | 186 | const changelog = { 187 | meta: { 188 | repository: { 189 | type: 'github', 190 | domain: 'github.com', 191 | user: 'frinyvonnick', 192 | project: 'gitmoji-changelog', 193 | url: 'https://github.com/frinyvonnick/gitmoji-changelog', 194 | bugsUrl: 'https://github.com/frinyvonnick/gitmoji-changelog/issues', 195 | }, 196 | lastVersion: '1.0.0', 197 | }, 198 | changes: [ 199 | { 200 | version: 'next', 201 | groups: [ 202 | { 203 | group: 'changed', 204 | label: 'Changed', 205 | commits: [ 206 | { 207 | hash: 'c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c', 208 | author: 'John Doe', 209 | date: '2018-08-28T10:07:00+02:00', 210 | subject: ':recycle: Upgrade brand new feature', 211 | emoji: '♻️', 212 | message: 'Upgrade brand new feature', 213 | body: 'Waouh this is awesome 3', 214 | siblings: [], 215 | }, 216 | ], 217 | }, 218 | ], 219 | }, 220 | ], 221 | } 222 | 223 | await buildMarkdownFile(changelog, { release: 'next', mode: 'incremental', output: './CHANGELOG.md' }) 224 | 225 | expect(result).toEqual(`# Changelog 226 | > Custom header 227 | 228 | 229 | ## next 230 | 231 | ### Changed 232 | 233 | - ♻️ Upgrade brand new feature [[c40ee86](https://github.com/frinyvonnick/gitmoji-changelog/commit/c40ee8669ba7ea5151adc2942fa8a7fc98d9e23c)] 234 | 235 | 236 | 237 | ## 1.0.0 238 | I am the last version 239 | `) 240 | }) 241 | }) 242 | 243 | describe('getHashUrl', () => { 244 | it('should return null if no hash given', () => { 245 | const result = getShortHash() 246 | expect(result).toBeNull() 247 | }) 248 | 249 | it('should return short hash if no gitInfo given', () => { 250 | const result = getShortHash('xxxxxxxxxxxxxxxxx') 251 | expect(result).toBe('xxxxxxx') 252 | }) 253 | 254 | it('should return short hash markdown with repo url', () => { 255 | const result = getShortHash('xxxxxxxxxxxxxxxxx', { 256 | url: 'https://github.com/frinyvonnick/gitmoji-changelog', 257 | }) 258 | expect(result).toBe('[xxxxxxx](https://github.com/frinyvonnick/gitmoji-changelog/commit/xxxxxxxxxxxxxxxxx)') 259 | }) 260 | }) 261 | 262 | describe('autolink', () => { 263 | it('should return null if no message given', () => { 264 | const result = autolink() 265 | expect(result).toBeNull() 266 | }) 267 | 268 | it('should return the message if no gitInfo given', () => { 269 | const result = autolink('message without gitinfo') 270 | expect(result).toBe('message without gitinfo') 271 | }) 272 | 273 | it('should return the message if nothing match', () => { 274 | const result = autolink('nothing match in this message') 275 | expect(result).toBe('nothing match in this message') 276 | }) 277 | 278 | it('should autolink with markdown one hashtag issue in message', () => { 279 | const result = autolink(':bug: fix issue #123', { 280 | bugsUrl: 'https://github.com/frinyvonnick/gitmoji-changelog/issues', 281 | }) 282 | expect(result).toBe(':bug: fix issue [#123](https://github.com/frinyvonnick/gitmoji-changelog/issues/123)') 283 | }) 284 | 285 | it('should autolink with markdown severals hashtag issues in message', () => { 286 | const result = autolink(':bug: fix issue #123 and #456', { 287 | bugsUrl: 'https://github.com/frinyvonnick/gitmoji-changelog/issues', 288 | }) 289 | expect(result).toBe(':bug: fix issue [#123](https://github.com/frinyvonnick/gitmoji-changelog/issues/123) and [#456](https://github.com/frinyvonnick/gitmoji-changelog/issues/456)') 290 | }) 291 | }) 292 | 293 | describe('matchVersionBreakpoint', () => { 294 | it('should return true if match with the given version breakpoint', () => { 295 | const result = matchVersionBreakpoint('', '1.0.0') 296 | expect(result).toBe(true) 297 | }) 298 | 299 | it('should return true if match with any version breakpoint', () => { 300 | const result = matchVersionBreakpoint('') 301 | expect(result).toBe(true) 302 | }) 303 | 304 | it('should return false if no match with a version breakpoint', () => { 305 | const result = matchVersionBreakpoint('hello world', '1.0.0') 306 | expect(result).toBe(false) 307 | }) 308 | }) 309 | -------------------------------------------------------------------------------- /packages/gitmoji-changelog-markdown/src/template.md: -------------------------------------------------------------------------------- 1 | {{#each changelog}} 2 | 3 | ## {{version}}{{#if date}} ({{date}}){{/if}} 4 | 5 | {{#each groups}} 6 | ### {{label}} 7 | 8 | {{#each commits}} 9 | - {{emoji}} {{message}} [{{hash}}]{{#if author}} (by {{author}}){{/if}} 10 | {{#each siblings}} 11 | * {{emoji}} {{message}} ({{hash}}) 12 | {{/each}} 13 | {{/each}} 14 | 15 | {{/each}} 16 | 17 | {{/each}} 18 | --------------------------------------------------------------------------------