├── .github └── workflows │ └── Playground.yml ├── .gitignore ├── .vscode ├── launch.json └── tasks.json ├── README.md ├── lerna.json ├── my_generator.js ├── package.json ├── packages ├── chglog_cli │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── tsconfig.json ├── chglog_fetcher │ ├── fetch.ts │ ├── index.ts │ ├── package.json │ ├── tsconfig.json │ └── visitor.ts └── chglog_grouping_generator │ ├── index.ts │ ├── package.json │ └── tsconfig.json ├── test ├── package.json ├── sample.ts └── tsconfig.json ├── tsconfig.json └── yarn.lock /.github/workflows/Playground.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Playground 4 | 5 | 6 | # Controls when the action will run. 7 | on: 8 | workflow_dispatch: 9 | inputs: 10 | repository: 11 | description: "Repository /" 12 | required: true 13 | left: 14 | descripton: "Base" 15 | required: true 16 | right: 17 | description: "Head" 18 | required: true 19 | 20 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 21 | jobs: 22 | # This workflow contains a single job called "build" 23 | build: 24 | # The type of runner that the job will run on 25 | runs-on: ubuntu-latest 26 | 27 | # Steps represent a sequence of tasks that will be executed as part of the job 28 | steps: 29 | - uses: actions/setup-node@v2 30 | with: 31 | node-version: '12' 32 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 33 | - uses: actions/checkout@v2 34 | with: 35 | repository: ${{ github.event.inputs.repository }} 36 | path: "./_target" 37 | - uses: actions/checkout@v2 38 | with: 39 | fetch-depth: 1 40 | - name: Bootstrap 41 | run: npx lerna bootstrap 42 | - name: Build 43 | run: npx lerna exec yarn run build 44 | - name: Link 45 | run: cd ./packages/chglog_cli && npm link 46 | - name: Generate 47 | run: cd ./_target && chglog changelog -l -r --github_token $PUBLIC_REPO_ONLY_TOKEN 48 | env: 49 | PUBLIC_REPO_ONLY_TOKEN: ${{ secrets.PUBLIC_REPO_ONLY_TOKEN }} 50 | LEFT: ${{ github.event.inputs.left }} 51 | RIGHT: ${{ github.event.inputs.right }} 52 | 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node 3 | # Edit at https://www.gitignore.io/?templates=node 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # rollup.js default build output 84 | dist/ 85 | 86 | # Uncomment the public line if your project uses Gatsby 87 | # https://nextjs.org/blog/next-9-1#public-directory-support 88 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 89 | # public 90 | 91 | # Storybook build outputs 92 | .out 93 | .storybook-out 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # Temporary folders 108 | tmp/ 109 | temp/ 110 | 111 | # End of https://www.gitignore.io/api/node 112 | # 113 | .DS_Store 114 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [{ 7 | "type": "node", 8 | "request": "launch", 9 | "name": "Run CLI", 10 | "skipFiles": ["/**"], 11 | "program": "${workspaceFolder}/dist/test/sample.js", 12 | "preLaunchTask": "build_all", 13 | "outFiles": ["${workspaceFolder}/dist/**/*.js"] 14 | }] 15 | } 16 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "shell", 8 | "label": "build_all", 9 | "command": "lerna", 10 | "args": ["exec", "yarn", "run", "build"], 11 | "group": "build", 12 | "problemMatcher": [], 13 | "options": { 14 | "cwd": "${workspaceFolder}" 15 | } 16 | }] 17 | } 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A changelog generator that regarding pulls and specified commits in THE SPECIFIC RANGE. 2 | 3 | ## First look you can create 4 | 5 | 6 | 7 | ## Motivation 8 | 9 | We can find a lot of tools that generate summarized changelogs from git and GitHub. 10 | However I could not find something more simple tool which generates from any range. 11 | 12 | So this library separated several modules as followings: 13 | * Fetching module - It fetches the all of pull-reqests in the range as possible. 14 | * Generator module - It generates text from the gathered pull-requests from fetching module with visitor pattern. 15 | * CLI module - It calls fetching module and passing data to generator module and then output in console. 16 | 17 | ## Installation 18 | 19 | ``` 20 | npm install -g @muukii/chglog 21 | ``` 22 | 23 | ## Usage 24 | 25 |
Click to see an example output 26 |

