├── .gitignore ├── release.sh ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.md └── bin └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | node_modules 16 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | set -u 5 | set -o pipefail 6 | 7 | if [ $# -ne 1 ] 8 | then 9 | echo "must specify a semver value (major, minor, patch)" 10 | exit 11 | fi 12 | 13 | if ([ "$1" != "major" ] && [ "$1" != "minor" ] && [ "$1" != "patch" ]) 14 | then 15 | echo "please specify one of (major, minor, patch)" 16 | exit 17 | fi 18 | 19 | git checkout master 20 | npm install 21 | npm version $1 22 | VERSION=`cat package.json | json version` 23 | node bin/index.js -o lalitkapoor -r github-changes --only-pulls -v -a --use-commit-body --reverse-changes -n v$VERSION 24 | git add CHANGELOG.md 25 | git commit --amend --no-edit 26 | git push origin master 27 | git push origin --tags 28 | npm publish 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Lalit Kapoor 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-changes", 3 | "version": "2.0.3", 4 | "description": "generate changelog for github repos", 5 | "main": "./bin/index.js", 6 | "bin": { 7 | "github-changes": "bin/index.js" 8 | }, 9 | "dependencies": { 10 | "@octokit/rest": "^18.0.12", 11 | "bluebird": "^3.5.4", 12 | "commander": "^6.2.1", 13 | "ghauth": "^5.0.0", 14 | "lodash": "^4.17.11", 15 | "moment-timezone": "^0.5.23", 16 | "semver": "^6.0.0" 17 | }, 18 | "devDependencies": { 19 | "json": "^10.0.0" 20 | }, 21 | "scripts": { 22 | "test": "echo \"Error: no test specified\" && exit 1" 23 | }, 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/lalitkapoor/github-changes.git" 27 | }, 28 | "keywords": [ 29 | "github", 30 | "changelog", 31 | "git", 32 | "changes", 33 | "history", 34 | "changed", 35 | "change" 36 | ], 37 | "author": "Lalit Kapoor ", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/lalitkapoor/github-changes/issues" 41 | }, 42 | "preferGlobal": true, 43 | "homepage": "https://github.com/lalitkapoor/github-changes" 44 | } 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Change Log 2 | 3 | ### v2.0.3 (2021/07/23 05:57 +00:00) 4 | - [#94](https://github.com/lalitkapoor/github-changes/pull/94) allow using without auth or token (#94) (@lalitkapoor) 5 | - [#100](https://github.com/lalitkapoor/github-changes/pull/100) Bump lodash from 4.17.20 to 4.17.21 (#100) (@dependabot[bot]) 6 | - [#99](https://github.com/lalitkapoor/github-changes/pull/99) Bump json from 9.0.6 to 10.0.0 (#99) (@dependabot[bot]) 7 | 8 | ### v2.0.2 (2021/01/06 06:33 +00:00) 9 | - [#93](https://github.com/lalitkapoor/github-changes/pull/93) validate tags given in --between-tags option (#93) (@TheJavaGuy) 10 | 11 | ### v2.0.1 (2020/12/30 19:27 +00:00) 12 | - [#92](https://github.com/lalitkapoor/github-changes/pull/92) upgrade octokit-rest (#92) (@lalitkapoor) 13 | 14 | ### v2.0.0 (2020/12/29 23:31 +00:00) 15 | - [#72](https://github.com/lalitkapoor/github-changes/pull/72) upgrade semver lib (#72) (@lalitkapoor) 16 | - [#82](https://github.com/lalitkapoor/github-changes/pull/82) Add trailing newline to CHANGELOG (#82) (@khiav223577) 17 | - [#91](https://github.com/lalitkapoor/github-changes/pull/91) Upgrade dependencies (#91) (@lalitkapoor) 18 | 19 | ### v1.1.1 (2017/11/08 05:06 +00:00) 20 | - [#71](https://github.com/lalitkapoor/github-changes/pull/71) Fix Markdown format (#71) (@baopham) 21 | - [#69](https://github.com/lalitkapoor/github-changes/pull/69) Paramaterise Github API timeout (#69) (@samdunne) 22 | 23 | ### v1.1.0 (2017/03/16 21:18 +00:00) 24 | - [#64](https://github.com/lalitkapoor/github-changes/pull/64) Support for squash and merge (@304NotModified, @lalitkapoor) 25 | 26 | ### v1.0.3 (2016/08/19 08:25 +00:00) 27 | - [#55](https://github.com/lalitkapoor/github-changes/pull/55) Update README with correct links! (@PunkChameleon) 28 | - [#59](https://github.com/lalitkapoor/github-changes/pull/59) added --time-zone option (@YuG1224) 29 | 30 | ### v1.0.2 (2016/02/22 00:53 +00:00) 31 | - [#53](https://github.com/lalitkapoor/github-changes/pull/53) added --for-tag option to generate changelog for single tag (@ivpusic) 32 | 33 | ### v1.0.1 (2016/01/12 01:52 +00:00) 34 | - [#52](https://github.com/lalitkapoor/github-changes/pull/52) Update ghauth dependency (@nunorafaelrocha) 35 | 36 | ### v1.0.0 (2015/04/12 14:32 +00:00) 37 | - [#47](https://github.com/lalitkapoor/github-changes/pull/47) Add a Gitter chat badge to README.md (@gitter-badger) 38 | 39 | ### v0.0.16 (2014/11/26 11:15 +00:00) 40 | - [#30](https://github.com/lalitkapoor/github-changes/pull/30) show changes between two tags (@lalitkapoor) 41 | 42 | ### v0.0.14 (2014/11/06 02:45 +00:00) 43 | - [#45](https://github.com/lalitkapoor/github-changes/pull/45) Add option to allow specifying the changelog title (@fixe) 44 | - [#46](https://github.com/lalitkapoor/github-changes/pull/46) Add option to allow specifying the date format (@fixe) 45 | - [#41](https://github.com/lalitkapoor/github-changes/pull/41) Aesthetic fixes (@nylen) 46 | 47 | ### v0.0.13 (2014/10/26 23:25 +00:00) 48 | - [#42](https://github.com/lalitkapoor/github-changes/pull/42) Fetch 100 tags per page (only 1 page for now) (@nylen) 49 | 50 | ### v0.0.12 (2014/09/02 05:37 +00:00) 51 | - [#35](https://github.com/lalitkapoor/github-changes/pull/35) Update README.md with Grunt Plugin Info (@PunkChameleon) 52 | - [#36](https://github.com/lalitkapoor/github-changes/pull/36) PR links point to https://null/... (@lalitkapoor) 53 | 54 | ### v0.0.11 (2014/05/12 19:01 +00:00) 55 | - [#32](https://github.com/lalitkapoor/github-changes/pull/32) support for github enterprise (@lalitkapoor) 56 | 57 | ### v0.0.10 (2014/03/01 06:02 +00:00) 58 | - [#26](https://github.com/lalitkapoor/github-changes/pull/26) handle missing author in pull requests (@lalitkapoor) 59 | 60 | ### v0.0.9 (2014/03/01 04:09 +00:00) 61 | - [#22](https://github.com/lalitkapoor/github-changes/pull/22) show commit message if commit body is empty for --use-commit-body (@lalitkapoor) 62 | - [#23](https://github.com/lalitkapoor/github-changes/pull/23) though tagged correctly commit ordering may vary (@lalitkapoor) 63 | - [#24](https://github.com/lalitkapoor/github-changes/pull/24) sorting commits by date doesn't work for --data pulls (@lalitkapoor) 64 | - [#21](https://github.com/lalitkapoor/github-changes/pull/21) attribution is not always correct for pull requests (@lalitkapoor) 65 | 66 | ### v0.0.8 (2014/02/25 08:05 +00:00) 67 | - [#18](https://github.com/lalitkapoor/github-changes/pull/18) Add a Bitdeli Badge to README (@bitdeli-chef) 68 | - [#15](https://github.com/lalitkapoor/github-changes/pull/15) add option to order versions based on semver instead of the tag date (@lalitkapoor) 69 | 70 | ### v0.0.7 (2014/02/25 06:40 +00:00) 71 | - [#17](https://github.com/lalitkapoor/github-changes/pull/17) use-commit-body only works when only-pulls or only-merges is used (@lalitkapoor) 72 | 73 | ### v0.0.6 (2014/02/25 06:21 +00:00) 74 | - [#13](https://github.com/lalitkapoor/github-changes/pull/13) don't strip down pull request object (@lalitkapoor) 75 | - [#14](https://github.com/lalitkapoor/github-changes/pull/14) flags to improve what merge items to into the changelog (@lalitkapoor) 76 | - [#16](https://github.com/lalitkapoor/github-changes/pull/16) when parsing pull request pr links not generated and attribution incorrect (@lalitkapoor) 77 | 78 | ### v0.0.5 (2014/02/23 10:14 +00:00) 79 | - [#9](https://github.com/lalitkapoor/github-changes/pull/9) don't double space in the default formatter (@lalitkapoor) 80 | 81 | ### v0.0.4 (2014/02/23 08:43 +00:00) 82 | - [#7](https://github.com/lalitkapoor/github-changes/pull/7) use commit date not author date when determining which commits belong to which tags (@lalitkapoor) 83 | 84 | ### v0.0.3 (2014/02/23 08:24 +00:00) 85 | - [#2](https://github.com/lalitkapoor/github-changes/pull/2) Fixed typos. (@Fraser999) 86 | - [#4](https://github.com/lalitkapoor/github-changes/pull/4) consider generating changelog based on commit messages (@lalitkapoor) 87 | 88 | ### v0.0.2 (2014/02/20 06:23 +00:00) 89 | - [#1](https://github.com/lalitkapoor/github-changes/pull/1) error thrown when link header doesn't contain last (@lalitkapoor) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | github-changes [![NPM version](https://badge.fury.io/js/github-changes.png)](http://badge.fury.io/js/github-changes) 2 | ============== 3 | 4 | Generate a changelog based on merged pull requests or commit messages 5 | 6 | [![NPM](https://nodei.co/npm/github-changes.png)](https://nodei.co/npm/github-changes/) 7 | 8 | [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/lalitkapoor/github-changes?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | 10 | ### Installation 11 | 12 | ``` 13 | npm install -g github-changes 14 | ``` 15 | 16 | ``` 17 | Usage: github-changes [options] 18 | 19 | Options: 20 | -o, --owner (required) owner of the Github repository 21 | -r, --repository (required) name of the Github repository 22 | -d, --data (DEPRECATED) use pull requests or commits (choices: pulls, commits) [commits] 23 | -b, --branch name of the default branch [master] 24 | -n, --tag-name tag name for upcoming release [upcoming] 25 | -a, --auth prompt to auth with Github - use this for private repos and higher rate limits 26 | -k, --token need to use this or --auth for private repos and higher rate limits 27 | -f, --file name of the file to output the changelog to [CHANGELOG.md] 28 | -t, --title title to appear in the top of the changelog [Change Log] 29 | -z, --time-zone time zone [UTC] 30 | -m, --date-format date format [(YYYY/MM/DD HH:mm Z)] 31 | -v, --verbose output details 32 | --host alternate host name to use with github enterprise [api.github.com] 33 | --path-prefix path-prefix for use with github enterprise 34 | --between-tags only diff between these two tags, separate by 3 dots ... 35 | --for-tag only get changes for this tag 36 | --issue-body (DEPRECATED) include the body of the issue (--data MUST equal 'pulls') 37 | --no-merges do not include merges 38 | --only-merges only include merges 39 | --only-pulls only include pull requests 40 | --use-commit-body use the commit body of a merge instead of the message - "Merge branch..." 41 | --order-semver use semantic versioning for the ordering instead of the tag date 42 | --reverse-changes reverse the order of changes within a release (show oldest first) 43 | --hide-tag-names hide tag names in changelog 44 | ``` 45 | 46 | ### Example usage 47 | 48 | #### Generate changelog via pull requests 49 | ```bash 50 | github-changes -o lalitkapoor -r github-changes -a --only-pulls --use-commit-body 51 | ``` 52 | 53 | #### Output 54 | ## Change Log 55 | 56 | ### v1.0.3 (2016/08/19 08:25 +00:00) 57 | - [#59](https://github.com/lalitkapoor/github-changes/pull/59) added --time-zone option (@YuG1224) 58 | - [#55](https://github.com/lalitkapoor/github-changes/pull/55) Update README with correct links! (@PunkChameleon) 59 | 60 | ### v1.0.2 (2016/02/22 00:53 +00:00) 61 | - [#53](https://github.com/lalitkapoor/github-changes/pull/53) added --for-tag option to generate changelog for single tag (@ivpusic) 62 | 63 | ### v1.0.1 (2016/01/12 01:52 +00:00) 64 | - [#52](https://github.com/lalitkapoor/github-changes/pull/52) Update ghauth dependency (@nunorafaelrocha) 65 | 66 | ### v1.0.0 (2015/04/12 14:32 +00:00) 67 | - [#47](https://github.com/lalitkapoor/github-changes/pull/47) Add a Gitter chat badge to README.md (@gitter-badger) 68 | 69 | ### v0.0.16 (2014/11/26 11:15 +00:00) 70 | - [#30](https://github.com/lalitkapoor/github-changes/pull/30) show changes between two tags (@lalitkapoor) 71 | 72 | ### v0.0.14 (2014/11/06 02:45 +00:00) 73 | - [#41](https://github.com/lalitkapoor/github-changes/pull/41) Aesthetic fixes (@nylen) 74 | - [#46](https://github.com/lalitkapoor/github-changes/pull/46) Add option to allow specifying the date format (@fixe) 75 | - [#45](https://github.com/lalitkapoor/github-changes/pull/45) Add option to allow specifying the changelog title (@fixe) 76 | 77 | ### v0.0.13 (2014/10/26 23:25 +00:00) 78 | - [#42](https://github.com/lalitkapoor/github-changes/pull/42) Fetch 100 tags per page (only 1 page for now) (@nylen) 79 | 80 | ### v0.0.12 (2014/09/02 05:37 +00:00) 81 | - [#36](https://github.com/lalitkapoor/github-changes/pull/36) PR links point to https://null/... (@lalitkapoor) 82 | - [#35](https://github.com/lalitkapoor/github-changes/pull/35) Update README.md with Grunt Plugin Info (@PunkChameleon) 83 | 84 | ... 85 | 86 | 87 | 88 | #### Generate changelog via commit messages 89 | ```bash 90 | github-changes -o npm -r npm -a 91 | ``` 92 | 93 | #### Output 94 | 95 | ## Change Log 96 | 97 | ### upcoming (2014/02/23 10:02 +00:00) 98 | - [70fd532](https://github.com/npm/npm/commit/70fd532c91335e76bda9366234b53a0498b9901a) fix prune.js test with empty cache (@robertkowalski) 99 | - [6fd6ff7](https://github.com/npm/npm/commit/6fd6ff7e536ea6acd33037b1878d4eca1f931985) Sort dependencies when --save'ing. (@domenic) 100 | - [2ddd060](https://github.com/npm/npm/commit/2ddd06037e9bd58cd95a380a9381ff90bea47f0d) add test, some boyscouting (@robertkowalski) 101 | - [17f07df](https://github.com/npm/npm/commit/17f07df8ad8e594304c2445bf7489cb53346f2c5) Add --save-exact config for --save[-dev|-optional]. (@timoxley) 102 | - [4b51920](https://github.com/npm/npm/commit/4b5192071654e2b312a7678b7586e435be62f473) Prevent creation of node_modules/npm-4503-c (@timoxley) 103 | - [30b6783](https://github.com/npm/npm/commit/30b67836b51b68614c9e87dc476c0961d53ec6d4) doc: update misc/semver.md (@isaacs) 104 | 105 | ### v1.4.4 (2014/02/20 16:04 +00:00) 106 | - [05d2490](https://github.com/npm/npm/commit/05d2490526fa40adc55727e92d4d30bd63aabaad) uid-number@0.0.4 (@isaacs) 107 | - [3850441](https://github.com/npm/npm/commit/3850441fd8c2fd71ebfd8e9986bc5f2e482ab6db) Document the --tag option of npm-publish (@kriskowal) 108 | - [14e650b](https://github.com/npm/npm/commit/14e650bce0bfebba10094c961ac104a61417a5de) alias 't' to 'test' (@isaacs) 109 | - [d50b826](https://github.com/npm/npm/commit/d50b826b9e5884c0f4e1101b90c7206a138a43e7) uid-number@0.0.5 (@isaacs) 110 | - [cd7e4a2](https://github.com/npm/npm/commit/cd7e4a23037f3ae1928bac02332784ffab557be9) v1.4.4 (@isaacs) 111 | 112 | ### v1.4.3 (2014/02/17 04:37 +00:00) 113 | - [3ce6905](https://github.com/npm/npm/commit/3ce6905bf6b0963956d7dbb8a89fc29d379de91c) view: remove arbitrary cache limit (@isaacs) 114 | - [bb6fb4d](https://github.com/npm/npm/commit/bb6fb4d158f175ddeb2956b361f854c273b6bed0) read-installed@1.0.0 (@isaacs) 115 | - [caa7065](https://github.com/npm/npm/commit/caa7065b06ffb55ea3410e5a14ddc80c26844b13) new tests for read-installed (@isaacs) 116 | - [401a642](https://github.com/npm/npm/commit/401a64286aa6665a94d1d2f13604f7014c5fce87) link: do not allow linking unnamed packages (@isaacs) 117 | - [09223de](https://github.com/npm/npm/commit/09223de8778b3e8fb0ecfec82cf6058d2c659518) Forbid deleting important npm dirs (@isaacs) 118 | - [86028e9](https://github.com/npm/npm/commit/86028e9fd8524d5e520ce01ba2ebab5a030103fc) dedupe: respect dependency versions (@rafeca) 119 | - [02d4322](https://github.com/npm/npm/commit/02d4322cd4f67a078a29019d2c4ef591b281132c) Follow redirects on curl|sh installer script (@isaacs) 120 | - [8a26f6f](https://github.com/npm/npm/commit/8a26f6ff7e9769985f74b60eed54e488a4d4a804) Test for repo command (@isaacs) 121 | - [acc4d02](https://github.com/npm/npm/commit/acc4d023c57d07704b20a0955e4bf10ee91bdc83) prune: Added back --production support (@davglass) 122 | - [0a3151c](https://github.com/npm/npm/commit/0a3151c9cbeb50c1c65895685c2eabdc7e2608dc) default to ^ instead of ~ (@mikolalysenko) 123 | - [9ae71de](https://github.com/npm/npm/commit/9ae71de7802132c349c60f1b740a734761fec4a1) npm-registry-client@0.4.4 (@isaacs) 124 | - [46d8768](https://github.com/npm/npm/commit/46d876821d1dd94c050d5ebc86444bed12c56739) "install ./pkg@1.2.3" should install local module (@rlidwka) 125 | - [f469847](https://github.com/npm/npm/commit/f46984787e8bb219cfd1d8394932dca2ed6b3b2c) test: express is not in mocks, use underscore instead (@isaacs) 126 | 127 | ... 128 | 129 | ### Using with Grunt 130 | 131 | If you want to generate a changelog within a grunt workflow, [a grunt plugin] (https://github.com/PunkChameleon/grunt-github-changes) that can be utilized. To install: 132 | 133 | ``` 134 | npm install grunt-github-changes --save-dev 135 | ``` 136 | 137 | For further details and specifics on how to use (and to contribute), see [grunt-github-changes](https://github.com/PunkChameleon/grunt-github-changes). 138 | 139 | ### FAQ 140 | 141 | #### How are squashed pull request matched? 142 | 143 | When a pull request is merged with "Squash and merge", there isn't a merge commit. 144 | By checking the commit message for ` (#123)` etc, we can match the correct pull request. 145 | -------------------------------------------------------------------------------- /bin/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // Overwrite global promise, so GithubApi will use bluebird too. 4 | Promise = require("bluebird"); 5 | 6 | const fs = require('fs'); 7 | const _ = require('lodash'); 8 | const http = require('http'); 9 | const https = require('https'); 10 | const domain = require('domain'); 11 | const moment = require('moment-timezone'); 12 | const parser = require('commander'); 13 | const semver = require('semver'); 14 | const { Octokit } = require("@octokit/rest") 15 | const ghauth = Promise.promisify(require('ghauth')); 16 | 17 | // Increase number of concurrent requests 18 | http.globalAgent.maxSockets = 30; 19 | https.globalAgent.maxSockets = 30; 20 | 21 | // It might be faster to just go through commits on the branch 22 | // instead of iterating over closed issues, look into this later. 23 | // 24 | // Even better yet. I might just be able to do this with git log. 25 | // tags: git log --tags --simplify-by-decoration --format="%ci%n%d" 26 | // prs: git log --grep="Merge pull request #" --format="%s%n%ci%n%b" 27 | 28 | // parse cli options 29 | var opts = parser 30 | .version(require('../package.json').version) 31 | .requiredOption('-o, --owner ', '(required) owner of the Github repository') 32 | .requiredOption('-r, --repository ', '(required) name of the Github repository') 33 | .option('-d, --data [type]', '(DEPRECATED) use pull requests or commits (choices: pulls, commits)', 'commits') 34 | .option('-b, --branch [name]', 'name of the default branch', 'master') 35 | .option('-n, --tag-name [name]', 'tag name for upcoming release', 'upcoming') 36 | .option('-a, --auth', 'prompt to auth with Github - use this for private repos and higher rate limits') 37 | .option('-k, --token [token]', 'need to use this or --auth for private repos and higher rate limits') 38 | .option('-f, --file [name]', 'name of the file to output the changelog to', 'CHANGELOG.md') 39 | .option('-t, --title [title]', 'title to appear in the top of the changelog', 'Change Log') 40 | .option('-z, --time-zone [zone]', 'time zone', 'UTC') 41 | .option('-m, --date-format [format]', 'date format', '(YYYY/MM/DD HH:mm Z)') 42 | .option('-v, --verbose', 'output details') 43 | .option('--host [domain]', 'alternate host name to use with github enterprise', 'api.github.com') 44 | .option('--path-prefix [path]', 'path-prefix for use with github enterprise') 45 | .option('--between-tags [range]', 'only diff between these two tags, separate by 3 dots ...') 46 | .option('--issue-body', '(DEPRECATED) include the body of the issue (--data MUST equal \'pulls\')') 47 | .option('--for-tag [tag]', 'only get changes for this tag') 48 | .option('--no-merges', 'do not include merges') 49 | .option('--only-merges', 'only include merges') 50 | .option('--only-pulls', 'only include pull requests') 51 | .option('--use-commit-body', 'use the commit body of a merge instead of the message - "Merge branch..."') 52 | .option('--order-semver', 'use semantic versioning for the ordering instead of the tag date') 53 | .option('--reverse-changes', 'reverse the order of changes within a release (show oldest first)') 54 | .option('--hide-tag-names', 'hide tag names in changelog') 55 | .option('--timeout [milliseconds]', 'Github API timeout', 10000) 56 | .parse(process.argv); 57 | 58 | if (opts.onlyPulls) opts.merges = true; 59 | 60 | var betweenTags = [null, null]; 61 | var betweenTagsNames = null; 62 | 63 | if (opts.betweenTags) { 64 | if (!opts.betweenTags.length) { 65 | return console.error(`Invalid value for --between-tags. Please specify two tags separated by 3 dots ...`); 66 | } 67 | 68 | betweenTagsNames = opts.betweenTags.split('...'); 69 | if (!betweenTagsNames[0] || !betweenTagsNames[1]) { 70 | return console.error(`Invalid value for --between-tags. Please specify two tags separated by 3 dots ...`); 71 | } 72 | } 73 | 74 | var forTag = opts.forTag; 75 | 76 | var commitsBySha = {}; // populated when calling getAllCommits 77 | var currentDate = moment(); 78 | 79 | var github = null; 80 | 81 | // github auth token 82 | var token = null; 83 | 84 | // ~/.config/changelog.json will store the token 85 | var authOptions = { 86 | clientId : '899aa18ee35dbb76c97c' 87 | , configName : 'changelog' 88 | , scopes : ['user', 'public_repo', 'repo'] 89 | }; 90 | 91 | // TODO: Could probably fetch releases so we don't have to get the commit data 92 | // for the sha of each tag to figure out the date. Could save alot on api 93 | // calls. 94 | var getTags = function(){ 95 | var tagOpts = { 96 | owner: opts.owner 97 | , repo: opts.repository 98 | , per_page: 100 99 | }; 100 | 101 | return github.repos.listTags(tagOpts) 102 | .then(result => result.data) 103 | .then(tagArray => { 104 | // check that the tags asked for exist (--between-tags) 105 | if (betweenTagsNames) { 106 | const tagNames = tagArray.map(e => e.name); 107 | if (!tagNames.includes(betweenTagsNames[0])) { 108 | console.error(`Tag ${betweenTagsNames[0]} was given as a first value of --between-tags but it doesn't exist in repository`); 109 | process.exit(1); 110 | } 111 | if (!tagNames.includes(betweenTagsNames[1])) { 112 | console.error(`Tag ${betweenTagsNames[1]} was given as a second value of --between-tags but it doesn't exist in repository`); 113 | process.exit(1); 114 | } 115 | } 116 | 117 | return tagArray; 118 | }) 119 | .map(function(ref){ 120 | return github.repos.getCommit({ 121 | owner: tagOpts.owner 122 | , repo: tagOpts.repo 123 | , ref: ref.commit.sha 124 | }).then(function({data: commit}){ 125 | opts.verbose && console.log('pulled commit data for tag - ', ref.name); 126 | var tag = { 127 | name: ref.name 128 | , date: moment(commit.commit.committer.date) 129 | }; 130 | 131 | // if --between-tags is specified then reference the appropriate tag 132 | if (betweenTagsNames && (betweenTagsNames.indexOf(tag.name)>-1)) { 133 | betweenTags[betweenTagsNames.indexOf(tag.name)] = tag; 134 | } 135 | 136 | return tag; 137 | }); 138 | }); 139 | }; 140 | 141 | var _getAllPullRequests = function(page = 1) { 142 | return github.pulls.list({ 143 | owner: opts.owner 144 | , repo: opts.repository 145 | , base: opts.branch 146 | , state: 'closed' 147 | , sort: 'updated' 148 | , direction: 'desc' 149 | , per_page: 100 150 | , page: page 151 | // , since: null // TODO: this is an improvement to save API calls 152 | }) 153 | .then(result => { 154 | opts.verbose && console.log('fetched %d pull requests', ((page - 1) * 100) + result.data.length) 155 | 156 | var pulls = result.data.filter(pr => pr.merged_at !== null); 157 | 158 | if (result.headers.link && result.headers.link.indexOf('rel="next"') > 0) { 159 | return _getAllPullRequests(page + 1).then(list => pulls.concat(list)); 160 | } 161 | 162 | return pulls; 163 | }) 164 | ; 165 | }; 166 | 167 | var getPullRequests = function() { 168 | opts.verbose && console.log('fetching pull requests'); 169 | 170 | return _getAllPullRequests().then(pulls => { 171 | opts.verbose && console.log('fetched all pull requests'); 172 | return pulls; 173 | }); 174 | }; 175 | 176 | var _getAllCommits = function(page = 1) { 177 | return github.repos.listCommits({ 178 | owner: opts.owner 179 | , repo: opts.repository 180 | , sha: opts.branch 181 | , per_page: 100 182 | , page: page 183 | }) 184 | .then(result => { 185 | opts.verbose && console.log('fetched %d commits', ((page - 1) * 100) + result.data.length) 186 | 187 | var commits = result.data.slice(); 188 | 189 | result.data.forEach(commit => { 190 | commitsBySha[commit.sha] = commit; 191 | }); 192 | 193 | if (result.headers.link && result.headers.link.indexOf('rel="next"') > 0) { 194 | return _getAllCommits(page + 1).then(list => commits.concat(list)); 195 | } 196 | return commits; 197 | }); 198 | }; 199 | 200 | var getAllCommits = function() { 201 | opts.verbose && console.log('fetching commits'); 202 | 203 | return _getAllCommits().then(commits => { 204 | opts.verbose && console.log('fetched all commits'); 205 | return commits; 206 | }); 207 | }; 208 | 209 | var getData = function() { 210 | if (opts.data === 'commits') return getAllCommits(); 211 | return getPullRequests(); 212 | }; 213 | 214 | var tagger = function(sortedTags, data) { 215 | var date = null; 216 | if (opts.data === 'commits') date = moment(data.commit.committer.date); 217 | else date = moment(data.merged_at); 218 | 219 | var current = null; 220 | for (var i=0, len=sortedTags.length; i < len; i++) { 221 | var tag = sortedTags[i]; 222 | if (tag.date < date) break; 223 | current = tag; 224 | } 225 | if (!current) current = {name: opts.tagName, date: currentDate}; 226 | return current; 227 | }; 228 | 229 | var prFormatter = function(data) { 230 | var currentTagName = ''; 231 | var output = "## " + opts.title + "\n"; 232 | data.forEach(function(pr){ 233 | if (!opts.hideTagNames) { 234 | if (pr.tag === null) { 235 | currentTagName = opts.TagName; 236 | output+= "\n### " + opts.tagName; 237 | output+= "\n"; 238 | } else if (pr.tag.name != currentTagName) { 239 | currentTagName = pr.tag.name; 240 | output+= "\n### " + pr.tag.name 241 | output+= " " + pr.tag.date.tz(opts.timeZone).format(opts.dateFormat); 242 | output+= "\n"; 243 | } 244 | } 245 | 246 | output += "- [#" + pr.number + "](" + pr.html_url + ") " + pr.title 247 | if (pr.user && pr.user.login) output += " (@" + pr.user.login + ")"; 248 | if (opts.issueBody && pr.body && pr.body.trim()) output += "\n\n >" + pr.body.trim().replace(/\n/ig, "\n > ") +"\n"; 249 | 250 | // output += " " + moment(pr.merged_at).utc().format(opts.dateFormat); 251 | output += "\n"; 252 | }); 253 | return output.trim() + "\n"; 254 | }; 255 | 256 | var getCommitsInMerge = function(mergeCommit) { 257 | // direct descendents of the mergeCommit 258 | var directDescendents = {}; 259 | 260 | // store reachable commits 261 | var store1 = {}; 262 | var store2 = {}; 263 | 264 | var currentCommit = mergeCommit; 265 | while (currentCommit && currentCommit.parents && currentCommit.parents.length > 0) { 266 | directDescendents[currentCommit.parents[0].sha] = true; 267 | currentCommit = commitsBySha[currentCommit.parents[0].sha]; 268 | } 269 | 270 | var getAllReachableCommits = function(sha, store) { 271 | if (!commitsBySha[sha]) return; 272 | store[sha]=true; 273 | commitsBySha[sha].parents.forEach(function(parent){ 274 | if (directDescendents[parent.sha]) return; 275 | if (store[parent.sha]) return; // don't revist commits we've explored 276 | return getAllReachableCommits(parent.sha, store); 277 | }) 278 | }; 279 | 280 | var parentShas = _.map(mergeCommit.parents, 'sha'); 281 | var notSha = parentShas.shift(); // value to pass to --not flag in git log 282 | parentShas.forEach(function(sha){ 283 | return getAllReachableCommits(sha, store1); 284 | }); 285 | getAllReachableCommits(notSha, store2); 286 | 287 | return _.difference( 288 | Object.keys(store1) 289 | , Object.keys(store2) 290 | ).map(function(sha){ 291 | return commitsBySha[sha]; 292 | }); 293 | }; 294 | 295 | var commitFormatter = function(data) { 296 | var currentTagName = ''; 297 | var output = "## " + opts.title + "\n"; 298 | data.forEach(function(commit){ 299 | if (betweenTagsNames && commit.tag.date<=betweenTags[0].date) return; 300 | if (betweenTagsNames && betweenTags[1] && commit.tag.date>betweenTags[1].date) return; 301 | if (forTag && commit.tag.name !== forTag) return; 302 | 303 | var isMerge = (commit.parents.length > 1); 304 | var isPull = isMerge && /^Merge pull request #/i.test(commit.commit.message); 305 | var isSquashAndMerge = false; 306 | 307 | // handle checking for a squash & merge 308 | if (!isPull) { 309 | isPull = /\s\(\#\d+\)/i.test(commit.commit.message); //contains ' (#123)'? 310 | if (isPull) { 311 | isMerge = true; 312 | isSquashAndMerge = true; 313 | } 314 | } 315 | 316 | // exits 317 | if ((opts.merges === false) && isMerge) return ''; 318 | if ((opts.onlyMerges) && commit.parents.length < 2) return ''; 319 | if ((opts.onlyPulls) && !isPull) return ''; 320 | 321 | // choose message content 322 | var messages = commit.commit.message.split('\n'); 323 | var message = messages.shift().trim(); 324 | 325 | if (!isSquashAndMerge && opts.useCommitBody && commit.parents.length > 1) { 326 | message = messages.join(' ').trim() || message; 327 | } 328 | 329 | if (!opts.hideTagNames) { 330 | if (commit.tag === null) { 331 | currentTagName = opts.tagName; 332 | output+= "\n### " + opts.tagName; 333 | output+= "\n"; 334 | } else if (commit.tag.name != currentTagName) { 335 | currentTagName = commit.tag.name; 336 | output+= "\n### " + commit.tag.name 337 | output+= " " + commit.tag.date.tz(opts.timeZone).format(opts.dateFormat); 338 | output+= "\n"; 339 | } 340 | } 341 | 342 | // if commit is a merge then find all commits that belong to the merge 343 | // and extract authors out of those. Do this for --only-merges and for 344 | // --only-pulls 345 | var authors = {}; 346 | if (isMerge && (opts.onlyMerges || opts.onlyPulls)) { 347 | getCommitsInMerge(commit).forEach(function(c){ 348 | // ignore the author of a merge commit, they might have reviewed, 349 | // resolved conflicts, and merged, but I don't think this alone 350 | // should result in them being considered one of the authors in 351 | // the pull request 352 | if (c.parents.length > 1) return; 353 | 354 | if (c.author && c.author.login) { 355 | authors[c.author.login] = true; 356 | } 357 | }); 358 | } 359 | authors = Object.keys(authors); 360 | 361 | // if it's a pull request, then the link should be to the pull request 362 | if (isPull) { 363 | var prNumber = null; 364 | var author = null; 365 | var authorName = commit.commit.author && commit.commit.author.name; 366 | 367 | if (isSquashAndMerge) { 368 | prNumber = commit.commit.message.match(/\(#\d+\)/)[0].replace(/\(|\)|#/g,''); 369 | author = (commit.author && commit.author.login); 370 | } else { 371 | prNumber = commit.commit.message.split('#')[1].split(' ')[0]; 372 | author = (commit.commit.message.split(/\#\d+\sfrom\s/)[1]||'').split('/')[0]; 373 | } 374 | 375 | 376 | var host = (opts.host === 'api.github.com') ? 'github.com' : opts.host; 377 | var url = "https://"+host+"/"+opts.owner+"/"+opts.repository+"/pull/"+prNumber; 378 | output += "- [#" + prNumber + "](" + url + ") " + message; 379 | 380 | if (authors.length) { 381 | output += ' (' + authors.map(function(author){return '@' + author}).join(', ') + ')'; 382 | } else if (author) { 383 | output += " (@" + author + ")"; 384 | } else if (authorName) { 385 | output += " (" + authorName + ")"; 386 | } 387 | 388 | } else { //otherwise link to the commit 389 | output += "- [" + commit.sha.substr(0, 7) + "](" + commit.html_url + ") " + message; 390 | 391 | if (authors.length) 392 | output += ' (' + authors.map(function(author){return '@' + author}).join(', ') + ')'; 393 | else if (commit.author && commit.author.login) 394 | output += " (@" + commit.author.login + ")"; 395 | } 396 | 397 | // output += " " + moment(commit.commit.committer.date).utc().format(opts.dateFormat); 398 | output += "\n"; 399 | }); 400 | return output.trim(); 401 | }; 402 | 403 | var formatter = function(data) { 404 | if (opts.data === 'commits') return commitFormatter(data); 405 | return prFormatter(data); 406 | }; 407 | 408 | var getGithubToken = function() { 409 | if (opts.token) return Promise.resolve({token: opts.token}); 410 | if (opts.auth) return ghauth(authOptions); 411 | return Promise.resolve({}); 412 | }; 413 | 414 | var task = function() { 415 | getGithubToken() 416 | .then(function(authData){ 417 | if (authData.token) token = authData.token; 418 | 419 | github = new Octokit({ 420 | version: '3.0.0' 421 | , protocol: 'https' 422 | , pathPrefix: opts.pathPrefix 423 | , host: opts.host 424 | , request: { 425 | timeout: opts.timeout 426 | } 427 | , auth: token 428 | }); 429 | }) 430 | .then(function(){ 431 | return Promise.all([getTags(), getData()]) 432 | }) 433 | .spread(function(tags, data){ 434 | allTags = _.sortBy(tags, 'date').reverse(); 435 | return data; 436 | }) 437 | .map(function(data){ 438 | data.tag = tagger(allTags, data); 439 | data.tagDate = data.tag.date; 440 | return data; 441 | }) 442 | .then(function(data){ 443 | // order by commit date DESC by default / ASC if --reverse-changes given 444 | var compareSign = (opts.reverseChanges) ? -1 : 1; 445 | 446 | // order by tag date then commit date 447 | if (!opts.orderSemver && opts.data === 'commits') { 448 | data = data.sort(function(a,b){ 449 | var tagCompare = (a.tagDate - b.tagDate); 450 | return (tagCompare) ? tagCompare : compareSign * (moment(a.commit.committer.date) - moment(b.commit.committer.date)); 451 | }).reverse(); 452 | return data; 453 | } else if (!opts.orderSemver && opts.data === 'pulls') { 454 | data = data.sort(function(a,b){ 455 | var tagCompare = (a.tagDate - b.tagDate); 456 | return (tagCompare) ? tagCompare : compareSign * (moment(a.merged_at) - moment(b.merged_at)); 457 | }).reverse(); 458 | return data; 459 | } 460 | 461 | // order by semver then commit date 462 | data = data.sort(function(a,b){ 463 | var tagCompare = 0; 464 | if (a.tag.name === b.tag.name) tagCompare = 0; 465 | else if (a.tag.name === opts.tagName) tagCompare = 1; 466 | else if (b.tag.name === opts.tagName) tagCompare -1; 467 | else tagCompare = semver.compare(a.tag.name, b.tag.name); 468 | return (tagCompare) ? tagCompare : compareSign * (moment(a.commit.committer.date) - moment(b.commit.committer.date)); 469 | }).reverse(); 470 | return data; 471 | }) 472 | .then(function(data){ 473 | fs.writeFileSync(opts.file, formatter(data)); 474 | }) 475 | .then(function(){ 476 | process.exit(0); 477 | }) 478 | .catch(function(error){ 479 | console.error('error', error); 480 | console.error('stack', error.stack); 481 | process.exit(1); 482 | }) 483 | ; 484 | }; 485 | 486 | var done = function (error) { 487 | if (!error) process.exit(0); 488 | console.log(error); 489 | console.log(error.stack); 490 | process.exit(1); 491 | }; 492 | 493 | var runner = function () { 494 | var d = domain.create(); 495 | d.on('error', done); 496 | d.run(task); 497 | }; 498 | 499 | runner(); 500 | --------------------------------------------------------------------------------