27 | 28 | 29 | Number of PRs : 31 30 | 31 | |tag|number of PRs| 32 | |--|--:| 33 | |Breaking Changes | 11| 34 | |Performance | 3| 35 | |Rename | 3| 36 | |New Feature | 4| 37 | |Docs | 1| 38 | |Remove Symobl | 1| 39 | 40 | 41 | 42 | ### Group: Fix issues (3) 43 | - Make EventEmitter delivering event by commit order. [#222](https://github.com/VergeGroup/Verge/pull/222) by @muukii 44 | - `Breaking Changes` 45 | - Add runtime sanitizer - Debug only [#220](https://github.com/VergeGroup/Verge/pull/220) by @muukii 46 | - Fix InoutRef's wrapped property [#189](https://github.com/VergeGroup/Verge/pull/189) by @muukii 47 | 48 | 49 | 50 | ### Group: Enhancement (26) 51 | - Reduce reflecting to get performance [#226](https://github.com/VergeGroup/Verge/pull/226) by @muukii 52 | - `Breaking Changes` `Performance` 53 | - Improve EntityType performance [#224](https://github.com/VergeGroup/Verge/pull/224) by @muukii 54 | - `Performance` 55 | - Update default queue in sinkPrimitiveValue [#214](https://github.com/VergeGroup/Verge/pull/214) by @muukii 56 | - `Breaking Changes` 57 | - Reduce the number of Emitters [#216](https://github.com/VergeGroup/Verge/pull/216) by @muukii 58 | - Add runtime sanitizer - Debug only [#220](https://github.com/VergeGroup/Verge/pull/220) by @muukii 59 | - Add documentation [#219](https://github.com/VergeGroup/Verge/pull/219) by @muukii 60 | - Rename MemoizeMap to Pipeline [#211](https://github.com/VergeGroup/Verge/pull/211) by @muukii 61 | - `Rename` 62 | - Add Sink method to DispatcherType [#208](https://github.com/VergeGroup/Verge/pull/208) by @muukii 63 | - `New Feature` 64 | - [Experimental] Add creation method of SwiftUI.Binding [#210](https://github.com/VergeGroup/Verge/pull/210) by @muukii 65 | - Support RxSwift 6 [#209](https://github.com/VergeGroup/Verge/pull/209) by @muukii 66 | - `Breaking Changes` `New Feature` `Performance` 67 | - Update methods of Changes that become to use Comparer instead of closure [#207](https://github.com/VergeGroup/Verge/pull/207) by @muukii 68 | - `Breaking Changes` 69 | - Add isEmpty to EntityTable [#206](https://github.com/VergeGroup/Verge/pull/206) by @muukii 70 | - `New Feature` 71 | - Update Rx extension [#202](https://github.com/VergeGroup/Verge/pull/202) by @muukii 72 | - Add method that maps Edge [#201](https://github.com/VergeGroup/Verge/pull/201) by @muukii 73 | - `Rename` 74 | - Remove Verge/Core [#200](https://github.com/VergeGroup/Verge/pull/200) by @muukii 75 | - `Breaking Changes` 76 | - Update CachedMapStorage [#199](https://github.com/VergeGroup/Verge/pull/199) by @muukii 77 | - `New Feature` 78 | - Update how Derived retain itself to publish the value [#198](https://github.com/VergeGroup/Verge/pull/198) by @muukii 79 | - Update cancelling in EventEmitter [#197](https://github.com/VergeGroup/Verge/pull/197) by @muukii 80 | - `Breaking Changes` 81 | - Fix race-condition in VergeAnyCancellable [#196](https://github.com/VergeGroup/Verge/pull/196) by @muukii 82 | - Drop receive changes itself in Store, Dispatcher [#195](https://github.com/VergeGroup/Verge/pull/195) by @muukii 83 | - `Breaking Changes` 84 | - [Trivial] Update docs and few renames. [#194](https://github.com/VergeGroup/Verge/pull/194) by @muukii 85 | - `Docs` `Rename` 86 | - [ORM] context.entities [#192](https://github.com/VergeGroup/Verge/pull/192) by @muukii 87 | - Add precondition [#191](https://github.com/VergeGroup/Verge/pull/191) by @muukii 88 | - Changes get a modification that indicates how the state changed [#190](https://github.com/VergeGroup/Verge/pull/190) by @muukii 89 | - Support assign-assignee from Store [#187](https://github.com/VergeGroup/Verge/pull/187) by @muukii 90 | - `Breaking Changes` `Remove Symobl` 91 | - Deprecation combined derived method [#184](https://github.com/VergeGroup/Verge/pull/184) by @muukii 92 | - `Breaking Changes` 93 | 94 | 95 | ## Other (3) 96 | 97 | - Bump ini from 1.3.5 to 1.3.8 in /Docs [#205](https://github.com/VergeGroup/Verge/pull/205) by @dependabot 98 | - Changes default parameter which is queue in sink -> .mainIsolated() [#193](https://github.com/VergeGroup/Verge/pull/193) by @muukii 99 | - `Breaking Changes` 100 | - Support rx.commitBinder [#188](https://github.com/VergeGroup/Verge/pull/188) by @muukii 101 | 102 | 103 | --- 104 | 105 | Generated by chglog 106 | ; 107 | 108 |

109 |
110 | 111 | ## Usage 112 | 113 | ``` 114 | $ cd /path/to/your-repo 115 | ``` 116 | 117 | > ⚠️ 118 | Currently this cli must run in the directory where has .git. 119 | Because, Over GitHub API can't get well the all of commits between the specified ref range. 120 | So take care cloning git repo, chglog can get only commits which .git has. 121 | 122 | ``` 123 | $ chglog changelog --github_token -l 8.5.0 -r 8.6.0 124 | ``` 125 | 126 | > `--github_token` reads also enviroment variable `GITHUB_ACCESS_TOKEN`. 127 | > Or if you use [`gh`](https://github.com/cli/cli) and authenticated, that auth infomation would be used. 128 | 129 | ## Setting your repository up for creating a effective changelog 130 | 131 | `chglog` provides a built-in generator as a module. 132 | It would be used when we set no custom generator. 133 | 134 | That default generator has following features: 135 | 136 | - Grouping pull-request 137 | - Displaying the tags each pull-requests 138 | - Displaying summary of the tags of pull-request 139 | 140 | Setting up steps: 141 | 142 | **Define a label that has `Grooup: ` prefix.** 143 | 144 | CleanShot 2021-02-06 at 01 06 12@2x 145 | 146 | 147 | **Define a label that has `Tag: ` prefix** 148 | 149 | CleanShot 2021-02-06 at 01 06 57@2x 150 | 151 | You can add groups and tags as you need. 152 | Regarding those, the built-in generator parses and prints. 153 | 154 | ## Customization - Inject JS 155 | 156 | In addition using built-in generator, we can inject javascript code that generates a changelog with our own rules. 157 | 158 | Create javascript file and use following template code. 159 | 160 | ```js 161 | module.exports = () => { 162 | 163 | const state = { 164 | titles: [] 165 | } 166 | 167 | return { 168 | 169 | visit(pullRequest) { 170 | state.titles.push(pullRequest.title) 171 | }, 172 | 173 | visitLabel(label, pullRequest) { 174 | 175 | }, 176 | 177 | visitAuthor(author, pullRequest) { 178 | 179 | }, 180 | 181 | render() { 182 | 183 | return JSON.stringify(state, null, 2) 184 | } 185 | } 186 | } 187 | ``` 188 | 189 | Pssing this from argument 190 | 191 | ```sh 192 | $ chglog changelog -g /path/to/your_generator.js 193 | ``` 194 | 195 | ## Customization - From JS 196 | 197 | Define a visitor 198 | 199 | ```ts 200 | export interface Visitor { 201 | visitLabel(label: Label, source: PullRequest): void; 202 | visitAuthor(author: User, source: PullRequest): void; 203 | } 204 | ``` 205 | 206 | ```ts 207 | const createSampleVistor = () => { 208 | return { 209 | visitLabel(label: Label, source: PullRequest) { 210 | ... 211 | }, 212 | visitAuthor(author: User, source: PullRequest) { 213 | ... 214 | }, 215 | }; 216 | ``` 217 | 218 | Fetch and parse 219 | 220 | ```ts 221 | const visitor = createSampleVistor(); 222 | 223 | await fetchData( 224 | { 225 | rightRef: "", 226 | leftRef: "", 227 | githubToken: "", 228 | repoOwner: "", 229 | repoName: "", 230 | workingDirectory: "", 231 | }, 232 | visitor 233 | ); 234 | ``` 235 | 236 | ## Development 237 | 238 | Module resolutions: 239 | 240 | - core 241 | - extensions 242 | - cli 243 | 244 | 245 | Install dependencies 246 | 247 | ``` 248 | $ lerna bootstrap 249 | ``` 250 | 251 | Build all packages 252 | 253 | ``` 254 | $ lerna exec yarn run build 255 | ``` 256 | 257 | Run CLI 258 | 259 | ``` 260 | $ cd ./packages/cli 261 | $ yarn run dev 262 | ``` 263 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "2.2.0", 6 | "npmClient": "yarn", 7 | "useWorkspaces": true 8 | } 9 | -------------------------------------------------------------------------------- /my_generator.js: -------------------------------------------------------------------------------- 1 | console.log("Loaded my cutsom generator") 2 | 3 | module.exports = () => { 4 | 5 | const state = { 6 | titles: [] 7 | } 8 | 9 | return { 10 | 11 | visit(pullRequest) { 12 | state.titles.push(pullRequest.title) 13 | }, 14 | 15 | visitLabel(label, pullRequest) { 16 | 17 | }, 18 | 19 | visitAuthor(author, pullRequest) { 20 | 21 | }, 22 | 23 | render() { 24 | 25 | return JSON.stringify(state, null, 2) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yarn-workspace", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "yusuke mori <210861+jiska@users.noreply.github.com>", 6 | "license": "MIT", 7 | "private": true, 8 | "workspaces": [ 9 | "packages/*" 10 | ], 11 | "devDependencies": { 12 | "lerna": "^3.20.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/chglog_cli/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { fetchData, Visitor } from "@muukii/chglog_fetcher"; 4 | import commander, { exitOverride } from "commander"; 5 | import createVisitor from "@muukii/chglog_grouping_generator"; 6 | import path from "path"; 7 | const getGitHubURL = require("github-url-from-git"); 8 | import chalk from "chalk"; 9 | import yaml from "yaml"; 10 | import fs from "fs"; 11 | import os from "os"; 12 | 13 | import Git from "nodegit"; 14 | 15 | const log = console.log; 16 | const colorError = chalk.bold.red; 17 | const colorWarning = chalk.keyword("orange"); 18 | 19 | const logError = (args) => { 20 | console.log("❌ ", chalk.bold.red(args)); 21 | }; 22 | 23 | const logSuccess = (args) => { 24 | console.log("✅ ", chalk.bold.blue(args)); 25 | }; 26 | 27 | process.on("SIGTERM", () => { 28 | console.log("Process terminated"); 29 | }); 30 | 31 | process.on("uncaughtException", () => { 32 | process.exit(1); 33 | }); 34 | 35 | const errorHandler = (error) => { 36 | logError(error); 37 | process.exit(1); 38 | }; 39 | 40 | const main = async () => { 41 | const getGitHubOwnerRepo = async () => { 42 | const git = await Git.Repository.open("./").catch((error) => { 43 | throw "Not found git repository. CLI needs git in working directory."; 44 | }); 45 | 46 | const remote = await git.getRemote("origin"); 47 | const url = getGitHubURL(remote.url()); 48 | 49 | const regex = /https:\/\/github.com\/(.*)\/(.*)/; 50 | 51 | const result = regex.exec(url); 52 | 53 | if (!(result.length === 3)) { 54 | throw "Invalid url"; 55 | } 56 | 57 | return { 58 | owner: result[1], 59 | repo: result[2], 60 | }; 61 | }; 62 | 63 | const getGitHubTokenFromGH = () => { 64 | const file = fs.readFileSync( 65 | `${os.homedir()}/.config/gh/hosts.yml`, 66 | "utf8" 67 | ); 68 | const object = yaml.parse(file); 69 | 70 | const githubNode = object["github.com"]; 71 | 72 | if (!githubNode) { 73 | return null; 74 | } 75 | 76 | const token = githubNode["oauth_token"]; 77 | 78 | if (!token) { 79 | return null; 80 | } 81 | return token; 82 | }; 83 | 84 | const currentRepo = await getGitHubOwnerRepo(); 85 | 86 | const program = new commander.Command(); 87 | program.version(process.env.npm_package_version); 88 | 89 | program 90 | .command("changelog") 91 | .description("Generate changelog") 92 | .requiredOption("-l, --left ", "left side ref") 93 | .requiredOption("-r, --right ", "right side ref") 94 | .requiredOption("--repo ", "repo", currentRepo.repo) 95 | .requiredOption("--owner ", "owner", currentRepo.owner) 96 | .requiredOption( 97 | "--github_token ", 98 | "Access token for GitHub", 99 | getGitHubTokenFromGH() || process.env.GITHUB_ACCESS_TOKEN 100 | ) 101 | .option("-g, --generator ", "generator") 102 | .action(async (program: any) => { 103 | let generator: Visitor = null; 104 | if (program.generator) { 105 | const p = path.resolve(program.generator); 106 | generator = require(p)(); 107 | } else { 108 | generator = createVisitor(); 109 | } 110 | 111 | await fetchData( 112 | { 113 | leftRef: program.left, 114 | rightRef: program.right, 115 | githubToken: program.github_token, 116 | repoOwner: program.owner, 117 | repoName: program.repo, 118 | workingDirectory: "./", 119 | }, 120 | generator 121 | ).catch(errorHandler); 122 | const result = generator.render(); 123 | log(result); 124 | }); 125 | 126 | if (process.argv.length === 2) { 127 | log(process.env); 128 | log(program.helpInformation()); 129 | } else { 130 | program.parse(process.argv); 131 | } 132 | }; 133 | 134 | main().catch(errorHandler); 135 | -------------------------------------------------------------------------------- /packages/chglog_cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@muukii/chglog-cli", 3 | "version": "2.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@types/node": { 8 | "version": "14.14.25", 9 | "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz", 10 | "integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ==", 11 | "dev": true 12 | }, 13 | "@types/nodegit": { 14 | "version": "0.26.12", 15 | "resolved": "https://registry.npmjs.org/@types/nodegit/-/nodegit-0.26.12.tgz", 16 | "integrity": "sha512-4YpeTImFZNJ1cve4lEueHFVS8rAs8XpZqlmx+Bm9bMc+XMiCrcwaUf6peN7pod7Rl3esVlGP1zdBB7Z12eMVAA==", 17 | "dev": true, 18 | "requires": { 19 | "@types/node": "*" 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/chglog_cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@muukii/chglog-cli", 3 | "version": "2.2.0", 4 | "description": "A change log generator", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "tsc --build tsconfig.json", 10 | "prepublishOnly": "npm run build", 11 | "dev": "npm run build && node dist/index.js" 12 | }, 13 | "bin": { 14 | "chglog": "dist/index.js" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/muukii/chglog.git" 19 | }, 20 | "author": "muukii (http://muukii.app/)", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/muukii/chglog/issues" 24 | }, 25 | "homepage": "https://github.com/muukii/chglog#readme", 26 | "dependencies": { 27 | "@muukii/chglog_fetcher": "^2.1.0", 28 | "@muukii/chglog_grouping_generator": "^2.1.0", 29 | "chalk": "^4.1.0", 30 | "commander": "^7.0.0", 31 | "github-url-from-git": "^1.5.0", 32 | "nodegit": "^0.27.0", 33 | "ora": "^5.3.0", 34 | "yaml": "^1.10.0" 35 | }, 36 | "devDependencies": { 37 | "@types/nodegit": "^0.26.12", 38 | "typescript": "^4.1.3" 39 | }, 40 | "gitHead": "7cda4a9f928ad727d362da697eb6ce04364ffcc8", 41 | "publishConfig": { 42 | "access": "public" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/chglog_cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/chglog_fetcher/fetch.ts: -------------------------------------------------------------------------------- 1 | import util from "util"; 2 | import _ from "lodash"; 3 | import { graphql } from "@octokit/graphql"; 4 | import { Visitor } from "./Visitor"; 5 | const exec = util.promisify(require("child_process").exec); 6 | 7 | export type Context = { 8 | rightRef: string; 9 | leftRef: string; 10 | githubToken: string; 11 | repoOwner: string; 12 | repoName: string; 13 | workingDirectory: string; 14 | }; 15 | 16 | export type Response = { 17 | pullRequest: Record; 18 | }; 19 | 20 | export type User = { 21 | login: string; 22 | }; 23 | 24 | export type Label = { 25 | name: string; 26 | }; 27 | 28 | export type PullRequest = { 29 | author: User; 30 | editor: User; 31 | mergedBy: User; 32 | labels: { 33 | nodes: Label[]; 34 | }; 35 | merged: boolean; 36 | title: string; 37 | permalink: string; 38 | bodyText: string; 39 | number: number; 40 | id: string; 41 | }; 42 | 43 | export type PullRequestNode = { 44 | associatedPullRequests: { nodes: PullRequest[] }; 45 | }; 46 | 47 | const getCommitsFromGit = async ( 48 | leftRef: string, 49 | rightRef: string, 50 | workingDir: string 51 | ) => { 52 | const { stdout }: { stdout: string } = await exec( 53 | `cd ${workingDir} && git log --pretty=format:'%h' --abbrev-commit --right-only ${leftRef}..${rightRef}` 54 | ).catch((error) => { 55 | throw error; 56 | }); 57 | return stdout; 58 | }; 59 | 60 | const getPRsFromGit = async ( 61 | leftRef: string, 62 | rightRef: string, 63 | workingDir: string 64 | ) => { 65 | const { stdout }: { stdout: string } = await exec( 66 | `cd ${workingDir} && git log --oneline --abbrev-commit --right-only "${leftRef}..${rightRef}" | grep -Eo '#[0-9]+' | tr -d '#'` 67 | ).catch((error) => { 68 | throw error; 69 | }); 70 | return stdout; 71 | }; 72 | 73 | export const fetchData = async (context: Context, visitor: Visitor) => { 74 | const { githubToken, repoOwner, repoName } = context; 75 | 76 | const framgent = ` 77 | fragment PRParam on PullRequest { 78 | createdAt 79 | editor { 80 | login 81 | } 82 | author { 83 | login 84 | } 85 | merged 86 | mergedAt 87 | mergedBy { 88 | login 89 | } 90 | number 91 | permalink 92 | title 93 | labels(first: 100) { 94 | nodes { 95 | name 96 | } 97 | } 98 | id 99 | bodyText 100 | } 101 | `; 102 | 103 | const fetchDataFromCommits = async () => { 104 | const string = await getCommitsFromGit( 105 | context.leftRef, 106 | context.rightRef, 107 | context.workingDirectory 108 | ).catch((error) => { 109 | throw error; 110 | }); 111 | 112 | const commitRefs = string.split("\n").filter((e) => { 113 | return e.length > 0; 114 | }); 115 | 116 | if (commitRefs.length == 0) { 117 | return []; 118 | } 119 | 120 | const fetch = async (commitRefs: string[]) => { 121 | const frag = commitRefs 122 | .map((commitRef) => { 123 | return `ref_${commitRef}: object(expression: "${commitRef}") { 124 | ...PR 125 | } 126 | `; 127 | }) 128 | .join("\n"); 129 | 130 | const query = ` 131 | { 132 | pullRequest: repository(owner: "${repoOwner}", name: "${repoName}") { 133 | ${frag} 134 | } 135 | } 136 | 137 | ${framgent} 138 | 139 | fragment PR on GitObject { 140 | ... on Commit { 141 | associatedPullRequests(first: 100) { 142 | nodes { 143 | ...PRParam 144 | } 145 | } 146 | } 147 | } 148 | `; 149 | 150 | const result = (await graphql(query, { 151 | headers: { 152 | authorization: `token ${githubToken}`, 153 | }, 154 | })) as Response | null; 155 | return result!.pullRequest; 156 | }; 157 | 158 | let pullRequests: Record = {}; 159 | 160 | const chunks = _.chunk(commitRefs, 10); 161 | 162 | for (let slice of chunks) { 163 | pullRequests = { 164 | ...pullRequests, 165 | ...(await fetch(slice)), 166 | }; 167 | } 168 | 169 | let prs = _.flatMap( 170 | Object.values(pullRequests), 171 | (e) => e.associatedPullRequests.nodes 172 | ); 173 | 174 | return prs; 175 | }; 176 | 177 | const fetchDataFromPRNumbers = async () => { 178 | const string = await getPRsFromGit( 179 | context.leftRef, 180 | context.rightRef, 181 | context.workingDirectory 182 | ); 183 | 184 | const prNumbers = string.split("\n").filter((e) => { 185 | return e.length > 0; 186 | }); 187 | 188 | if (prNumbers.length == 0) { 189 | return []; 190 | } 191 | 192 | const frag = prNumbers 193 | .map((number) => { 194 | return `pr_${number}: pullRequest(number: ${number}) { 195 | ...PRParam 196 | } 197 | `; 198 | }) 199 | .join("\n"); 200 | 201 | const query = ` 202 | { 203 | repository(owner: "${repoOwner}", name: "${repoName}") { 204 | ${frag} 205 | } 206 | } 207 | 208 | ${framgent} 209 | 210 | `; 211 | 212 | const result: any = await graphql(query, { 213 | headers: { 214 | authorization: `token ${githubToken}`, 215 | }, 216 | }).catch((e) => { 217 | return e.data; 218 | }); 219 | 220 | const prs = Object.values(result.repository).filter((e) => e !== null); 221 | return prs as PullRequest[]; 222 | }; 223 | 224 | let prs: PullRequest[] = []; 225 | 226 | prs = prs.concat(await fetchDataFromCommits()); 227 | prs = prs.concat(await fetchDataFromPRNumbers()); 228 | 229 | prs = prs.filter((pr) => pr.merged == true); 230 | 231 | const unique: Record = {}; 232 | for (const pr of prs) { 233 | unique[pr.id] = pr; 234 | } 235 | 236 | Object.values(unique).forEach((source) => { 237 | visitor.visit(source); 238 | visitor.visitAuthor(source.author, source); 239 | source.labels.nodes.forEach((element) => { 240 | visitor.visitLabel(element, source); 241 | }); 242 | }); 243 | }; 244 | -------------------------------------------------------------------------------- /packages/chglog_fetcher/index.ts: -------------------------------------------------------------------------------- 1 | export * from './fetch'; 2 | export * from './Visitor'; 3 | -------------------------------------------------------------------------------- /packages/chglog_fetcher/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@muukii/chglog_fetcher", 3 | "version": "2.1.0", 4 | "description": "A change log generator", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "tsc --build tsconfig.json", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/muukii/chglog.git" 15 | }, 16 | "author": "muukii (http://muukii.app/)", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/muukii/chglog/issues" 20 | }, 21 | "homepage": "https://github.com/muukii/chglog#readme", 22 | "dependencies": { 23 | "@octokit/graphql": "^4.3.1", 24 | "commander": "^4.0.1", 25 | "lodash": "^4.17.15" 26 | }, 27 | "devDependencies": { 28 | "@types/lodash": "^4.14.149", 29 | "typescript": "^3.7.5" 30 | }, 31 | "gitHead": "7cda4a9f928ad727d362da697eb6ce04364ffcc8", 32 | "publishConfig": { 33 | "access": "public" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/chglog_fetcher/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/chglog_fetcher/visitor.ts: -------------------------------------------------------------------------------- 1 | import { Label, PullRequest, User } from "./fetch"; 2 | 3 | export interface Visitor { 4 | visit(source: PullRequest): void; 5 | visitLabel(label: Label, source: PullRequest): void; 6 | visitAuthor(author: User, source: PullRequest): void; 7 | render(): string; 8 | } 9 | -------------------------------------------------------------------------------- /packages/chglog_grouping_generator/index.ts: -------------------------------------------------------------------------------- 1 | import { User, PullRequest, Label } from "@muukii/chglog_fetcher"; 2 | import _ from "lodash"; 3 | 4 | const genName = (pr: PullRequest) => { 5 | const tag = pr.labels.nodes 6 | .map((e) => e.name) 7 | .filter((e) => e.includes("Tag:")) 8 | .map((e) => `\`${e.replace("Tag: ", "")}\``) 9 | .join(" ") 10 | .trim(); 11 | 12 | const body = [ 13 | `- ${pr.title} [#${pr.number}](${pr.permalink}) by @${pr.author.login}`, 14 | (() => { 15 | if (tag.length > 0) { 16 | return ` - ${tag}`; 17 | } else { 18 | return ""; 19 | } 20 | })(), 21 | ] 22 | .filter((e) => e.length > 0) 23 | .join("\n"); 24 | 25 | return body; 26 | }; 27 | 28 | export default () => { 29 | type State = { 30 | allPRs: PullRequest[]; 31 | // group : PR 32 | prs: Record; 33 | tags: Record; 34 | }; 35 | const state: State = { 36 | allPRs: [], 37 | prs: {}, 38 | tags: {}, 39 | }; 40 | 41 | return { 42 | visitLabel(label: Label, source: PullRequest) { 43 | if (!label.name.includes("Group:")) { 44 | return; 45 | } 46 | 47 | let array = state.prs[label.name]; 48 | if (array == null) { 49 | state.prs[label.name] = []; 50 | array = state.prs[label.name]; 51 | } 52 | 53 | const found = array.find( 54 | (element: any) => element.number == source.number 55 | ); 56 | 57 | if (!found) { 58 | array.push(source); 59 | } 60 | }, 61 | visit(source: PullRequest) { 62 | state.allPRs.push(source); 63 | 64 | source.labels.nodes.forEach((e) => { 65 | let array = state.tags[e.name]; 66 | if (array == null) { 67 | state.tags[e.name] = []; 68 | array = state.tags[e.name]; 69 | } 70 | array.push(source); 71 | }); 72 | }, 73 | visitAuthor(author: User, source: PullRequest) {}, 74 | render(): string { 75 | type Composed = { 76 | label: string; 77 | prs: PullRequest[]; 78 | }; 79 | 80 | let buf: Composed[] = []; 81 | for (const key in state.prs) { 82 | buf.push({ 83 | label: key, 84 | prs: state.prs[key], 85 | }); 86 | } 87 | 88 | buf.sort((a, b) => { 89 | return a.label < b.label ? 1 : -1; 90 | }); 91 | 92 | const groupedIDs = _(buf).flatMap((e) => e.prs.map((e) => e.id)); 93 | 94 | const ungrouped = state.allPRs.filter((e) => !groupedIDs.includes(e.id)); 95 | 96 | const grouped = buf; 97 | 98 | const renderTotal = () => { 99 | let line = "|tag|number of PRs|\n"; 100 | line += "|--|--:|\n"; 101 | for (const key in state.tags) { 102 | if (key.includes("Tag:")) { 103 | const count = state.tags[key].length; 104 | line += `|${key.replace("Tag: ", "")} | ${count}|\n`; 105 | } 106 | } 107 | return line; 108 | }; 109 | 110 | const renderGrouped = (item: Composed) => { 111 | return ` 112 | ### ${item.label} (${item.prs.length}) 113 | ${item.prs 114 | .map((e) => { 115 | return genName(e); 116 | }) 117 | .join("\n")} 118 | `; 119 | }; 120 | 121 | const renderUngrouped = (item: PullRequest[]) => { 122 | return ` 123 | ${item 124 | .map((e) => { 125 | return genName(e); 126 | }) 127 | .join("\n")} 128 | `; 129 | }; 130 | 131 | const body = ` 132 | Number of PRs : ${state.allPRs.length} 133 | 134 | ${renderTotal()} 135 | 136 | ${grouped.map(renderGrouped).join("\n\n")} 137 | 138 | ## Other (${ungrouped.length}) 139 | ${renderUngrouped(ungrouped)} 140 | 141 | --- 142 | 143 | Generated by chglog 144 | 145 | `; 146 | 147 | return `${body}`; 148 | }, 149 | }; 150 | }; 151 | -------------------------------------------------------------------------------- /packages/chglog_grouping_generator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@muukii/chglog_grouping_generator", 3 | "version": "2.1.0", 4 | "description": "A change log generator", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build": "tsc --build tsconfig.json", 10 | "prepublishOnly": "npm run build" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/muukii/chglog.git" 15 | }, 16 | "author": "muukii (http://muukii.app/)", 17 | "license": "MIT", 18 | "dependencies": { 19 | "@muukii/chglog_fetcher": "^2.1.0", 20 | "lodash": "^4.17.15" 21 | }, 22 | "devDependencies": { 23 | "typescript": "^4.1.3" 24 | }, 25 | "gitHead": "7cda4a9f928ad727d362da697eb6ce04364ffcc8", 26 | "publishConfig": { 27 | "access": "public" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/chglog_grouping_generator/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chglog-test", 3 | "version": "1.2.0", 4 | "description": "Test", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --build tsconfig.json", 9 | "prepublishOnly": "npm run build", 10 | "dev": "npm run build && node dist/index.js" 11 | }, 12 | "bin": { 13 | "chglog": "dist/index.js" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/muukii/chglog.git" 18 | }, 19 | "author": "muukii (http://muukii.app/)", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/muukii/chglog/issues" 23 | }, 24 | "homepage": "https://github.com/muukii/chglog#readme", 25 | "dependencies": { 26 | "@muukii/chglog": "^1.2.0", 27 | "@muukii/chglog_grouping_generator": "^1.2.0" 28 | }, 29 | "devDependencies": { 30 | "typescript": "^4.1.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/sample.ts: -------------------------------------------------------------------------------- 1 | import { fetchData } from '../index'; 2 | 3 | export const createSampleVistor = () => { 4 | const state = { 5 | prs: {} 6 | }; 7 | 8 | return { 9 | visit(source: any) {}, 10 | visitLabel(label: any, source: any) { 11 | let array = state.prs[label.name]; 12 | if (array == null) { 13 | state.prs[label.name] = []; 14 | array = state.prs[label.name]; 15 | } 16 | 17 | const found = array.find(element => element.number == source.number); 18 | 19 | if (!found) { 20 | array.push(source); 21 | } 22 | 23 | const element = source; 24 | }, 25 | visitAuthor(author, source) {}, 26 | generate() { 27 | const getListInLabel = label => { 28 | const list = state.prs[label]; 29 | if (list == null) { 30 | return 'Nothing'; 31 | } else { 32 | return list.map(element => { 33 | return `- ${element.title} [#${element.number}](${element.permalink}) by @${element.author.login}`; 34 | }).join`\n`; 35 | } 36 | }; 37 | 38 | const displayLabels = [ 39 | { name: 'FEATURE DELETED', prefix: '🗑' }, 40 | { name: 'FIX', prefix: '👾' }, 41 | { name: 'UI UPDATE', prefix: '👋' }, 42 | { name: 'DEVELOPING IMPROVEMENT', prefix: '🚚' }, 43 | { name: 'FIX INADVANCE', prefix: '🏔' }, 44 | { name: 'Significant Changes', prefix: '☄️' } 45 | ]; 46 | 47 | const hoge = displayLabels 48 | .map(label => { 49 | return ` 50 | ### ${[label.prefix, label.name].join(` `)} 51 | ${getListInLabel(label.name)} 52 | `; 53 | }) 54 | .join(' '); 55 | 56 | return `${hoge}`; 57 | } 58 | }; 59 | }; 60 | 61 | (async () => { 62 | const visitor = createSampleVistor(); 63 | 64 | await fetchData( 65 | { 66 | rightRef: '', 67 | leftRef: '', 68 | githubToken: '', 69 | repoOwner: '', 70 | repoName: '', 71 | workingDirectory: '' 72 | }, 73 | visitor 74 | ); 75 | 76 | console.log(visitor.generate()); 77 | })(); 78 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "outDir": "dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "esnext", 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "strict": false, 8 | "sourceMap": true, 9 | "skipLibCheck": true, 10 | "declaration": true, 11 | "pretty": true, 12 | "newLine": "lf" 13 | } 14 | } 15 | --------------------------------------------------------------------------------