├── .github ├── FUNDING.yml └── opencollective.yml ├── .gitignore ├── .storybook ├── addons.js └── config.js ├── .travis.yml ├── cli ├── .gitignore ├── cli.js ├── git.js ├── package-lock.json ├── package.json ├── readme.md ├── server.js └── yarn.lock ├── craco.config.js ├── license ├── netlify.toml ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json ├── readme.md ├── src ├── airframe │ ├── airframe.js │ └── easing.js ├── animation.js ├── app-helpers.js ├── app.js ├── avatar.addy.jpg ├── avatar.css-tricks.jpg ├── avatar.github.jpg ├── avatar.smashing.jpg ├── comment-box.css ├── demo.mp4 ├── demo.webm ├── duotoneLight.js ├── git-providers │ ├── bitbucket-commit-fetcher.js │ ├── bitbucket-provider.js │ ├── cli-commit-fetcher.js │ ├── cli-provider.js │ ├── differ.js │ ├── github-commit-fetcher.js │ ├── github-provider.js │ ├── gitlab-commit-fetcher.js │ ├── gitlab-provider.js │ ├── language-detector.js │ ├── language-detector.test.js │ ├── providers.js │ ├── sources.js │ ├── tokenizer.js │ ├── versioner.js │ ├── versioner.worker.js │ └── vscode-provider.js ├── history.js ├── icons │ ├── chrome.svg │ ├── cli.svg │ ├── firefox.svg │ └── vscode.svg ├── index.js ├── landing.css ├── landing.js ├── nightOwl.js ├── scroller.css ├── scroller.js ├── scroller.story.js ├── slide.js ├── use-spring.js ├── use-spring.story.js ├── use-virtual-children.js ├── utils.js └── utils.test.js ├── vscode-ext ├── .gitignore ├── .vscode │ └── launch.json ├── extension.js ├── git.js ├── images │ └── icon.png ├── jsconfig.json ├── package.json ├── readme.md ├── test-git.js ├── test │ ├── extension.test.js │ └── index.js ├── vsc-extension-quickstart.md └── yarn.lock └── yarn.lock /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | open_collective: git-history 2 | github: [pomber] 3 | custom: https://www.paypal.me/pomber 4 | -------------------------------------------------------------------------------- /.github/opencollective.yml: -------------------------------------------------------------------------------- 1 | collective: git-history 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /*/site -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import "@storybook/addon-actions/register"; 2 | import "@storybook/addon-links/register"; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from "@storybook/react"; 2 | 3 | const req = require.context("../src", true, /\.story\.js$/); 4 | 5 | function loadStories() { 6 | req.keys().forEach(filename => req(filename)); 7 | } 8 | 9 | configure(loadStories, module); 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | cache: 5 | yarn: true 6 | -------------------------------------------------------------------------------- /cli/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | site -------------------------------------------------------------------------------- /cli/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const runServer = require("./server"); 4 | const fs = require("fs"); 5 | 6 | let path = process.argv[2] || "./."; 7 | 8 | if (path === "--help") { 9 | console.log(`Usage: 10 | 11 | githistory some/file.ext 12 | `); 13 | process.exit(); 14 | } 15 | 16 | if (!fs.existsSync(path)) { 17 | console.log(`File not found: ${path}`); 18 | process.exit(); 19 | } 20 | 21 | runServer(path); 22 | -------------------------------------------------------------------------------- /cli/git.js: -------------------------------------------------------------------------------- 1 | const execa = require("execa"); 2 | const pather = require("path"); 3 | 4 | async function getCommits(path, last, before) { 5 | const format = `{"hash":"%h","author":{"login":"%aN"},"date":"%ad"},`; 6 | const { stdout } = await execa( 7 | "git", 8 | [ 9 | "log", 10 | `--max-count=${before ? last + 1 : last}`, 11 | `--pretty=format:${format}`, 12 | "--date=iso", 13 | `${before || "HEAD"}`, 14 | "--", 15 | pather.basename(path) 16 | ], 17 | { cwd: pather.dirname(path) } 18 | ); 19 | const json = `[${stdout.slice(0, -1)}]`; 20 | 21 | const messagesOutput = await execa( 22 | "git", 23 | [ 24 | "log", 25 | `--max-count=${last}`, 26 | `--pretty=format:%s`, 27 | `${before || "HEAD"}`, 28 | "--", 29 | pather.basename(path) 30 | ], 31 | { cwd: pather.dirname(path) } 32 | ); 33 | 34 | const messages = messagesOutput.stdout.replace('"', '\\"').split(/\r?\n/); 35 | 36 | const result = JSON.parse(json).map((commit, i) => ({ 37 | ...commit, 38 | date: new Date(commit.date), 39 | message: messages[i] 40 | })); 41 | 42 | return before ? result.slice(1) : result; 43 | } 44 | 45 | async function getContent(commit, path) { 46 | const { stdout } = await execa( 47 | "git", 48 | ["show", `${commit.hash}:./${pather.basename(path)}`], 49 | { cwd: pather.dirname(path) } 50 | ); 51 | return stdout; 52 | } 53 | 54 | module.exports = async function(path, last, before) { 55 | const commits = await getCommits(path, last, before); 56 | await Promise.all( 57 | commits.map(async commit => { 58 | commit.content = await getContent(commit, path); 59 | }) 60 | ); 61 | return commits; 62 | }; 63 | -------------------------------------------------------------------------------- /cli/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-file-history", 3 | "version": "0.1.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "accepts": { 8 | "version": "1.3.5", 9 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz", 10 | "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=", 11 | "requires": { 12 | "mime-types": "~2.1.18", 13 | "negotiator": "0.6.1" 14 | } 15 | }, 16 | "ansi-regex": { 17 | "version": "3.0.0", 18 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 19 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=" 20 | }, 21 | "any-promise": { 22 | "version": "1.3.0", 23 | "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", 24 | "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" 25 | }, 26 | "balanced-match": { 27 | "version": "1.0.0", 28 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 29 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" 30 | }, 31 | "brace-expansion": { 32 | "version": "1.1.11", 33 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 34 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 35 | "requires": { 36 | "balanced-match": "^1.0.0", 37 | "concat-map": "0.0.1" 38 | } 39 | }, 40 | "bytes": { 41 | "version": "3.0.0", 42 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", 43 | "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=" 44 | }, 45 | "cache-content-type": { 46 | "version": "1.0.1", 47 | "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", 48 | "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", 49 | "requires": { 50 | "mime-types": "^2.1.18", 51 | "ylru": "^1.2.0" 52 | } 53 | }, 54 | "cliui": { 55 | "version": "4.1.0", 56 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", 57 | "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", 58 | "requires": { 59 | "string-width": "^2.1.1", 60 | "strip-ansi": "^4.0.0", 61 | "wrap-ansi": "^2.0.0" 62 | }, 63 | "dependencies": { 64 | "is-fullwidth-code-point": { 65 | "version": "1.0.0", 66 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 67 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 68 | "requires": { 69 | "number-is-nan": "^1.0.0" 70 | } 71 | }, 72 | "strip-ansi": { 73 | "version": "4.0.0", 74 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 75 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 76 | "requires": { 77 | "ansi-regex": "^3.0.0" 78 | } 79 | }, 80 | "wrap-ansi": { 81 | "version": "2.1.0", 82 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", 83 | "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", 84 | "requires": { 85 | "string-width": "^1.0.1", 86 | "strip-ansi": "^3.0.1" 87 | }, 88 | "dependencies": { 89 | "ansi-regex": { 90 | "version": "2.1.1", 91 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 92 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" 93 | }, 94 | "string-width": { 95 | "version": "1.0.2", 96 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 97 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 98 | "requires": { 99 | "code-point-at": "^1.0.0", 100 | "is-fullwidth-code-point": "^1.0.0", 101 | "strip-ansi": "^3.0.0" 102 | } 103 | }, 104 | "strip-ansi": { 105 | "version": "3.0.1", 106 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 107 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 108 | "requires": { 109 | "ansi-regex": "^2.0.0" 110 | } 111 | } 112 | } 113 | } 114 | } 115 | }, 116 | "co": { 117 | "version": "4.6.0", 118 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 119 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" 120 | }, 121 | "code-point-at": { 122 | "version": "1.1.0", 123 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 124 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" 125 | }, 126 | "concat-map": { 127 | "version": "0.0.1", 128 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 129 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" 130 | }, 131 | "content-disposition": { 132 | "version": "0.5.2", 133 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", 134 | "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ=" 135 | }, 136 | "content-type": { 137 | "version": "1.0.4", 138 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 139 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 140 | }, 141 | "cookies": { 142 | "version": "0.7.3", 143 | "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.7.3.tgz", 144 | "integrity": "sha512-+gixgxYSgQLTaTIilDHAdlNPZDENDQernEMiIcZpYYP14zgHsCt4Ce1FEjFtcp6GefhozebB6orvhAAWx/IS0A==", 145 | "requires": { 146 | "depd": "~1.1.2", 147 | "keygrip": "~1.0.3" 148 | } 149 | }, 150 | "cross-env": { 151 | "version": "5.2.0", 152 | "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-5.2.0.tgz", 153 | "integrity": "sha512-jtdNFfFW1hB7sMhr/H6rW1Z45LFqyI431m3qU6bFXcQ3Eh7LtBuG3h74o7ohHZ3crrRkkqHlo4jYHFPcjroANg==", 154 | "dev": true, 155 | "requires": { 156 | "cross-spawn": "^6.0.5", 157 | "is-windows": "^1.0.0" 158 | } 159 | }, 160 | "cross-spawn": { 161 | "version": "6.0.5", 162 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 163 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 164 | "requires": { 165 | "nice-try": "^1.0.4", 166 | "path-key": "^2.0.1", 167 | "semver": "^5.5.0", 168 | "shebang-command": "^1.2.0", 169 | "which": "^1.2.9" 170 | } 171 | }, 172 | "debug": { 173 | "version": "3.1.0", 174 | "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", 175 | "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", 176 | "requires": { 177 | "ms": "2.0.0" 178 | } 179 | }, 180 | "decamelize": { 181 | "version": "1.2.0", 182 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 183 | "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" 184 | }, 185 | "deep-equal": { 186 | "version": "1.0.1", 187 | "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", 188 | "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" 189 | }, 190 | "delegates": { 191 | "version": "1.0.0", 192 | "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", 193 | "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" 194 | }, 195 | "depd": { 196 | "version": "1.1.2", 197 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 198 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 199 | }, 200 | "destroy": { 201 | "version": "1.0.4", 202 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 203 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 204 | }, 205 | "ee-first": { 206 | "version": "1.1.1", 207 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 208 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 209 | }, 210 | "emoji-regex": { 211 | "version": "7.0.3", 212 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 213 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" 214 | }, 215 | "end-of-stream": { 216 | "version": "1.4.1", 217 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz", 218 | "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==", 219 | "requires": { 220 | "once": "^1.4.0" 221 | } 222 | }, 223 | "error-inject": { 224 | "version": "1.0.0", 225 | "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", 226 | "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" 227 | }, 228 | "escape-html": { 229 | "version": "1.0.3", 230 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 231 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 232 | }, 233 | "execa": { 234 | "version": "1.0.0", 235 | "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", 236 | "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", 237 | "requires": { 238 | "cross-spawn": "^6.0.0", 239 | "get-stream": "^4.0.0", 240 | "is-stream": "^1.1.0", 241 | "npm-run-path": "^2.0.0", 242 | "p-finally": "^1.0.0", 243 | "signal-exit": "^3.0.0", 244 | "strip-eof": "^1.0.0" 245 | } 246 | }, 247 | "fast-url-parser": { 248 | "version": "1.1.3", 249 | "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", 250 | "integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=", 251 | "requires": { 252 | "punycode": "^1.3.2" 253 | } 254 | }, 255 | "fresh": { 256 | "version": "0.5.2", 257 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 258 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 259 | }, 260 | "get-caller-file": { 261 | "version": "2.0.5", 262 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 263 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" 264 | }, 265 | "get-port": { 266 | "version": "4.1.0", 267 | "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.1.0.tgz", 268 | "integrity": "sha512-4/fqAYrzrzOiqDrdeZRKXGdTGgbkfTEumGlNQPeP6Jy8w0PzN9mzeNQ3XgHaTNie8pQ3hOUkrwlZt2Fzk5H9mA==" 269 | }, 270 | "get-stream": { 271 | "version": "4.1.0", 272 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", 273 | "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", 274 | "requires": { 275 | "pump": "^3.0.0" 276 | } 277 | }, 278 | "http-assert": { 279 | "version": "1.4.0", 280 | "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.0.tgz", 281 | "integrity": "sha512-tPVv62a6l3BbQoM/N5qo969l0OFxqpnQzNUPeYfTP6Spo4zkgWeDBD1D5thI7sDLg7jCCihXTLB0X8UtdyAy8A==", 282 | "requires": { 283 | "deep-equal": "~1.0.1", 284 | "http-errors": "~1.7.1" 285 | } 286 | }, 287 | "http-errors": { 288 | "version": "1.7.2", 289 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 290 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 291 | "requires": { 292 | "depd": "~1.1.2", 293 | "inherits": "2.0.3", 294 | "setprototypeof": "1.1.1", 295 | "statuses": ">= 1.5.0 < 2", 296 | "toidentifier": "1.0.0" 297 | } 298 | }, 299 | "inherits": { 300 | "version": "2.0.3", 301 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 302 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 303 | }, 304 | "invert-kv": { 305 | "version": "2.0.0", 306 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz", 307 | "integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==" 308 | }, 309 | "is-fullwidth-code-point": { 310 | "version": "2.0.0", 311 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 312 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" 313 | }, 314 | "is-generator-function": { 315 | "version": "1.0.7", 316 | "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", 317 | "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" 318 | }, 319 | "is-stream": { 320 | "version": "1.1.0", 321 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 322 | "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=" 323 | }, 324 | "is-windows": { 325 | "version": "1.0.2", 326 | "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", 327 | "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", 328 | "dev": true 329 | }, 330 | "isarray": { 331 | "version": "0.0.1", 332 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", 333 | "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" 334 | }, 335 | "isexe": { 336 | "version": "2.0.0", 337 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 338 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 339 | }, 340 | "keygrip": { 341 | "version": "1.0.3", 342 | "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.0.3.tgz", 343 | "integrity": "sha512-/PpesirAIfaklxUzp4Yb7xBper9MwP6hNRA6BGGUFCgbJ+BM5CKBtsoxinNXkLHAr+GXS1/lSlF2rP7cv5Fl+g==" 344 | }, 345 | "koa": { 346 | "version": "2.7.0", 347 | "resolved": "https://registry.npmjs.org/koa/-/koa-2.7.0.tgz", 348 | "integrity": "sha512-7ojD05s2Q+hFudF8tDLZ1CpCdVZw8JQELWSkcfG9bdtoTDzMmkRF6BQBU7JzIzCCOY3xd3tftiy/loHBUYaY2Q==", 349 | "requires": { 350 | "accepts": "^1.3.5", 351 | "cache-content-type": "^1.0.0", 352 | "content-disposition": "~0.5.2", 353 | "content-type": "^1.0.4", 354 | "cookies": "~0.7.1", 355 | "debug": "~3.1.0", 356 | "delegates": "^1.0.0", 357 | "depd": "^1.1.2", 358 | "destroy": "^1.0.4", 359 | "error-inject": "^1.0.0", 360 | "escape-html": "^1.0.3", 361 | "fresh": "~0.5.2", 362 | "http-assert": "^1.3.0", 363 | "http-errors": "^1.6.3", 364 | "is-generator-function": "^1.0.7", 365 | "koa-compose": "^4.1.0", 366 | "koa-convert": "^1.2.0", 367 | "koa-is-json": "^1.0.0", 368 | "on-finished": "^2.3.0", 369 | "only": "~0.0.2", 370 | "parseurl": "^1.3.2", 371 | "statuses": "^1.5.0", 372 | "type-is": "^1.6.16", 373 | "vary": "^1.1.2" 374 | } 375 | }, 376 | "koa-compose": { 377 | "version": "4.1.0", 378 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", 379 | "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" 380 | }, 381 | "koa-convert": { 382 | "version": "1.2.0", 383 | "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", 384 | "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", 385 | "requires": { 386 | "co": "^4.6.0", 387 | "koa-compose": "^3.0.0" 388 | }, 389 | "dependencies": { 390 | "koa-compose": { 391 | "version": "3.2.1", 392 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 393 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 394 | "requires": { 395 | "any-promise": "^1.1.0" 396 | } 397 | } 398 | } 399 | }, 400 | "koa-is-json": { 401 | "version": "1.0.0", 402 | "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", 403 | "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" 404 | }, 405 | "koa-router": { 406 | "version": "7.4.0", 407 | "resolved": "https://registry.npmjs.org/koa-router/-/koa-router-7.4.0.tgz", 408 | "integrity": "sha512-IWhaDXeAnfDBEpWS6hkGdZ1ablgr6Q6pGdXCyK38RbzuH4LkUOpPqPw+3f8l8aTDrQmBQ7xJc0bs2yV4dzcO+g==", 409 | "requires": { 410 | "debug": "^3.1.0", 411 | "http-errors": "^1.3.1", 412 | "koa-compose": "^3.0.0", 413 | "methods": "^1.0.1", 414 | "path-to-regexp": "^1.1.1", 415 | "urijs": "^1.19.0" 416 | }, 417 | "dependencies": { 418 | "koa-compose": { 419 | "version": "3.2.1", 420 | "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", 421 | "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", 422 | "requires": { 423 | "any-promise": "^1.1.0" 424 | } 425 | }, 426 | "path-to-regexp": { 427 | "version": "1.7.0", 428 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", 429 | "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", 430 | "requires": { 431 | "isarray": "0.0.1" 432 | } 433 | } 434 | } 435 | }, 436 | "koa-send": { 437 | "version": "5.0.0", 438 | "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.0.tgz", 439 | "integrity": "sha512-90ZotV7t0p3uN9sRwW2D484rAaKIsD8tAVtypw/aBU+ryfV+fR2xrcAwhI8Wl6WRkojLUs/cB9SBSCuIb+IanQ==", 440 | "requires": { 441 | "debug": "^3.1.0", 442 | "http-errors": "^1.6.3", 443 | "mz": "^2.7.0", 444 | "resolve-path": "^1.4.0" 445 | } 446 | }, 447 | "koa-static": { 448 | "version": "5.0.0", 449 | "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", 450 | "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", 451 | "requires": { 452 | "debug": "^3.1.0", 453 | "koa-send": "^5.0.0" 454 | } 455 | }, 456 | "lcid": { 457 | "version": "2.0.0", 458 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", 459 | "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", 460 | "requires": { 461 | "invert-kv": "^2.0.0" 462 | } 463 | }, 464 | "map-age-cleaner": { 465 | "version": "0.1.3", 466 | "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", 467 | "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", 468 | "requires": { 469 | "p-defer": "^1.0.0" 470 | } 471 | }, 472 | "media-typer": { 473 | "version": "0.3.0", 474 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 475 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 476 | }, 477 | "mem": { 478 | "version": "4.1.0", 479 | "resolved": "https://registry.npmjs.org/mem/-/mem-4.1.0.tgz", 480 | "integrity": "sha512-I5u6Q1x7wxO0kdOpYBB28xueHADYps5uty/zg936CiG8NTe5sJL8EjrCuLneuDW3PlMdZBGDIn8BirEVdovZvg==", 481 | "requires": { 482 | "map-age-cleaner": "^0.1.1", 483 | "mimic-fn": "^1.0.0", 484 | "p-is-promise": "^2.0.0" 485 | } 486 | }, 487 | "methods": { 488 | "version": "1.1.2", 489 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 490 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 491 | }, 492 | "mime-db": { 493 | "version": "1.33.0", 494 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", 495 | "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==" 496 | }, 497 | "mime-types": { 498 | "version": "2.1.18", 499 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", 500 | "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", 501 | "requires": { 502 | "mime-db": "~1.33.0" 503 | } 504 | }, 505 | "mimic-fn": { 506 | "version": "1.2.0", 507 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", 508 | "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==" 509 | }, 510 | "minimatch": { 511 | "version": "3.0.4", 512 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 513 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 514 | "requires": { 515 | "brace-expansion": "^1.1.7" 516 | } 517 | }, 518 | "ms": { 519 | "version": "2.0.0", 520 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 521 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 522 | }, 523 | "mz": { 524 | "version": "2.7.0", 525 | "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", 526 | "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", 527 | "requires": { 528 | "any-promise": "^1.0.0", 529 | "object-assign": "^4.0.1", 530 | "thenify-all": "^1.0.0" 531 | } 532 | }, 533 | "negotiator": { 534 | "version": "0.6.1", 535 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", 536 | "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk=" 537 | }, 538 | "nice-try": { 539 | "version": "1.0.5", 540 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 541 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" 542 | }, 543 | "npm-run-path": { 544 | "version": "2.0.2", 545 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", 546 | "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", 547 | "requires": { 548 | "path-key": "^2.0.0" 549 | } 550 | }, 551 | "number-is-nan": { 552 | "version": "1.0.1", 553 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 554 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" 555 | }, 556 | "object-assign": { 557 | "version": "4.1.1", 558 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 559 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" 560 | }, 561 | "on-finished": { 562 | "version": "2.3.0", 563 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 564 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 565 | "requires": { 566 | "ee-first": "1.1.1" 567 | } 568 | }, 569 | "once": { 570 | "version": "1.4.0", 571 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 572 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 573 | "requires": { 574 | "wrappy": "1" 575 | } 576 | }, 577 | "only": { 578 | "version": "0.0.2", 579 | "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", 580 | "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" 581 | }, 582 | "open": { 583 | "version": "0.0.5", 584 | "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", 585 | "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" 586 | }, 587 | "opencollective-postinstall": { 588 | "version": "2.0.2", 589 | "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz", 590 | "integrity": "sha512-pVOEP16TrAO2/fjej1IdOyupJY8KDUM1CvsaScRbw6oddvpQoOfGk4ywha0HKKVAD6RkW4x6Q+tNBwhf3Bgpuw==" 591 | }, 592 | "os-locale": { 593 | "version": "3.1.0", 594 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz", 595 | "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", 596 | "requires": { 597 | "execa": "^1.0.0", 598 | "lcid": "^2.0.0", 599 | "mem": "^4.0.0" 600 | } 601 | }, 602 | "p-defer": { 603 | "version": "1.0.0", 604 | "resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz", 605 | "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=" 606 | }, 607 | "p-finally": { 608 | "version": "1.0.0", 609 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", 610 | "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=" 611 | }, 612 | "p-is-promise": { 613 | "version": "2.0.0", 614 | "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.0.0.tgz", 615 | "integrity": "sha512-pzQPhYMCAgLAKPWD2jC3Se9fEfrD9npNos0y150EeqZll7akhEgGhTW/slB6lHku8AvYGiJ+YJ5hfHKePPgFWg==" 616 | }, 617 | "parseurl": { 618 | "version": "1.3.2", 619 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz", 620 | "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M=" 621 | }, 622 | "path-exists": { 623 | "version": "3.0.0", 624 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 625 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" 626 | }, 627 | "path-is-absolute": { 628 | "version": "1.0.1", 629 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 630 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" 631 | }, 632 | "path-is-inside": { 633 | "version": "1.0.2", 634 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 635 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=" 636 | }, 637 | "path-key": { 638 | "version": "2.0.1", 639 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 640 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=" 641 | }, 642 | "path-to-regexp": { 643 | "version": "2.2.1", 644 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", 645 | "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==" 646 | }, 647 | "pump": { 648 | "version": "3.0.0", 649 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 650 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 651 | "requires": { 652 | "end-of-stream": "^1.1.0", 653 | "once": "^1.3.1" 654 | } 655 | }, 656 | "punycode": { 657 | "version": "1.4.1", 658 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", 659 | "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" 660 | }, 661 | "range-parser": { 662 | "version": "1.2.0", 663 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", 664 | "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4=" 665 | }, 666 | "require-directory": { 667 | "version": "2.1.1", 668 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 669 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" 670 | }, 671 | "require-main-filename": { 672 | "version": "2.0.0", 673 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", 674 | "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" 675 | }, 676 | "resolve-path": { 677 | "version": "1.4.0", 678 | "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", 679 | "integrity": "sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc=", 680 | "requires": { 681 | "http-errors": "~1.6.2", 682 | "path-is-absolute": "1.0.1" 683 | }, 684 | "dependencies": { 685 | "http-errors": { 686 | "version": "1.6.3", 687 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", 688 | "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", 689 | "requires": { 690 | "depd": "~1.1.2", 691 | "inherits": "2.0.3", 692 | "setprototypeof": "1.1.0", 693 | "statuses": ">= 1.4.0 < 2" 694 | } 695 | }, 696 | "setprototypeof": { 697 | "version": "1.1.0", 698 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", 699 | "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" 700 | } 701 | } 702 | }, 703 | "semver": { 704 | "version": "5.6.0", 705 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz", 706 | "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg==" 707 | }, 708 | "serve-handler": { 709 | "version": "5.0.8", 710 | "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-5.0.8.tgz", 711 | "integrity": "sha512-pqk0SChbBLLHfMIxQ55czjdiW7tj2cFy53svvP8e5VqEN/uB/QpfiTJ8k1uIYeFTDVoi+FGi5aqXScuu88bymg==", 712 | "requires": { 713 | "bytes": "3.0.0", 714 | "content-disposition": "0.5.2", 715 | "fast-url-parser": "1.1.3", 716 | "mime-types": "2.1.18", 717 | "minimatch": "3.0.4", 718 | "path-is-inside": "1.0.2", 719 | "path-to-regexp": "2.2.1", 720 | "range-parser": "1.2.0" 721 | } 722 | }, 723 | "set-blocking": { 724 | "version": "2.0.0", 725 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 726 | "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" 727 | }, 728 | "setprototypeof": { 729 | "version": "1.1.1", 730 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 731 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 732 | }, 733 | "shebang-command": { 734 | "version": "1.2.0", 735 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 736 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 737 | "requires": { 738 | "shebang-regex": "^1.0.0" 739 | } 740 | }, 741 | "shebang-regex": { 742 | "version": "1.0.0", 743 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 744 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=" 745 | }, 746 | "signal-exit": { 747 | "version": "3.0.2", 748 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 749 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 750 | }, 751 | "statuses": { 752 | "version": "1.5.0", 753 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 754 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 755 | }, 756 | "string-width": { 757 | "version": "2.1.1", 758 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 759 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 760 | "requires": { 761 | "is-fullwidth-code-point": "^2.0.0", 762 | "strip-ansi": "^4.0.0" 763 | }, 764 | "dependencies": { 765 | "strip-ansi": { 766 | "version": "4.0.0", 767 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 768 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 769 | "requires": { 770 | "ansi-regex": "^3.0.0" 771 | } 772 | } 773 | } 774 | }, 775 | "strip-eof": { 776 | "version": "1.0.0", 777 | "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", 778 | "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=" 779 | }, 780 | "thenify": { 781 | "version": "3.3.0", 782 | "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", 783 | "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", 784 | "requires": { 785 | "any-promise": "^1.0.0" 786 | } 787 | }, 788 | "thenify-all": { 789 | "version": "1.6.0", 790 | "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", 791 | "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=", 792 | "requires": { 793 | "thenify": ">= 3.1.0 < 4" 794 | } 795 | }, 796 | "toidentifier": { 797 | "version": "1.0.0", 798 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 799 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 800 | }, 801 | "type-is": { 802 | "version": "1.6.16", 803 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz", 804 | "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==", 805 | "requires": { 806 | "media-typer": "0.3.0", 807 | "mime-types": "~2.1.18" 808 | } 809 | }, 810 | "urijs": { 811 | "version": "1.19.1", 812 | "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.1.tgz", 813 | "integrity": "sha512-xVrGVi94ueCJNrBSTjWqjvtgvl3cyOTThp2zaMaFNGp3F542TR6sM3f2o8RqZl+AwteClSVmoCyt0ka4RjQOQg==" 814 | }, 815 | "vary": { 816 | "version": "1.1.2", 817 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 818 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 819 | }, 820 | "which": { 821 | "version": "1.3.1", 822 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 823 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 824 | "requires": { 825 | "isexe": "^2.0.0" 826 | } 827 | }, 828 | "which-module": { 829 | "version": "2.0.0", 830 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", 831 | "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" 832 | }, 833 | "wrappy": { 834 | "version": "1.0.2", 835 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 836 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 837 | }, 838 | "y18n": { 839 | "version": "4.0.0", 840 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz", 841 | "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w==" 842 | }, 843 | "yargs": { 844 | "version": "13.2.2", 845 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.2.tgz", 846 | "integrity": "sha512-WyEoxgyTD3w5XRpAQNYUB9ycVH/PQrToaTXdYXRdOXvEy1l19br+VJsc0vcO8PTGg5ro/l/GY7F/JMEBmI0BxA==", 847 | "requires": { 848 | "cliui": "^4.0.0", 849 | "find-up": "^3.0.0", 850 | "get-caller-file": "^2.0.1", 851 | "os-locale": "^3.1.0", 852 | "require-directory": "^2.1.1", 853 | "require-main-filename": "^2.0.0", 854 | "set-blocking": "^2.0.0", 855 | "string-width": "^3.0.0", 856 | "which-module": "^2.0.0", 857 | "y18n": "^4.0.0", 858 | "yargs-parser": "^13.0.0" 859 | }, 860 | "dependencies": { 861 | "ansi-regex": { 862 | "version": "4.1.0", 863 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 864 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" 865 | }, 866 | "camelcase": { 867 | "version": "5.2.0", 868 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz", 869 | "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==" 870 | }, 871 | "find-up": { 872 | "version": "3.0.0", 873 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", 874 | "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", 875 | "requires": { 876 | "locate-path": "^3.0.0" 877 | } 878 | }, 879 | "locate-path": { 880 | "version": "3.0.0", 881 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", 882 | "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", 883 | "requires": { 884 | "p-locate": "^3.0.0", 885 | "path-exists": "^3.0.0" 886 | } 887 | }, 888 | "p-limit": { 889 | "version": "2.2.0", 890 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", 891 | "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", 892 | "requires": { 893 | "p-try": "^2.0.0" 894 | } 895 | }, 896 | "p-locate": { 897 | "version": "3.0.0", 898 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", 899 | "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", 900 | "requires": { 901 | "p-limit": "^2.0.0" 902 | } 903 | }, 904 | "p-try": { 905 | "version": "2.1.0", 906 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.1.0.tgz", 907 | "integrity": "sha512-H2RyIJ7+A3rjkwKC2l5GGtU4H1vkxKCAGsWasNVd0Set+6i4znxbWy6/j16YDPJDWxhsgZiKAstMEP8wCdSpjA==" 908 | }, 909 | "string-width": { 910 | "version": "3.1.0", 911 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 912 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 913 | "requires": { 914 | "emoji-regex": "^7.0.1", 915 | "is-fullwidth-code-point": "^2.0.0", 916 | "strip-ansi": "^5.1.0" 917 | } 918 | }, 919 | "strip-ansi": { 920 | "version": "5.2.0", 921 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 922 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 923 | "requires": { 924 | "ansi-regex": "^4.1.0" 925 | } 926 | }, 927 | "yargs-parser": { 928 | "version": "13.0.0", 929 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.0.0.tgz", 930 | "integrity": "sha512-w2LXjoL8oRdRQN+hOyppuXs+V/fVAYtpcrRxZuF7Kt/Oc+Jr2uAcVntaUTNT6w5ihoWfFDpNY8CPx1QskxZ/pw==", 931 | "requires": { 932 | "camelcase": "^5.0.0", 933 | "decamelize": "^1.2.0" 934 | } 935 | } 936 | } 937 | }, 938 | "ylru": { 939 | "version": "1.2.1", 940 | "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", 941 | "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" 942 | } 943 | } 944 | } 945 | -------------------------------------------------------------------------------- /cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "git-file-history", 3 | "description": "Quickly browse the history of a file from any git repository", 4 | "version": "1.0.1", 5 | "repository": "pomber/git-history", 6 | "keywords": [ 7 | "cli", 8 | "git", 9 | "file", 10 | "history", 11 | "log", 12 | "commits", 13 | "change", 14 | "animation", 15 | "gui" 16 | ], 17 | "license": "MIT", 18 | "bin": { 19 | "git-file-history": "./cli.js", 20 | "githistory": "./cli.js", 21 | "git-history": "./cli.js" 22 | }, 23 | "files": [ 24 | "site", 25 | "*.js" 26 | ], 27 | "dependencies": { 28 | "execa": "^1.0.0", 29 | "get-port": "^4.1.0", 30 | "koa": "^2.7.0", 31 | "koa-router": "^7.4.0", 32 | "koa-static": "^5.0.0", 33 | "open": "^0.0.5", 34 | "opencollective-postinstall": "^2.0.2", 35 | "serve-handler": "^5.0.8", 36 | "yargs": "^13.2.2" 37 | }, 38 | "scripts": { 39 | "build-site": "cd .. && cross-env REACT_APP_GIT_PROVIDER=cli yarn build && rm -fr cli/site/ && cp -r build/ cli/site/", 40 | "build": "yarn build-site", 41 | "ls-package": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz", 42 | "postinstall": "opencollective-postinstall" 43 | }, 44 | "devDependencies": { 45 | "cross-env": "^5.2.0" 46 | }, 47 | "collective": { 48 | "type": "opencollective", 49 | "url": "https://opencollective.com/git-history" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /cli/readme.md: -------------------------------------------------------------------------------- 1 |
2 | demo 3 |
4 | 5 | # Git History CLI 6 | 7 | Quickly browse the history of a file from any git repository. 8 | 9 | > You need [node](https://nodejs.org/en/) to run this 10 | 11 | ```bash 12 | $ npx git-file-history path/to/file.ext 13 | ``` 14 | 15 | or 16 | 17 | ```bash 18 | $ npm install -g git-file-history 19 | $ git-file-history path/to/file.ext 20 | ``` 21 | 22 | ### Sponsors 23 | 24 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/git-history#sponsor)] 25 | 26 | 27 | 28 | ### Backers 29 | 30 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)] 31 | 32 | 33 | -------------------------------------------------------------------------------- /cli/server.js: -------------------------------------------------------------------------------- 1 | const serve = require("koa-static"); 2 | const Koa = require("koa"); 3 | const pather = require("path"); 4 | const getCommits = require("./git"); 5 | const getPort = require("get-port"); 6 | const open = require("open"); 7 | const router = require("koa-router")(); 8 | const argv = require("yargs") 9 | .usage("Usage: $0 [options]") 10 | .describe("port", "Port number (default = 5000)") 11 | .default("port", 5000).argv; 12 | 13 | const sitePath = pather.join(__dirname, "site/"); 14 | 15 | router.get("/api/commits", async ctx => { 16 | const query = ctx.query; 17 | const { path, last = 10, before = null } = query; 18 | 19 | const commits = await getCommits(path, last, before); 20 | 21 | ctx.body = commits; 22 | }); 23 | 24 | const app = new Koa(); 25 | app.use(router.routes()); 26 | app.use(serve(sitePath)); 27 | app.on("error", err => { 28 | console.error("Server error", err); 29 | console.error( 30 | "Let us know of the error at https://github.com/pomber/git-history/issues" 31 | ); 32 | }); 33 | 34 | module.exports = async function runServer(path) { 35 | const port = await getPort({ port: argv.port }); 36 | app.listen(port); 37 | console.log("Running at http://localhost:" + port); 38 | open(`http://localhost:${port}/?path=${encodeURIComponent(path)}`); 39 | }; 40 | -------------------------------------------------------------------------------- /cli/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@^1.3.5: 6 | version "1.3.5" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 8 | dependencies: 9 | mime-types "~2.1.18" 10 | negotiator "0.6.1" 11 | 12 | ansi-regex@^2.0.0: 13 | version "2.1.1" 14 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 15 | 16 | ansi-regex@^3.0.0: 17 | version "3.0.0" 18 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 19 | 20 | ansi-regex@^4.1.0: 21 | version "4.1.0" 22 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" 23 | 24 | any-promise@^1.0.0, any-promise@^1.1.0: 25 | version "1.3.0" 26 | resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" 27 | 28 | balanced-match@^1.0.0: 29 | version "1.0.0" 30 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 31 | 32 | brace-expansion@^1.1.7: 33 | version "1.1.11" 34 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 35 | dependencies: 36 | balanced-match "^1.0.0" 37 | concat-map "0.0.1" 38 | 39 | bytes@3.0.0: 40 | version "3.0.0" 41 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 42 | 43 | cache-content-type@^1.0.0: 44 | version "1.0.1" 45 | resolved "https://registry.yarnpkg.com/cache-content-type/-/cache-content-type-1.0.1.tgz#035cde2b08ee2129f4a8315ea8f00a00dba1453c" 46 | dependencies: 47 | mime-types "^2.1.18" 48 | ylru "^1.2.0" 49 | 50 | camelcase@^5.0.0: 51 | version "5.2.0" 52 | resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.2.0.tgz#e7522abda5ed94cc0489e1b8466610e88404cf45" 53 | 54 | cliui@^4.0.0: 55 | version "4.1.0" 56 | resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" 57 | dependencies: 58 | string-width "^2.1.1" 59 | strip-ansi "^4.0.0" 60 | wrap-ansi "^2.0.0" 61 | 62 | co@^4.6.0: 63 | version "4.6.0" 64 | resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" 65 | 66 | code-point-at@^1.0.0: 67 | version "1.1.0" 68 | resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" 69 | 70 | concat-map@0.0.1: 71 | version "0.0.1" 72 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 73 | 74 | content-disposition@0.5.2: 75 | version "0.5.2" 76 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 77 | 78 | content-disposition@~0.5.2: 79 | version "0.5.3" 80 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.3.tgz#e130caf7e7279087c5616c2007d0485698984fbd" 81 | dependencies: 82 | safe-buffer "5.1.2" 83 | 84 | content-type@^1.0.4: 85 | version "1.0.4" 86 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 87 | 88 | cookies@~0.7.1: 89 | version "0.7.3" 90 | resolved "https://registry.yarnpkg.com/cookies/-/cookies-0.7.3.tgz#7912ce21fbf2e8c2da70cf1c3f351aecf59dadfa" 91 | dependencies: 92 | depd "~1.1.2" 93 | keygrip "~1.0.3" 94 | 95 | cross-env@^5.2.0: 96 | version "5.2.0" 97 | resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.2.0.tgz#6ecd4c015d5773e614039ee529076669b9d126f2" 98 | dependencies: 99 | cross-spawn "^6.0.5" 100 | is-windows "^1.0.0" 101 | 102 | cross-spawn@^6.0.0, cross-spawn@^6.0.5: 103 | version "6.0.5" 104 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" 105 | dependencies: 106 | nice-try "^1.0.4" 107 | path-key "^2.0.1" 108 | semver "^5.5.0" 109 | shebang-command "^1.2.0" 110 | which "^1.2.9" 111 | 112 | debug@^3.1.0: 113 | version "3.2.6" 114 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" 115 | dependencies: 116 | ms "^2.1.1" 117 | 118 | debug@~3.1.0: 119 | version "3.1.0" 120 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 121 | dependencies: 122 | ms "2.0.0" 123 | 124 | decamelize@^1.2.0: 125 | version "1.2.0" 126 | resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" 127 | 128 | deep-equal@~1.0.1: 129 | version "1.0.1" 130 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" 131 | 132 | delegates@^1.0.0: 133 | version "1.0.0" 134 | resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" 135 | 136 | depd@^1.1.2, depd@~1.1.2: 137 | version "1.1.2" 138 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 139 | 140 | destroy@^1.0.4: 141 | version "1.0.4" 142 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 143 | 144 | ee-first@1.1.1: 145 | version "1.1.1" 146 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 147 | 148 | emoji-regex@^7.0.1: 149 | version "7.0.3" 150 | resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" 151 | 152 | end-of-stream@^1.1.0: 153 | version "1.4.1" 154 | resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.1.tgz#ed29634d19baba463b6ce6b80a37213eab71ec43" 155 | dependencies: 156 | once "^1.4.0" 157 | 158 | error-inject@^1.0.0: 159 | version "1.0.0" 160 | resolved "https://registry.yarnpkg.com/error-inject/-/error-inject-1.0.0.tgz#e2b3d91b54aed672f309d950d154850fa11d4f37" 161 | 162 | escape-html@^1.0.3: 163 | version "1.0.3" 164 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 165 | 166 | execa@^1.0.0: 167 | version "1.0.0" 168 | resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" 169 | dependencies: 170 | cross-spawn "^6.0.0" 171 | get-stream "^4.0.0" 172 | is-stream "^1.1.0" 173 | npm-run-path "^2.0.0" 174 | p-finally "^1.0.0" 175 | signal-exit "^3.0.0" 176 | strip-eof "^1.0.0" 177 | 178 | fast-url-parser@1.1.3: 179 | version "1.1.3" 180 | resolved "https://registry.yarnpkg.com/fast-url-parser/-/fast-url-parser-1.1.3.tgz#f4af3ea9f34d8a271cf58ad2b3759f431f0b318d" 181 | dependencies: 182 | punycode "^1.3.2" 183 | 184 | find-up@^3.0.0: 185 | version "3.0.0" 186 | resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" 187 | dependencies: 188 | locate-path "^3.0.0" 189 | 190 | fresh@~0.5.2: 191 | version "0.5.2" 192 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 193 | 194 | get-caller-file@^2.0.1: 195 | version "2.0.5" 196 | resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" 197 | 198 | get-port@^4.1.0: 199 | version "4.1.0" 200 | resolved "https://registry.yarnpkg.com/get-port/-/get-port-4.1.0.tgz#93eb3d5552c197497d76e9c389a6ac9920e20192" 201 | 202 | get-stream@^4.0.0: 203 | version "4.1.0" 204 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-4.1.0.tgz#c1b255575f3dc21d59bfc79cd3d2b46b1c3a54b5" 205 | dependencies: 206 | pump "^3.0.0" 207 | 208 | http-assert@^1.3.0: 209 | version "1.4.0" 210 | resolved "https://registry.yarnpkg.com/http-assert/-/http-assert-1.4.0.tgz#0e550b4fca6adf121bbeed83248c17e62f593a9a" 211 | dependencies: 212 | deep-equal "~1.0.1" 213 | http-errors "~1.7.1" 214 | 215 | http-errors@^1.3.1, http-errors@^1.6.3, http-errors@~1.7.1: 216 | version "1.7.1" 217 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.7.1.tgz#6a4ffe5d35188e1c39f872534690585852e1f027" 218 | dependencies: 219 | depd "~1.1.2" 220 | inherits "2.0.3" 221 | setprototypeof "1.1.0" 222 | statuses ">= 1.5.0 < 2" 223 | toidentifier "1.0.0" 224 | 225 | http-errors@~1.6.2: 226 | version "1.6.3" 227 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 228 | dependencies: 229 | depd "~1.1.2" 230 | inherits "2.0.3" 231 | setprototypeof "1.1.0" 232 | statuses ">= 1.4.0 < 2" 233 | 234 | inherits@2.0.3: 235 | version "2.0.3" 236 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 237 | 238 | invert-kv@^2.0.0: 239 | version "2.0.0" 240 | resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" 241 | 242 | is-fullwidth-code-point@^1.0.0: 243 | version "1.0.0" 244 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" 245 | dependencies: 246 | number-is-nan "^1.0.0" 247 | 248 | is-fullwidth-code-point@^2.0.0: 249 | version "2.0.0" 250 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 251 | 252 | is-generator-function@^1.0.7: 253 | version "1.0.7" 254 | resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" 255 | 256 | is-stream@^1.1.0: 257 | version "1.1.0" 258 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 259 | 260 | is-windows@^1.0.0: 261 | version "1.0.2" 262 | resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" 263 | 264 | isarray@0.0.1: 265 | version "0.0.1" 266 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 267 | 268 | isexe@^2.0.0: 269 | version "2.0.0" 270 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 271 | 272 | keygrip@~1.0.3: 273 | version "1.0.3" 274 | resolved "https://registry.yarnpkg.com/keygrip/-/keygrip-1.0.3.tgz#399d709f0aed2bab0a059e0cdd3a5023a053e1dc" 275 | 276 | koa-compose@^3.0.0: 277 | version "3.2.1" 278 | resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-3.2.1.tgz#a85ccb40b7d986d8e5a345b3a1ace8eabcf54de7" 279 | dependencies: 280 | any-promise "^1.1.0" 281 | 282 | koa-compose@^4.1.0: 283 | version "4.1.0" 284 | resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877" 285 | 286 | koa-convert@^1.2.0: 287 | version "1.2.0" 288 | resolved "https://registry.yarnpkg.com/koa-convert/-/koa-convert-1.2.0.tgz#da40875df49de0539098d1700b50820cebcd21d0" 289 | dependencies: 290 | co "^4.6.0" 291 | koa-compose "^3.0.0" 292 | 293 | koa-is-json@^1.0.0: 294 | version "1.0.0" 295 | resolved "https://registry.yarnpkg.com/koa-is-json/-/koa-is-json-1.0.0.tgz#273c07edcdcb8df6a2c1ab7d59ee76491451ec14" 296 | 297 | koa-router@^7.4.0: 298 | version "7.4.0" 299 | resolved "https://registry.yarnpkg.com/koa-router/-/koa-router-7.4.0.tgz#aee1f7adc02d5cb31d7d67465c9eacc825e8c5e0" 300 | dependencies: 301 | debug "^3.1.0" 302 | http-errors "^1.3.1" 303 | koa-compose "^3.0.0" 304 | methods "^1.0.1" 305 | path-to-regexp "^1.1.1" 306 | urijs "^1.19.0" 307 | 308 | koa-send@^5.0.0: 309 | version "5.0.0" 310 | resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.0.tgz#5e8441e07ef55737734d7ced25b842e50646e7eb" 311 | dependencies: 312 | debug "^3.1.0" 313 | http-errors "^1.6.3" 314 | mz "^2.7.0" 315 | resolve-path "^1.4.0" 316 | 317 | koa-static@^5.0.0: 318 | version "5.0.0" 319 | resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943" 320 | dependencies: 321 | debug "^3.1.0" 322 | koa-send "^5.0.0" 323 | 324 | koa@^2.7.0: 325 | version "2.7.0" 326 | resolved "https://registry.yarnpkg.com/koa/-/koa-2.7.0.tgz#7e00843506942b9d82c6cc33749f657c6e5e7adf" 327 | dependencies: 328 | accepts "^1.3.5" 329 | cache-content-type "^1.0.0" 330 | content-disposition "~0.5.2" 331 | content-type "^1.0.4" 332 | cookies "~0.7.1" 333 | debug "~3.1.0" 334 | delegates "^1.0.0" 335 | depd "^1.1.2" 336 | destroy "^1.0.4" 337 | error-inject "^1.0.0" 338 | escape-html "^1.0.3" 339 | fresh "~0.5.2" 340 | http-assert "^1.3.0" 341 | http-errors "^1.6.3" 342 | is-generator-function "^1.0.7" 343 | koa-compose "^4.1.0" 344 | koa-convert "^1.2.0" 345 | koa-is-json "^1.0.0" 346 | on-finished "^2.3.0" 347 | only "~0.0.2" 348 | parseurl "^1.3.2" 349 | statuses "^1.5.0" 350 | type-is "^1.6.16" 351 | vary "^1.1.2" 352 | 353 | lcid@^2.0.0: 354 | version "2.0.0" 355 | resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" 356 | dependencies: 357 | invert-kv "^2.0.0" 358 | 359 | locate-path@^3.0.0: 360 | version "3.0.0" 361 | resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" 362 | dependencies: 363 | p-locate "^3.0.0" 364 | path-exists "^3.0.0" 365 | 366 | map-age-cleaner@^0.1.1: 367 | version "0.1.3" 368 | resolved "https://registry.yarnpkg.com/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz#7d583a7306434c055fe474b0f45078e6e1b4b92a" 369 | dependencies: 370 | p-defer "^1.0.0" 371 | 372 | media-typer@0.3.0: 373 | version "0.3.0" 374 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 375 | 376 | mem@^4.0.0: 377 | version "4.2.0" 378 | resolved "https://registry.yarnpkg.com/mem/-/mem-4.2.0.tgz#5ee057680ed9cb8dad8a78d820f9a8897a102025" 379 | dependencies: 380 | map-age-cleaner "^0.1.1" 381 | mimic-fn "^2.0.0" 382 | p-is-promise "^2.0.0" 383 | 384 | methods@^1.0.1: 385 | version "1.1.2" 386 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 387 | 388 | mime-db@~1.33.0: 389 | version "1.33.0" 390 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 391 | 392 | mime-db@~1.38.0: 393 | version "1.38.0" 394 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.38.0.tgz#1a2aab16da9eb167b49c6e4df2d9c68d63d8e2ad" 395 | 396 | mime-types@2.1.18: 397 | version "2.1.18" 398 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" 399 | dependencies: 400 | mime-db "~1.33.0" 401 | 402 | mime-types@^2.1.18, mime-types@~2.1.18: 403 | version "2.1.22" 404 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.22.tgz#fe6b355a190926ab7698c9a0556a11199b2199bd" 405 | dependencies: 406 | mime-db "~1.38.0" 407 | 408 | mimic-fn@^2.0.0: 409 | version "2.0.0" 410 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.0.0.tgz#0913ff0b121db44ef5848242c38bbb35d44cabde" 411 | 412 | minimatch@3.0.4: 413 | version "3.0.4" 414 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 415 | dependencies: 416 | brace-expansion "^1.1.7" 417 | 418 | ms@2.0.0: 419 | version "2.0.0" 420 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 421 | 422 | ms@^2.1.1: 423 | version "2.1.1" 424 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 425 | 426 | mz@^2.7.0: 427 | version "2.7.0" 428 | resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" 429 | dependencies: 430 | any-promise "^1.0.0" 431 | object-assign "^4.0.1" 432 | thenify-all "^1.0.0" 433 | 434 | negotiator@0.6.1: 435 | version "0.6.1" 436 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 437 | 438 | nice-try@^1.0.4: 439 | version "1.0.5" 440 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" 441 | 442 | npm-run-path@^2.0.0: 443 | version "2.0.2" 444 | resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" 445 | dependencies: 446 | path-key "^2.0.0" 447 | 448 | number-is-nan@^1.0.0: 449 | version "1.0.1" 450 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 451 | 452 | object-assign@^4.0.1: 453 | version "4.1.1" 454 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 455 | 456 | on-finished@^2.3.0: 457 | version "2.3.0" 458 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 459 | dependencies: 460 | ee-first "1.1.1" 461 | 462 | once@^1.3.1, once@^1.4.0: 463 | version "1.4.0" 464 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 465 | dependencies: 466 | wrappy "1" 467 | 468 | only@~0.0.2: 469 | version "0.0.2" 470 | resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4" 471 | 472 | open@^0.0.5: 473 | version "0.0.5" 474 | resolved "https://registry.yarnpkg.com/open/-/open-0.0.5.tgz#42c3e18ec95466b6bf0dc42f3a2945c3f0cad8fc" 475 | 476 | opencollective-postinstall@^2.0.2: 477 | version "2.0.2" 478 | resolved "https://registry.yarnpkg.com/opencollective-postinstall/-/opencollective-postinstall-2.0.2.tgz#5657f1bede69b6e33a45939b061eb53d3c6c3a89" 479 | 480 | os-locale@^3.1.0: 481 | version "3.1.0" 482 | resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" 483 | dependencies: 484 | execa "^1.0.0" 485 | lcid "^2.0.0" 486 | mem "^4.0.0" 487 | 488 | p-defer@^1.0.0: 489 | version "1.0.0" 490 | resolved "https://registry.yarnpkg.com/p-defer/-/p-defer-1.0.0.tgz#9f6eb182f6c9aa8cd743004a7d4f96b196b0fb0c" 491 | 492 | p-finally@^1.0.0: 493 | version "1.0.0" 494 | resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" 495 | 496 | p-is-promise@^2.0.0: 497 | version "2.0.0" 498 | resolved "https://registry.yarnpkg.com/p-is-promise/-/p-is-promise-2.0.0.tgz#7554e3d572109a87e1f3f53f6a7d85d1b194f4c5" 499 | 500 | p-limit@^2.0.0: 501 | version "2.2.0" 502 | resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.0.tgz#417c9941e6027a9abcba5092dd2904e255b5fbc2" 503 | dependencies: 504 | p-try "^2.0.0" 505 | 506 | p-locate@^3.0.0: 507 | version "3.0.0" 508 | resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" 509 | dependencies: 510 | p-limit "^2.0.0" 511 | 512 | p-try@^2.0.0: 513 | version "2.1.0" 514 | resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.1.0.tgz#c1a0f1030e97de018bb2c718929d2af59463e505" 515 | 516 | parseurl@^1.3.2: 517 | version "1.3.2" 518 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 519 | 520 | path-exists@^3.0.0: 521 | version "3.0.0" 522 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 523 | 524 | path-is-absolute@1.0.1: 525 | version "1.0.1" 526 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 527 | 528 | path-is-inside@1.0.2: 529 | version "1.0.2" 530 | resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" 531 | 532 | path-key@^2.0.0, path-key@^2.0.1: 533 | version "2.0.1" 534 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 535 | 536 | path-to-regexp@2.2.1: 537 | version "2.2.1" 538 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-2.2.1.tgz#90b617025a16381a879bc82a38d4e8bdeb2bcf45" 539 | 540 | path-to-regexp@^1.1.1: 541 | version "1.7.0" 542 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" 543 | dependencies: 544 | isarray "0.0.1" 545 | 546 | pump@^3.0.0: 547 | version "3.0.0" 548 | resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" 549 | dependencies: 550 | end-of-stream "^1.1.0" 551 | once "^1.3.1" 552 | 553 | punycode@^1.3.2: 554 | version "1.4.1" 555 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" 556 | 557 | range-parser@1.2.0: 558 | version "1.2.0" 559 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 560 | 561 | require-directory@^2.1.1: 562 | version "2.1.1" 563 | resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" 564 | 565 | require-main-filename@^2.0.0: 566 | version "2.0.0" 567 | resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" 568 | 569 | resolve-path@^1.4.0: 570 | version "1.4.0" 571 | resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7" 572 | dependencies: 573 | http-errors "~1.6.2" 574 | path-is-absolute "1.0.1" 575 | 576 | safe-buffer@5.1.2: 577 | version "5.1.2" 578 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" 579 | 580 | semver@^5.5.0: 581 | version "5.6.0" 582 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" 583 | 584 | serve-handler@^5.0.8: 585 | version "5.0.8" 586 | resolved "https://registry.yarnpkg.com/serve-handler/-/serve-handler-5.0.8.tgz#790dbe340dabf1d61bdbaa02ea37dcab372377a8" 587 | dependencies: 588 | bytes "3.0.0" 589 | content-disposition "0.5.2" 590 | fast-url-parser "1.1.3" 591 | mime-types "2.1.18" 592 | minimatch "3.0.4" 593 | path-is-inside "1.0.2" 594 | path-to-regexp "2.2.1" 595 | range-parser "1.2.0" 596 | 597 | set-blocking@^2.0.0: 598 | version "2.0.0" 599 | resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" 600 | 601 | setprototypeof@1.1.0: 602 | version "1.1.0" 603 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 604 | 605 | shebang-command@^1.2.0: 606 | version "1.2.0" 607 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 608 | dependencies: 609 | shebang-regex "^1.0.0" 610 | 611 | shebang-regex@^1.0.0: 612 | version "1.0.0" 613 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 614 | 615 | signal-exit@^3.0.0: 616 | version "3.0.2" 617 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 618 | 619 | "statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0: 620 | version "1.5.0" 621 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 622 | 623 | string-width@^1.0.1: 624 | version "1.0.2" 625 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" 626 | dependencies: 627 | code-point-at "^1.0.0" 628 | is-fullwidth-code-point "^1.0.0" 629 | strip-ansi "^3.0.0" 630 | 631 | string-width@^2.1.1: 632 | version "2.1.1" 633 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 634 | dependencies: 635 | is-fullwidth-code-point "^2.0.0" 636 | strip-ansi "^4.0.0" 637 | 638 | string-width@^3.0.0: 639 | version "3.1.0" 640 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" 641 | dependencies: 642 | emoji-regex "^7.0.1" 643 | is-fullwidth-code-point "^2.0.0" 644 | strip-ansi "^5.1.0" 645 | 646 | strip-ansi@^3.0.0, strip-ansi@^3.0.1: 647 | version "3.0.1" 648 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 649 | dependencies: 650 | ansi-regex "^2.0.0" 651 | 652 | strip-ansi@^4.0.0: 653 | version "4.0.0" 654 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 655 | dependencies: 656 | ansi-regex "^3.0.0" 657 | 658 | strip-ansi@^5.1.0: 659 | version "5.2.0" 660 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" 661 | dependencies: 662 | ansi-regex "^4.1.0" 663 | 664 | strip-eof@^1.0.0: 665 | version "1.0.0" 666 | resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" 667 | 668 | thenify-all@^1.0.0: 669 | version "1.6.0" 670 | resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" 671 | dependencies: 672 | thenify ">= 3.1.0 < 4" 673 | 674 | "thenify@>= 3.1.0 < 4": 675 | version "3.3.0" 676 | resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" 677 | dependencies: 678 | any-promise "^1.0.0" 679 | 680 | toidentifier@1.0.0: 681 | version "1.0.0" 682 | resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" 683 | 684 | type-is@^1.6.16: 685 | version "1.6.16" 686 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 687 | dependencies: 688 | media-typer "0.3.0" 689 | mime-types "~2.1.18" 690 | 691 | urijs@^1.19.0: 692 | version "1.19.1" 693 | resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.1.tgz#5b0ff530c0cbde8386f6342235ba5ca6e995d25a" 694 | 695 | vary@^1.1.2: 696 | version "1.1.2" 697 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 698 | 699 | which-module@^2.0.0: 700 | version "2.0.0" 701 | resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" 702 | 703 | which@^1.2.9: 704 | version "1.3.1" 705 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 706 | dependencies: 707 | isexe "^2.0.0" 708 | 709 | wrap-ansi@^2.0.0: 710 | version "2.1.0" 711 | resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" 712 | dependencies: 713 | string-width "^1.0.1" 714 | strip-ansi "^3.0.1" 715 | 716 | wrappy@1: 717 | version "1.0.2" 718 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 719 | 720 | y18n@^4.0.0: 721 | version "4.0.0" 722 | resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" 723 | 724 | yargs-parser@^13.0.0: 725 | version "13.0.0" 726 | resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.0.0.tgz#3fc44f3e76a8bdb1cc3602e860108602e5ccde8b" 727 | dependencies: 728 | camelcase "^5.0.0" 729 | decamelize "^1.2.0" 730 | 731 | yargs@^13.2.2: 732 | version "13.2.2" 733 | resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.2.tgz#0c101f580ae95cea7f39d927e7770e3fdc97f993" 734 | dependencies: 735 | cliui "^4.0.0" 736 | find-up "^3.0.0" 737 | get-caller-file "^2.0.1" 738 | os-locale "^3.1.0" 739 | require-directory "^2.1.1" 740 | require-main-filename "^2.0.0" 741 | set-blocking "^2.0.0" 742 | string-width "^3.0.0" 743 | which-module "^2.0.0" 744 | y18n "^4.0.0" 745 | yargs-parser "^13.0.0" 746 | 747 | ylru@^1.2.0: 748 | version "1.2.1" 749 | resolved "https://registry.yarnpkg.com/ylru/-/ylru-1.2.1.tgz#f576b63341547989c1de7ba288760923b27fe84f" 750 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | webpack: { 3 | configure: { 4 | output: { 5 | // I need "this" for workerize-loader 6 | globalObject: "this" 7 | } 8 | } 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Rodrigo Pombo 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. -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [[redirects]] 2 | from = "/*" 3 | to = "/index.html" 4 | status = 200 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "githistory-web", 3 | "version": "1.0.1", 4 | "repository": "pomber/git-history", 5 | "private": true, 6 | "dependencies": { 7 | "@craco/craco": "^3.5.0", 8 | "diff": "^4.0.1", 9 | "js-base64": "^2.5.1", 10 | "netlify-auth-providers": "^1.0.0-alpha5", 11 | "opencollective-postinstall": "^2.0.2", 12 | "prismjs": "^1.15.0", 13 | "react": "^16.8.4", 14 | "react-dom": "^16.8.4", 15 | "react-scripts": "2.1.3", 16 | "react-swipeable": "^4.3.2", 17 | "react-use": "^5.2.2", 18 | "rebound": "^0.1.0", 19 | "workerize-loader": "^1.0.4" 20 | }, 21 | "scripts": { 22 | "start": "craco --openssl-legacy-provider start", 23 | "build": "craco build", 24 | "format": "prettier --write \"**/*.{js,jsx,md,json,html,css,yml}\" --ignore-path .gitignore", 25 | "test-prettier": "prettier --check \"**/*.{js,jsx,md,json,html,css,yml}\" --ignore-path .gitignore", 26 | "test-cra": "react-scripts test", 27 | "test": "run-s test-prettier test-cra", 28 | "eject": "react-scripts eject", 29 | "postinstall": "opencollective-postinstall", 30 | "storybook": "start-storybook -p 9009 -s public", 31 | "build-storybook": "build-storybook -s public" 32 | }, 33 | "eslintConfig": { 34 | "extends": "react-app" 35 | }, 36 | "browserslist": [ 37 | ">0.2%", 38 | "not dead", 39 | "not ie <= 11", 40 | "not op_mini all" 41 | ], 42 | "devDependencies": { 43 | "@babel/core": "^7.3.4", 44 | "@storybook/addon-actions": "^4.1.13", 45 | "@storybook/addon-links": "^4.1.13", 46 | "@storybook/addons": "^4.1.13", 47 | "@storybook/react": "^4.1.13", 48 | "babel-loader": "8.0.4", 49 | "npm-run-all": "^4.1.5", 50 | "prettier": "^1.16.4" 51 | }, 52 | "collective": { 53 | "type": "opencollective", 54 | "url": "https://opencollective.com/git-history" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 16 | 17 | 21 | 30 | Git History 31 | 64 | 65 | 66 |
67 | 68 | 69 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Git History", 3 | "name": "Git History", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#d6deeb", 14 | "background_color": "#011627" 15 | } 16 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | demo 4 | 5 |
6 | 7 | # [Git History](https://githistory.xyz) 8 | 9 | Quickly browse the history of files in any git repo: 10 | 11 | 1. Go to a file in **GitHub** (or **GitLab**, or **Bitbucket**) 12 | 1. Replace `github.com` with `github.githistory.xyz` 13 | 1. There's no step three 14 | 15 | [Try it](https://github.githistory.xyz/babel/babel/blob/master/packages/babel-core/test/browserify.js) 16 | 17 | > If you like this project consider [backing my open source work on Patreon!](https://patreon.com/pomber) 18 | > And follow [@pomber](https://twitter.com/pomber) on twitter for updates. 19 | 20 | ## Extensions 21 | 22 | ### Browsers 23 | 24 | You can also add an `Open in Git History` button to GitHub, GitLab and Bitbucket with the [Chrome](https://chrome.google.com/webstore/detail/github-history-browser-ex/laghnmifffncfonaoffcndocllegejnf) and [Firefox](https://addons.mozilla.org/firefox/addon/github-history/) extensions. 25 | 26 |
Or you can use a bookmarklet. 27 | 28 | ```javascript 29 | javascript: (function() { 30 | var url = window.location.href; 31 | var regEx = /^(https?\:\/\/)(www\.)?(github|gitlab|bitbucket)\.(com|org)\/(.*)$/i; 32 | if (regEx.test(url)) { 33 | url = url.replace(regEx, "$1$3.githistory.xyz/$5"); 34 | window.open(url, "_blank"); 35 | } else { 36 | alert("Not a Git File URL"); 37 | } 38 | })(); 39 | ``` 40 | 41 |
42 | 43 | ### Local Repos 44 | 45 | You can use Git History for local git repos with the [CLI](https://github.com/pomber/git-history/tree/master/cli) or with the [VS Code extension](https://marketplace.visualstudio.com/items?itemName=pomber.git-file-history). 46 | 47 | ## Support Git History 48 | 49 | ### Sponsors 50 | 51 | Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/git-history#sponsor)] 52 | 53 | selefra 54 | 55 | 56 | 57 | ### Backers 58 | 59 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)] 60 | 61 | 62 | 63 | ### Thanks 64 | 65 |

Browser testing via 66 | 67 | LambdaTest 68 |

69 | 70 | ### Credits 71 | 72 | Based on these amazing projects: 73 | 74 | - [Prism](https://github.com/PrismJS/prism) by [Lea Verou](https://twitter.com/leaverou) 75 | - [jsdiff](https://github.com/kpdecker/jsdiff) by [Kevin Decker](https://twitter.com/kpdecker) 76 | - [Night Owl](https://github.com/sdras/night-owl-vscode-theme) by [Sarah Drasner](https://twitter.com/sarah_edo) 77 | 78 | ## License 79 | 80 | MIT 81 | -------------------------------------------------------------------------------- /src/airframe/airframe.js: -------------------------------------------------------------------------------- 1 | import easing from "./easing"; 2 | const MULTIPLY = "multiply"; 3 | 4 | /* eslint-disable */ 5 | function mergeResults(results, composite) { 6 | const firstResult = results[0]; 7 | if (results.length < 2) { 8 | return firstResult; 9 | } 10 | if (Array.isArray(firstResult)) { 11 | return firstResult.map((_, i) => { 12 | return mergeResults(results.map(result => result[i]), composite); 13 | }); 14 | } else { 15 | const merged = Object.assign({}, ...results); 16 | 17 | if (composite === MULTIPLY) { 18 | const opacities = results.map(x => x.opacity).filter(x => x != null); 19 | if (opacities.length !== 0) { 20 | merged.opacity = opacities.reduce((a, b) => a * b); 21 | } 22 | } 23 | return merged; 24 | } 25 | } 26 | 27 | const airframe = { 28 | parallel: ({ children: fns }) => { 29 | return (t, ...args) => { 30 | const styles = fns.map(fn => fn(t, ...args)); 31 | const result = mergeResults(styles, MULTIPLY); 32 | return result; 33 | }; 34 | }, 35 | chain: ({ children: fns, durations }) => { 36 | return (t, ...args) => { 37 | let style = fns[0](0, ...args); 38 | let lowerDuration = 0; 39 | for (let i = 0; i < fns.length; i++) { 40 | const fn = fns[i]; 41 | const thisDuration = durations[i]; 42 | const upperDuration = lowerDuration + thisDuration; 43 | if (lowerDuration <= t && t <= upperDuration) { 44 | const innerT = (t - lowerDuration) / thisDuration; 45 | style = mergeResults([style, fn(innerT, ...args)]); 46 | } else if (upperDuration < t) { 47 | // merge the end of previous animation 48 | style = mergeResults([style, fn(1, ...args)]); 49 | } else if (t < lowerDuration) { 50 | // merge the start of future animation 51 | style = mergeResults([fn(0, ...args), style]); 52 | } 53 | lowerDuration = upperDuration; 54 | } 55 | return style; 56 | }; 57 | }, 58 | delay: () => () => ({}), 59 | tween: ({ from, to, ease = easing.linear }) => (t, targets) => { 60 | const style = {}; 61 | Object.keys(from).forEach(key => { 62 | const value = from[key] + (to[key] - from[key]) * ease(t); 63 | if (key === "x") { 64 | style["transform"] = `translateX(${value}px)`; 65 | } else { 66 | style[key] = value; 67 | } 68 | }); 69 | return style; 70 | } 71 | }; 72 | 73 | /* @jsx createAnimation */ 74 | export const Stagger = props => (t, targets) => { 75 | const filter = target => !props.filter || props.filter(target); 76 | const interval = 77 | targets.filter(filter).length < 2 78 | ? 0 79 | : props.interval / (targets.filter(filter).length - 1); 80 | let i = 0; 81 | return targets.map(target => { 82 | // console.log(target, props.filter(target)); 83 | if (!filter(target)) { 84 | return {}; 85 | } 86 | const animation = ( 87 | 88 | 89 | 90 | {props.children[0]} 91 | 92 | 93 | ); 94 | i++; 95 | const result = animation(t, target); 96 | // console.log("Stagger Result", t, result); 97 | return result; 98 | }); 99 | }; 100 | 101 | export function createAnimation(type, props, ...children) { 102 | const allProps = Object.assign({ children }, props); 103 | if (typeof type === "string") { 104 | if (window.LOG === "verbose") { 105 | return (t, ...args) => { 106 | console.groupCollapsed(type, t); 107 | const result = airframe[type](allProps)(t, ...args); 108 | console.log(result); 109 | console.groupEnd(); 110 | return result; 111 | }; 112 | } else { 113 | return airframe[type](allProps); 114 | } 115 | } else { 116 | if (window.LOG === "verbose") { 117 | return (t, ...args) => { 118 | console.groupCollapsed(type.name, t); 119 | const result = type(allProps)(t, ...args); 120 | console.log(result); 121 | console.groupEnd(); 122 | return result; 123 | }; 124 | } else { 125 | return type(allProps); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/airframe/easing.js: -------------------------------------------------------------------------------- 1 | export default { 2 | // no easing, no acceleration 3 | linear: function(t) { 4 | return t; 5 | }, 6 | // accelerating from zero velocity 7 | easeInQuad: function(t) { 8 | return t * t; 9 | }, 10 | // decelerating to zero velocity 11 | easeOutQuad: function(t) { 12 | return t * (2 - t); 13 | }, 14 | // acceleration until halfway, then deceleration 15 | easeInOutQuad: function(t) { 16 | return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; 17 | }, 18 | // accelerating from zero velocity 19 | easeInCubic: function(t) { 20 | return t * t * t; 21 | }, 22 | // decelerating to zero velocity 23 | easeOutCubic: function(t) { 24 | return --t * t * t + 1; 25 | }, 26 | // acceleration until halfway, then deceleration 27 | easeInOutCubic: function(t) { 28 | return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; 29 | }, 30 | // accelerating from zero velocity 31 | easeInQuart: function(t) { 32 | return t * t * t * t; 33 | }, 34 | // decelerating to zero velocity 35 | easeOutQuart: function(t) { 36 | return 1 - --t * t * t * t; 37 | }, 38 | // acceleration until halfway, then deceleration 39 | easeInOutQuart: function(t) { 40 | return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; 41 | }, 42 | // accelerating from zero velocity 43 | easeInQuint: function(t) { 44 | return t * t * t * t * t; 45 | }, 46 | // decelerating to zero velocity 47 | easeOutQuint: function(t) { 48 | return 1 + --t * t * t * t * t; 49 | }, 50 | // acceleration until halfway, then deceleration 51 | easeInOutQuint: function(t) { 52 | return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/animation.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { createAnimation, Stagger } from "./airframe/airframe"; 3 | import easing from "./airframe/easing"; 4 | 5 | const dx = 250; 6 | const offOpacity = 0.6; 7 | 8 | /* @jsx createAnimation */ 9 | 10 | // window.LOG = "verbose"; 11 | 12 | const SlideToLeft = () => ( 13 | 18 | ); 19 | 20 | function ShrinkHeight() { 21 | return ( 22 | 27 | ); 28 | } 29 | 30 | const SlideFromRight = () => ( 31 | 36 | ); 37 | function GrowHeight() { 38 | return ( 39 | 44 | ); 45 | } 46 | 47 | function SwitchLines({ filterExit, filterEnter, filterFadeOut }) { 48 | return ( 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 72 | 73 | !filterEnter(l) && !filterFadeOut(l)}> 74 | 75 | 76 | 77 | ); 78 | } 79 | 80 | export default ( 81 | 82 | line.left && !line.middle} 84 | filterEnter={line => !line.left && line.middle} 85 | filterFadeOut={line => false} 86 | /> 87 | line.middle && !line.right} 89 | filterEnter={line => !line.middle && line.right} 90 | filterFadeOut={line => !line.left && line.middle} 91 | /> 92 | 93 | ); 94 | -------------------------------------------------------------------------------- /src/app-helpers.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | 3 | export function Center({ children }) { 4 | return ( 5 |
15 | {children} 16 |
17 | ); 18 | } 19 | 20 | export function Loading({ repo, path }) { 21 | return ( 22 |
23 |

24 | Loading {path} history {repo ? "from " + repo : ""}... 25 |

26 |
27 | ); 28 | } 29 | 30 | export function Error({ error, gitProvider }) { 31 | const { LogInButton } = gitProvider; 32 | if (error.status === 403) { 33 | // FIX bitbucket uses 403 for private repos 34 | return ( 35 |
36 |

37 | GitHub API rate limit exceeded for your IP (60 requests per hour). 38 |

39 |

Sign in with GitHub for more:

40 | 41 |
42 | ); 43 | } 44 | 45 | if (error.status === 404) { 46 | return ( 47 |
48 |

File not found.

49 | {gitProvider.isLoggedIn && !gitProvider.isLoggedIn() && ( 50 | 51 |

Is it from a private repo? Sign in:

52 | 53 |
54 | )} 55 |
56 | ); 57 | } 58 | 59 | console.error(error); 60 | console.error( 61 | "Let us know of the error at https://github.com/pomber/git-history/issues" 62 | ); 63 | return ( 64 |
65 |

Unexpected error. Check the console.

66 |
67 | ); 68 | } 69 | 70 | export function useDocumentTitle(title) { 71 | useEffect(() => { 72 | document.title = title; 73 | }, [title]); 74 | } 75 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from "react"; 2 | import History from "./history"; 3 | import Landing from "./landing"; 4 | import { useDocumentTitle, Loading, Error } from "./app-helpers"; 5 | import getGitProvider from "./git-providers/providers"; 6 | 7 | export default function App() { 8 | const gitProvider = getGitProvider(); 9 | 10 | if (gitProvider.showLanding()) { 11 | return ; 12 | } else { 13 | return ( 14 | 15 | 16 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | function InnerApp({ gitProvider }) { 28 | const path = gitProvider.getPath(); 29 | const fileName = path.split("/").pop(); 30 | 31 | useDocumentTitle(`Git History - ${fileName}`); 32 | 33 | const [versions, loading, error, loadMore] = useVersionsLoader( 34 | gitProvider, 35 | path 36 | ); 37 | 38 | if (error) { 39 | return ; 40 | } 41 | 42 | if (!versions && loading) { 43 | return ; 44 | } 45 | 46 | if (!versions.length) { 47 | return ; 48 | } 49 | 50 | return ; 51 | } 52 | 53 | function useVersionsLoader(gitProvider) { 54 | const [state, setState] = useState({ 55 | data: null, 56 | loading: true, 57 | error: null, 58 | last: 10, 59 | noMore: false 60 | }); 61 | 62 | const loadMore = () => { 63 | setState(old => { 64 | const shouldFetchMore = !old.loading && !old.noMore; 65 | return shouldFetchMore 66 | ? { ...old, last: old.last + 10, loading: true } 67 | : old; 68 | }); 69 | }; 70 | 71 | useEffect(() => { 72 | gitProvider 73 | .getVersions(state.last) 74 | .then(data => { 75 | setState(old => ({ 76 | data, 77 | loading: false, 78 | error: false, 79 | last: old.last, 80 | noMore: data.length < old.last 81 | })); 82 | }) 83 | .catch(error => { 84 | setState(old => ({ 85 | ...old, 86 | loading: false, 87 | error: error.message || error 88 | })); 89 | }); 90 | }, [state.last]); 91 | 92 | return [state.data, state.loading, state.error, loadMore]; 93 | } 94 | -------------------------------------------------------------------------------- /src/avatar.addy.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/src/avatar.addy.jpg -------------------------------------------------------------------------------- /src/avatar.css-tricks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/src/avatar.css-tricks.jpg -------------------------------------------------------------------------------- /src/avatar.github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/src/avatar.github.jpg -------------------------------------------------------------------------------- /src/avatar.smashing.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/src/avatar.smashing.jpg -------------------------------------------------------------------------------- /src/comment-box.css: -------------------------------------------------------------------------------- 1 | .comment-box { 2 | position: relative; 3 | border: 2px solid rgb(214, 222, 235, 0.5); 4 | color: rgb(214, 222, 235, 0.8); 5 | padding: 6px; 6 | min-width: 500px; 7 | white-space: nowrap; 8 | } 9 | .comment-box:after, 10 | .comment-box:before { 11 | bottom: 100%; 12 | left: 50%; 13 | border: solid transparent; 14 | content: " "; 15 | height: 0; 16 | width: 0; 17 | position: absolute; 18 | pointer-events: none; 19 | } 20 | 21 | .comment-box:after { 22 | border-color: rgba(1, 22, 39, 0); 23 | border-bottom-color: rgb(1, 22, 39); 24 | border-width: 13px; 25 | margin-left: -13px; 26 | } 27 | .comment-box:before { 28 | border-color: rgba(1, 22, 39, 0); 29 | border-bottom-color: rgb(214, 222, 235, 0.5); 30 | border-width: 14px; 31 | margin-left: -14px; 32 | margin-bottom: 2px; 33 | } 34 | -------------------------------------------------------------------------------- /src/demo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/src/demo.mp4 -------------------------------------------------------------------------------- /src/demo.webm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/src/demo.webm -------------------------------------------------------------------------------- /src/duotoneLight.js: -------------------------------------------------------------------------------- 1 | // Duotone Light 2 | // Author: Simurai, adapted from DuoTone themes for Atom (http://simurai.com/projects/2016/01/01/duotone-themes) 3 | // Conversion: Bram de Haan (http://atelierbram.github.io/Base2Tone-prism/output/prism/prism-base2tone-evening-dark.css) 4 | // Generated with Base16 Builder (https://github.com/base16-builder/base16-builder) 5 | 6 | var theme /*: PrismTheme */ = { 7 | plain: { 8 | backgroundColor: "#faf8f5", 9 | color: "#728fcb" 10 | }, 11 | styles: [ 12 | { 13 | types: ["comment", "prolog", "doctype", "cdata", "punctuation"], 14 | style: { 15 | color: "#b6ad9a" 16 | } 17 | }, 18 | { 19 | types: ["namespace"], 20 | style: { 21 | opacity: 0.7 22 | } 23 | }, 24 | { 25 | types: ["tag", "operator", "number"], 26 | style: { 27 | color: "#063289" 28 | } 29 | }, 30 | { 31 | types: ["property", "function"], 32 | style: { 33 | color: "#b29762" 34 | } 35 | }, 36 | { 37 | types: ["tag-id", "selector", "atrule-id"], 38 | style: { 39 | color: "#2d2006" 40 | } 41 | }, 42 | { 43 | types: ["attr-name"], 44 | style: { 45 | color: "#896724" 46 | } 47 | }, 48 | { 49 | types: [ 50 | "boolean", 51 | "string", 52 | "entity", 53 | "url", 54 | "attr-value", 55 | "keyword", 56 | "control", 57 | "directive", 58 | "unit", 59 | "statement", 60 | "regex", 61 | "at-rule" 62 | ], 63 | style: { 64 | color: "#728fcb" 65 | } 66 | }, 67 | { 68 | types: ["placeholder", "variable"], 69 | style: { 70 | color: "#93abdc" 71 | } 72 | }, 73 | { 74 | types: ["deleted"], 75 | style: { 76 | textDecorationLine: "line-through" 77 | } 78 | }, 79 | { 80 | types: ["inserted"], 81 | style: { 82 | textDecorationLine: "underline" 83 | } 84 | }, 85 | { 86 | types: ["italic"], 87 | style: { 88 | fontStyle: "italic" 89 | } 90 | }, 91 | { 92 | types: ["important", "bold"], 93 | style: { 94 | fontWeight: "bold" 95 | } 96 | }, 97 | { 98 | types: ["important"], 99 | style: { 100 | color: "#896724" 101 | } 102 | } 103 | ] 104 | }; 105 | 106 | module.exports = theme; 107 | -------------------------------------------------------------------------------- /src/git-providers/bitbucket-commit-fetcher.js: -------------------------------------------------------------------------------- 1 | const cache = {}; 2 | 3 | async function getCommits({ repo, sha, path, last, token }) { 4 | if (!cache[path]) { 5 | let fields = 6 | "values.path,values.commit.date,values.commit.message,values.commit.hash,values.commit.author.*,values.commit.links.html, values.commit.author.user.nickname, values.commit.author.user.links.avatar.href, values.commit.links.html.href"; 7 | // fields = "*.*.*.*.*"; 8 | const commitsResponse = await fetch( 9 | `https://api.bitbucket.org/2.0/repositories/${repo}/filehistory/${sha}/${path}?fields=${fields}`, 10 | { headers: token ? { Authorization: `bearer ${token}` } : {} } 11 | ); 12 | 13 | if (!commitsResponse.ok) { 14 | throw { 15 | status: commitsResponse.status === 403 ? 404 : commitsResponse.status, 16 | body: commitsJson 17 | }; 18 | } 19 | 20 | const commitsJson = await commitsResponse.json(); 21 | 22 | cache[path] = commitsJson.values.map(({ commit }) => ({ 23 | sha: commit.hash, 24 | date: new Date(commit.date), 25 | author: { 26 | login: commit.author.user 27 | ? commit.author.user.nickname 28 | : commit.author.raw, 29 | avatar: commit.author.user && commit.author.user.links.avatar.href 30 | }, 31 | commitUrl: commit.links.html.href, 32 | message: commit.message 33 | })); 34 | } 35 | 36 | const commits = cache[path].slice(0, last); 37 | 38 | await Promise.all( 39 | commits.map(async commit => { 40 | if (!commit.content) { 41 | const info = await getContent(repo, commit.sha, path, token); 42 | commit.content = info.content; 43 | } 44 | }) 45 | ); 46 | 47 | return commits; 48 | } 49 | 50 | async function getContent(repo, sha, path, token) { 51 | const contentResponse = await fetch( 52 | `https://api.bitbucket.org/2.0/repositories/${repo}/src/${sha}/${path}`, 53 | { headers: token ? { Authorization: `bearer ${token}` } : {} } 54 | ); 55 | 56 | if (contentResponse.status === 404) { 57 | return { content: "" }; 58 | } 59 | 60 | if (!contentResponse.ok) { 61 | throw { 62 | status: contentResponse.status, 63 | body: await contentResponse.json() 64 | }; 65 | } 66 | 67 | const content = await contentResponse.text(); 68 | 69 | return { content }; 70 | } 71 | 72 | export default { 73 | getCommits 74 | }; 75 | -------------------------------------------------------------------------------- /src/git-providers/bitbucket-provider.js: -------------------------------------------------------------------------------- 1 | import netlify from "netlify-auth-providers"; 2 | import React from "react"; 3 | 4 | import versioner from "./versioner"; 5 | import { SOURCE } from "./sources"; 6 | 7 | const TOKEN_KEY = "bitbucket-token"; 8 | 9 | function isLoggedIn() { 10 | return !!window.localStorage.getItem(TOKEN_KEY); 11 | } 12 | 13 | function getUrlParams() { 14 | const [, owner, reponame, , sha, ...paths] = window.location.pathname.split( 15 | "/" 16 | ); 17 | 18 | if (!sha) { 19 | return []; 20 | } 21 | 22 | return [owner + "/" + reponame, sha, paths.join("/")]; 23 | } 24 | 25 | function getPath() { 26 | const [, , path] = getUrlParams(); 27 | return path; 28 | } 29 | 30 | function showLanding() { 31 | const [repo, ,] = getUrlParams(); 32 | return !repo; 33 | } 34 | 35 | function logIn() { 36 | // return new Promise((resolve, reject) => { 37 | var authenticator = new netlify({ 38 | site_id: "ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6" 39 | }); 40 | authenticator.authenticate({ provider: "bitbucket" }, function(err, data) { 41 | if (err) { 42 | console.error(err); 43 | return; 44 | } 45 | window.localStorage.setItem(TOKEN_KEY, data.token); 46 | window.location.reload(false); 47 | }); 48 | // }); 49 | } 50 | 51 | function LogInButton() { 52 | return ( 53 | 59 | ); 60 | } 61 | 62 | function getParams() { 63 | const [repo, sha, path] = getUrlParams(); 64 | const token = window.localStorage.getItem(TOKEN_KEY); 65 | return { repo, sha, path, token }; 66 | } 67 | 68 | async function getVersions(last) { 69 | const params = { ...getParams(), last }; 70 | return await versioner.getVersions(SOURCE.BITBUCKET, params); 71 | } 72 | 73 | export default { 74 | showLanding, 75 | getPath, 76 | getVersions, 77 | logIn, 78 | isLoggedIn, 79 | LogInButton 80 | }; 81 | -------------------------------------------------------------------------------- /src/git-providers/cli-commit-fetcher.js: -------------------------------------------------------------------------------- 1 | async function getCommits({ path, last }) { 2 | // TODO cache 3 | const response = await fetch( 4 | `/api/commits?path=${encodeURIComponent(path)}&last=${last}` 5 | ); 6 | const commits = await response.json(); 7 | commits.forEach(c => (c.date = new Date(c.date))); 8 | 9 | return commits; 10 | } 11 | 12 | export default { 13 | getCommits 14 | }; 15 | -------------------------------------------------------------------------------- /src/git-providers/cli-provider.js: -------------------------------------------------------------------------------- 1 | import versioner from "./versioner"; 2 | import { SOURCE } from "./sources"; 3 | 4 | function getPath() { 5 | return new URLSearchParams(window.location.search).get("path"); 6 | } 7 | 8 | function showLanding() { 9 | return false; 10 | } 11 | 12 | async function getVersions(last) { 13 | const params = { path: getPath(), last }; 14 | return await versioner.getVersions(SOURCE.CLI, params); 15 | } 16 | 17 | export default { 18 | showLanding, 19 | getVersions, 20 | getPath 21 | }; 22 | -------------------------------------------------------------------------------- /src/git-providers/differ.js: -------------------------------------------------------------------------------- 1 | import { diffLines } from "diff"; 2 | import tokenize from "./tokenizer"; 3 | const newlineRe = /\r\n|\r|\n/; 4 | 5 | function myDiff(oldCode, newCode) { 6 | const changes = diffLines(oldCode || "", newCode); 7 | 8 | let oldIndex = -1; 9 | return changes.map(({ value, count, removed, added }) => { 10 | const lines = value.split(newlineRe); 11 | // check if last line is empty, if it is, remove it 12 | const lastLine = lines.pop(); 13 | if (lastLine) { 14 | lines.push(lastLine); 15 | } 16 | const result = { 17 | oldIndex, 18 | lines, 19 | count, 20 | removed, 21 | added 22 | }; 23 | if (!added) { 24 | oldIndex += count; 25 | } 26 | return result; 27 | }); 28 | } 29 | 30 | function insert(array, index, elements) { 31 | return array.splice(index, 0, ...elements); 32 | } 33 | 34 | function slideDiff(lines, codes, slideIndex, language) { 35 | const prevLines = lines.filter(l => l.slides.includes(slideIndex - 1)); 36 | const prevCode = codes[slideIndex - 1] || ""; 37 | const currCode = codes[slideIndex]; 38 | 39 | const changes = myDiff(prevCode, currCode); 40 | 41 | changes.forEach(change => { 42 | if (change.added) { 43 | const prevLine = prevLines[change.oldIndex]; 44 | const addAtIndex = lines.indexOf(prevLine) + 1; 45 | const addLines = change.lines.map(content => ({ 46 | content, 47 | slides: [slideIndex] 48 | })); 49 | insert(lines, addAtIndex, addLines); 50 | } else if (!change.removed) { 51 | for (let j = 1; j <= change.count; j++) { 52 | prevLines[change.oldIndex + j].slides.push(slideIndex); 53 | } 54 | } 55 | }); 56 | 57 | const tokenLines = tokenize(currCode, language); 58 | const currLines = lines.filter(l => l.slides.includes(slideIndex)); 59 | currLines.forEach((line, index) => (line.tokens = tokenLines[index])); 60 | } 61 | 62 | export function parseLines(codes, language) { 63 | const lines = []; 64 | for (let slideIndex = 0; slideIndex < codes.length; slideIndex++) { 65 | slideDiff(lines, codes, slideIndex, language); 66 | } 67 | return lines; 68 | } 69 | 70 | export function getSlides(codes, language) { 71 | // codes are in reverse cronological order 72 | const lines = parseLines(codes, language); 73 | // console.log("lines", lines); 74 | return codes.map((_, slideIndex) => { 75 | return lines 76 | .map((line, lineIndex) => ({ 77 | content: line.content, 78 | tokens: line.tokens, 79 | left: line.slides.includes(slideIndex + 1), 80 | middle: line.slides.includes(slideIndex), 81 | right: line.slides.includes(slideIndex - 1), 82 | key: lineIndex 83 | })) 84 | .filter(line => line.middle || line.left || line.right); 85 | }); 86 | } 87 | 88 | export function getChanges(lines) { 89 | const changes = []; 90 | let currentChange = null; 91 | let i = 0; 92 | const isNewLine = i => !lines[i].left && lines[i].middle; 93 | while (i < lines.length) { 94 | if (isNewLine(i)) { 95 | if (!currentChange) { 96 | currentChange = { start: i }; 97 | } 98 | } else { 99 | if (currentChange) { 100 | currentChange.end = i - 1; 101 | changes.push(currentChange); 102 | currentChange = null; 103 | } 104 | } 105 | i++; 106 | } 107 | 108 | if (currentChange) { 109 | currentChange.end = i - 1; 110 | changes.push(currentChange); 111 | currentChange = null; 112 | } 113 | 114 | return changes; 115 | } 116 | -------------------------------------------------------------------------------- /src/git-providers/github-commit-fetcher.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from "js-base64"; 2 | 3 | const cache = {}; 4 | 5 | async function getCommits({ repo, sha, path, token, last }) { 6 | if (!cache[path]) { 7 | const commitsResponse = await fetch( 8 | `https://api.github.com/repos/${repo}/commits?sha=${sha}&path=${path}`, 9 | { headers: token ? { Authorization: `bearer ${token}` } : {} } 10 | ); 11 | 12 | if (!commitsResponse.ok) { 13 | throw { 14 | status: commitsResponse.status, 15 | body: commitsJson 16 | }; 17 | } 18 | 19 | const commitsJson = await commitsResponse.json(); 20 | 21 | cache[path] = commitsJson.map(commit => ({ 22 | sha: commit.sha, 23 | date: new Date(commit.commit.author.date), 24 | author: { 25 | login: commit.author ? commit.author.login : commit.commit.author.name, 26 | avatar: commit.author 27 | ? commit.author.avatar_url 28 | : "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" 29 | }, 30 | commitUrl: commit.html_url, 31 | message: commit.commit.message 32 | })); 33 | } 34 | 35 | const commits = cache[path].slice(0, last); 36 | 37 | await Promise.all( 38 | commits.map(async commit => { 39 | if (!commit.content) { 40 | const info = await getContent(repo, commit.sha, path, token); 41 | commit.content = info.content; 42 | commit.fileUrl = info.url; 43 | } 44 | }) 45 | ); 46 | 47 | return commits; 48 | } 49 | 50 | async function getContent(repo, sha, path, token) { 51 | const contentResponse = await fetch( 52 | `https://api.github.com/repos/${repo}/contents${path}?ref=${sha}`, 53 | { headers: token ? { Authorization: `bearer ${token}` } : {} } 54 | ); 55 | 56 | if (contentResponse.status === 404) { 57 | return { content: "" }; 58 | } 59 | 60 | const contentJson = await contentResponse.json(); 61 | 62 | if (!contentResponse.ok) { 63 | throw { 64 | status: contentResponse.status, 65 | body: contentJson 66 | }; 67 | } 68 | 69 | const content = Base64.decode(contentJson.content); 70 | return { content, url: contentJson.html_url }; 71 | } 72 | 73 | export default { 74 | getCommits 75 | }; 76 | -------------------------------------------------------------------------------- /src/git-providers/github-provider.js: -------------------------------------------------------------------------------- 1 | import netlify from "netlify-auth-providers"; 2 | import React from "react"; 3 | import versioner from "./versioner"; 4 | import { SOURCE } from "./sources"; 5 | 6 | const TOKEN_KEY = "github-token"; 7 | 8 | function isLoggedIn() { 9 | return !!window.localStorage.getItem(TOKEN_KEY); 10 | } 11 | 12 | function getUrlParams() { 13 | const [ 14 | , 15 | owner, 16 | reponame, 17 | action, 18 | sha, 19 | ...paths 20 | ] = window.location.pathname.split("/"); 21 | 22 | if (action !== "commits" && action !== "blob") { 23 | return []; 24 | } 25 | 26 | return [owner + "/" + reponame, sha, "/" + paths.join("/")]; 27 | } 28 | 29 | function getPath() { 30 | const [, , path] = getUrlParams(); 31 | return path; 32 | } 33 | 34 | function showLanding() { 35 | const [repo, ,] = getUrlParams(); 36 | return !repo; 37 | } 38 | 39 | function logIn() { 40 | // return new Promise((resolve, reject) => { 41 | var authenticator = new netlify({ 42 | site_id: "ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6" 43 | }); 44 | authenticator.authenticate({ provider: "github", scope: "repo" }, function( 45 | err, 46 | data 47 | ) { 48 | if (err) { 49 | console.error(err); 50 | return; 51 | } 52 | window.localStorage.setItem(TOKEN_KEY, data.token); 53 | window.location.reload(false); 54 | }); 55 | // }); 56 | } 57 | function LogInButton() { 58 | return ( 59 | 79 | ); 80 | } 81 | 82 | function getParams() { 83 | const [repo, sha, path] = getUrlParams(); 84 | const token = window.localStorage.getItem(TOKEN_KEY); 85 | return { repo, sha, path, token }; 86 | } 87 | 88 | async function getVersions(last) { 89 | const params = { ...getParams(), last }; 90 | return await versioner.getVersions(SOURCE.GITHUB, params); 91 | } 92 | 93 | export default { 94 | showLanding, 95 | getPath, 96 | getParams, 97 | getVersions, 98 | logIn, 99 | isLoggedIn, 100 | LogInButton 101 | }; 102 | -------------------------------------------------------------------------------- /src/git-providers/gitlab-commit-fetcher.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from "js-base64"; 2 | 3 | const cache = {}; 4 | 5 | async function getCommits({ repo, sha, path, token, last }) { 6 | if (!cache[path]) { 7 | const commitsResponse = await fetch( 8 | `https://gitlab.com/api/v4/projects/${encodeURIComponent( 9 | repo 10 | )}/repository/commits?path=${encodeURIComponent(path)}&ref_name=${sha}`, 11 | { headers: token ? { Authorization: `bearer ${token}` } : {} } 12 | ); 13 | 14 | const commitsJson = await commitsResponse.json(); 15 | 16 | if (!commitsResponse.ok) { 17 | throw { 18 | status: commitsResponse.status, 19 | body: commitsJson 20 | }; 21 | } 22 | 23 | cache[path] = commitsJson.map(commit => ({ 24 | sha: commit.id, 25 | date: new Date(commit.authored_date), 26 | author: { 27 | login: commit.author_name 28 | }, 29 | // commitUrl: commit.html_url, 30 | message: commit.title 31 | })); 32 | } 33 | 34 | const commits = cache[path].slice(0, last); 35 | 36 | await Promise.all( 37 | commits.map(async commit => { 38 | if (!commit.content) { 39 | const info = await getContent(repo, commit.sha, path, token); 40 | commit.content = info.content; 41 | } 42 | }) 43 | ); 44 | 45 | return commits; 46 | } 47 | 48 | async function getContent(repo, sha, path, token) { 49 | const contentResponse = await fetch( 50 | `https://gitlab.com/api/v4/projects/${encodeURIComponent( 51 | repo 52 | )}/repository/files/${encodeURIComponent(path)}?ref=${sha}`, 53 | { headers: token ? { Authorization: `bearer ${token}` } : {} } 54 | ); 55 | 56 | if (contentResponse.status === 404) { 57 | return { content: "" }; 58 | } 59 | 60 | const contentJson = await contentResponse.json(); 61 | 62 | if (!contentResponse.ok) { 63 | throw { 64 | status: contentResponse.status, 65 | body: contentJson 66 | }; 67 | } 68 | 69 | const content = Base64.decode(contentJson.content); 70 | return { content }; 71 | } 72 | 73 | export default { 74 | getCommits 75 | }; 76 | -------------------------------------------------------------------------------- /src/git-providers/gitlab-provider.js: -------------------------------------------------------------------------------- 1 | import netlify from "netlify-auth-providers"; 2 | import React from "react"; 3 | 4 | import versioner from "./versioner"; 5 | import { SOURCE } from "./sources"; 6 | 7 | const TOKEN_KEY = "gitlab-token"; 8 | 9 | function isLoggedIn() { 10 | return !!window.localStorage.getItem(TOKEN_KEY); 11 | } 12 | 13 | function getUrlParams() { 14 | const [ 15 | , 16 | owner, 17 | reponame, 18 | action, 19 | sha, 20 | ...paths 21 | ] = window.location.pathname.split("/"); 22 | 23 | if (action !== "commits" && action !== "blob") { 24 | return []; 25 | } 26 | 27 | return [owner + "/" + reponame, sha, paths.join("/")]; 28 | } 29 | 30 | function getPath() { 31 | const [, , path] = getUrlParams(); 32 | return path; 33 | } 34 | 35 | function showLanding() { 36 | const [repo, ,] = getUrlParams(); 37 | return !repo; 38 | } 39 | 40 | function logIn() { 41 | // return new Promise((resolve, reject) => { 42 | var authenticator = new netlify({ 43 | site_id: "ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6" 44 | }); 45 | authenticator.authenticate({ provider: "gitlab", scope: "api" }, function( 46 | err, 47 | data 48 | ) { 49 | if (err) { 50 | console.error(err); 51 | return; 52 | } 53 | window.localStorage.setItem(TOKEN_KEY, data.token); 54 | window.location.reload(false); 55 | }); 56 | // }); 57 | } 58 | 59 | function LogInButton() { 60 | return ( 61 | 67 | ); 68 | } 69 | 70 | function getParams() { 71 | const [repo, sha, path] = getUrlParams(); 72 | const token = window.localStorage.getItem(TOKEN_KEY); 73 | return { repo, sha, path, token }; 74 | } 75 | 76 | async function getVersions(last) { 77 | const params = { ...getParams(), last }; 78 | return await versioner.getVersions(SOURCE.GITLAB, params); 79 | } 80 | 81 | export default { 82 | showLanding, 83 | getPath, 84 | getVersions, 85 | logIn, 86 | isLoggedIn, 87 | LogInButton 88 | }; 89 | -------------------------------------------------------------------------------- /src/git-providers/language-detector.js: -------------------------------------------------------------------------------- 1 | const filenameRegex = [ 2 | { lang: "js", regex: /\.js$/i }, 3 | { lang: "jsx", regex: /\.jsx$/i }, 4 | { lang: "typescript", regex: /\.ts$/i }, 5 | { lang: "tsx", regex: /\.tsx$/i }, 6 | { lang: "json", regex: /\.json$|.babelrc$/i }, 7 | { lang: "yaml", regex: /\.yaml$|.yml$/i }, 8 | { lang: "bash", regex: /\.sh$/i }, 9 | { lang: "python", regex: /\.py$/i }, 10 | { lang: "dart", regex: /\.dart$/i }, 11 | { lang: "perl", regex: /\.pl$|.pm$/i }, 12 | { lang: "assembly", regex: /\.asm$/i }, 13 | { lang: "groovy", regex: /\.groovy$/i }, 14 | { lang: "sql", regex: /\.sql$/i }, 15 | { lang: "css", regex: /\.css$/i }, 16 | { lang: "less", regex: /\.less$/i }, 17 | { lang: "scss", regex: /\.scss$/i }, 18 | { lang: "ini", regex: /\.ini$|.editorconfig$/i }, 19 | { lang: "markup", regex: /\.xml$|\.html$|\.htm$|\.svg$|\.mathml$/i }, 20 | { lang: "batch", regex: /\.bat$/i }, 21 | { lang: "clojure", regex: /\.clj$/i }, 22 | { lang: "coffeescript", regex: /\.coffee$/i }, 23 | { lang: "cpp", regex: /\.cpp$|\.cc$/i }, 24 | { lang: "csharp", regex: /\.cs$/i }, 25 | { lang: "csp", regex: /\.csp$/i }, 26 | { lang: "diff", regex: /\.diff$/i }, 27 | { lang: "docker", regex: /dockerfile$/i }, 28 | { lang: "fsharp", regex: /\.fsharp$/i }, 29 | { lang: "go", regex: /\.go$/i }, 30 | { lang: "handlebars", regex: /\.hbs$/i }, 31 | { lang: "haskell", regex: /\.hs$/i }, 32 | { lang: "java", regex: /\.java$/i }, 33 | { lang: "kotlin", regex: /\.kt$/i }, 34 | { lang: "lua", regex: /\.lua$/i }, 35 | { lang: "markdown", regex: /\.md$/i }, 36 | { lang: "msdax", regex: /\.msdax$/i }, 37 | { lang: "sql", regex: /\.mysql$/i }, 38 | { lang: "objective-c", regex: /\.objc$/i }, 39 | { lang: "pgsql", regex: /\.pgsql$/i }, 40 | { lang: "php", regex: /\.php$/i }, 41 | { lang: "postiats", regex: /\.postiats$/i }, 42 | { lang: "powershell", regex: /\.ps$/i }, 43 | { lang: "pug", regex: /\.pug$/i }, 44 | { lang: "r", regex: /\.r$/i }, 45 | { lang: "razor", regex: /\.razor$/i }, 46 | { lang: "reason", regex: /\.re$/i }, 47 | { lang: "ruby", regex: /\.rb$/i }, 48 | { lang: "rust", regex: /\.rs$/i }, 49 | { lang: "small basic", regex: /\.smallbasic$/i }, 50 | { lang: "scala", regex: /\.scala$/i }, 51 | { lang: "scheme", regex: /\.scheme$/i }, 52 | { lang: "solidity", regex: /\.solidity$/i }, 53 | { lang: "st", regex: /\.st$/i }, 54 | { lang: "swift", regex: /\.swift$/i }, 55 | // { lang: "toml", regex: /\.toml$/i }, 56 | { lang: "vb", regex: /\.vb$/i }, 57 | { lang: "wasm", regex: /\.wasm$/i }, 58 | // fallback 59 | { lang: "js", regex: /.*/i } 60 | ]; 61 | 62 | export function getLanguage(filename) { 63 | return filenameRegex.find(x => x.regex.test(filename)).lang; 64 | } 65 | 66 | const dependencies = { 67 | cpp: ["c"], 68 | tsx: ["jsx"], 69 | scala: ["java"] 70 | }; 71 | 72 | export function getLanguageDependencies(lang) { 73 | return dependencies[lang]; 74 | } 75 | 76 | export function loadLanguage(lang) { 77 | if (["js", "css", "html"].includes(lang)) { 78 | return Promise.resolve(); 79 | } 80 | 81 | const deps = getLanguageDependencies(lang); 82 | 83 | let depPromise = import("prismjs"); 84 | 85 | if (deps) { 86 | depPromise = depPromise.then(() => 87 | Promise.all(deps.map(dep => import(`prismjs/components/prism-${dep}`))) 88 | ); 89 | } 90 | 91 | return depPromise.then(() => import(`prismjs/components/prism-${lang}`)); 92 | } 93 | -------------------------------------------------------------------------------- /src/git-providers/language-detector.test.js: -------------------------------------------------------------------------------- 1 | import { getLanguage, getLanguageDependencies } from "./language-detector"; 2 | 3 | describe("Can detect language", () => { 4 | test("javascript", () => { 5 | expect(getLanguage("my-file.js")).toBe("js"); 6 | }); 7 | 8 | test("jsx", () => { 9 | expect(getLanguage("my-file.jsx")).toBe("jsx"); 10 | }); 11 | 12 | test("typescript", () => { 13 | expect(getLanguage("my-file.ts")).toBe("typescript"); 14 | }); 15 | 16 | test("tsx", () => { 17 | expect(getLanguage("my-file.tsx")).toBe("tsx"); 18 | }); 19 | 20 | describe("json:", () => { 21 | test("json", () => { 22 | expect(getLanguage("my-file.json")).toBe("json"); 23 | }); 24 | test("babelrc", () => { 25 | expect(getLanguage("my-file.babelrc")).toBe("json"); 26 | }); 27 | }); 28 | 29 | describe("markup", () => { 30 | test("html", () => { 31 | expect(getLanguage("my-file.html")).toBe("markup"); 32 | }); 33 | 34 | test("htm", () => { 35 | expect(getLanguage("my-file.htm")).toBe("markup"); 36 | }); 37 | 38 | test("svg", () => { 39 | expect(getLanguage("my-file.svg")).toBe("markup"); 40 | }); 41 | test("xml", () => { 42 | expect(getLanguage("my-file.xml")).toBe("markup"); 43 | }); 44 | }); 45 | 46 | describe("yaml", () => { 47 | test("yaml", () => { 48 | expect(getLanguage("my-file.yaml")).toBe("yaml"); 49 | }); 50 | 51 | test("yml", () => { 52 | expect(getLanguage("my-file.yml")).toBe("yaml"); 53 | }); 54 | }); 55 | 56 | test("bash", () => { 57 | expect(getLanguage("my-file.sh")).toBe("bash"); 58 | }); 59 | 60 | test("pyhton", () => { 61 | expect(getLanguage("my-file.py")).toBe("python"); 62 | }); 63 | 64 | test("dart", () => { 65 | expect(getLanguage("my-file.dart")).toBe("dart"); 66 | }); 67 | 68 | describe("perl", () => { 69 | test("pl", () => { 70 | expect(getLanguage("my-file.pl")).toBe("perl"); 71 | }); 72 | 73 | test("pm", () => { 74 | expect(getLanguage("my-file.pm")).toBe("perl"); 75 | }); 76 | }); 77 | 78 | test("assembly", () => { 79 | expect(getLanguage("my-file.asm")).toBe("assembly"); 80 | }); 81 | 82 | test("groovy", () => { 83 | expect(getLanguage("my-file.groovy")).toBe("groovy"); 84 | }); 85 | 86 | test("sql", () => { 87 | expect(getLanguage("my-file.sql")).toBe("sql"); 88 | }); 89 | 90 | test("css", () => { 91 | expect(getLanguage("my-file.css")).toBe("css"); 92 | }); 93 | 94 | test("less", () => { 95 | expect(getLanguage("my-file.less")).toBe("less"); 96 | }); 97 | 98 | test("scss", () => { 99 | expect(getLanguage("my-file.scss")).toBe("scss"); 100 | }); 101 | 102 | describe("ini", () => { 103 | test("ini", () => { 104 | expect(getLanguage("my-file.ini")).toBe("ini"); 105 | }); 106 | 107 | test("editorconfig", () => { 108 | expect(getLanguage("my-file.editorconfig")).toBe("ini"); 109 | }); 110 | }); 111 | 112 | test("bat", () => { 113 | expect(getLanguage("my-file.bat")).toBe("batch"); 114 | }); 115 | 116 | test("clojure", () => { 117 | expect(getLanguage("my-file.clj")).toBe("clojure"); 118 | }); 119 | 120 | test("coffeescript", () => { 121 | expect(getLanguage("my-file.coffee")).toBe("coffeescript"); 122 | }); 123 | 124 | test("clojure", () => { 125 | expect(getLanguage("my-file.clj")).toBe("clojure"); 126 | }); 127 | 128 | describe("cpp", () => { 129 | test("cpp", () => { 130 | expect(getLanguage("my-file.cpp")).toBe("cpp"); 131 | }); 132 | 133 | test("cc", () => { 134 | expect(getLanguage("my-file.cc")).toBe("cpp"); 135 | }); 136 | }); 137 | 138 | test("csharp", () => { 139 | expect(getLanguage("my-file.cs")).toBe("csharp"); 140 | }); 141 | 142 | test("csp", () => { 143 | expect(getLanguage("my-file.csp")).toBe("csp"); 144 | }); 145 | 146 | test("diff", () => { 147 | expect(getLanguage("my-file.diff")).toBe("diff"); 148 | }); 149 | 150 | describe("docker", () => { 151 | test("long dockerfile", () => { 152 | expect(getLanguage("my-file.dockerfile")).toBe("docker"); 153 | }); 154 | 155 | test("dockerfile", () => { 156 | expect(getLanguage("Dockerfile")).toBe("docker"); 157 | }); 158 | }); 159 | 160 | test("fsharp", () => { 161 | expect(getLanguage("my-file.fsharp")).toBe("fsharp"); 162 | }); 163 | 164 | test("go", () => { 165 | expect(getLanguage("my-file.go")).toBe("go"); 166 | }); 167 | 168 | test("haskell", () => { 169 | expect(getLanguage("my-file.hs")).toBe("haskell"); 170 | }); 171 | 172 | test("java", () => { 173 | expect(getLanguage("my-file.java")).toBe("java"); 174 | }); 175 | 176 | test("kotlin", () => { 177 | expect(getLanguage("my-file.kt")).toBe("kotlin"); 178 | }); 179 | 180 | test("lua", () => { 181 | expect(getLanguage("my-file.lua")).toBe("lua"); 182 | }); 183 | 184 | test("markdown", () => { 185 | expect(getLanguage("my-file.md")).toBe("markdown"); 186 | }); 187 | 188 | test("msdax", () => { 189 | expect(getLanguage("my-file.msdax")).toBe("msdax"); 190 | }); 191 | 192 | test("sql", () => { 193 | expect(getLanguage("my-file.mysql")).toBe("sql"); 194 | }); 195 | 196 | test("objective-c", () => { 197 | expect(getLanguage("my-file.objc")).toBe("objective-c"); 198 | }); 199 | 200 | test("pgsql", () => { 201 | expect(getLanguage("my-file.pgsql")).toBe("pgsql"); 202 | }); 203 | 204 | test("php", () => { 205 | expect(getLanguage("my-file.php")).toBe("php"); 206 | }); 207 | 208 | test("postiats", () => { 209 | expect(getLanguage("my-file.postiats")).toBe("postiats"); 210 | }); 211 | 212 | test("powershell", () => { 213 | expect(getLanguage("my-file.ps")).toBe("powershell"); 214 | }); 215 | 216 | test("pug", () => { 217 | expect(getLanguage("my-file.pug")).toBe("pug"); 218 | }); 219 | 220 | test("r", () => { 221 | expect(getLanguage("my-file.r")).toBe("r"); 222 | }); 223 | 224 | test("razor", () => { 225 | expect(getLanguage("my-file.razor")).toBe("razor"); 226 | }); 227 | 228 | test("reason", () => { 229 | expect(getLanguage("my-file.re")).toBe("reason"); 230 | }); 231 | 232 | test("ruby", () => { 233 | expect(getLanguage("my-file.rb")).toBe("ruby"); 234 | }); 235 | 236 | test("rust", () => { 237 | expect(getLanguage("my-file.rs")).toBe("rust"); 238 | }); 239 | 240 | test("small basic", () => { 241 | expect(getLanguage("my-file.smallbasic")).toBe("small basic"); 242 | }); 243 | 244 | test("scala", () => { 245 | expect(getLanguage("my-file.scala")).toBe("scala"); 246 | }); 247 | 248 | test("scheme", () => { 249 | expect(getLanguage("my-file.scheme")).toBe("scheme"); 250 | }); 251 | 252 | test("solidity", () => { 253 | expect(getLanguage("my-file.solidity")).toBe("solidity"); 254 | }); 255 | 256 | test("swift", () => { 257 | expect(getLanguage("my-file.swift")).toBe("swift"); 258 | }); 259 | 260 | test("vb", () => { 261 | expect(getLanguage("my-file.vb")).toBe("vb"); 262 | }); 263 | 264 | test("wasm", () => { 265 | expect(getLanguage("my-file.wasm")).toBe("wasm"); 266 | }); 267 | }); 268 | 269 | describe("Fallback scenarios", () => { 270 | test("Random file extension", () => { 271 | expect(getLanguage("my-file.nonsense")).toBe("js"); 272 | }); 273 | 274 | test("No file extension", () => { 275 | expect(getLanguage("my-file")).toBe("js"); 276 | }); 277 | 278 | test("Empty string", () => { 279 | expect(getLanguage("")).toBe("js"); 280 | }); 281 | }); 282 | 283 | describe("Dependencies", () => { 284 | test("tsx", () => { 285 | expect(getLanguageDependencies("tsx")).toEqual(["jsx"]); 286 | }); 287 | 288 | test("cpp", () => { 289 | expect(getLanguageDependencies("cpp")).toEqual(["c"]); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /src/git-providers/providers.js: -------------------------------------------------------------------------------- 1 | const { SOURCE, getSource } = require("./sources"); 2 | 3 | let providers; 4 | if (process.env.REACT_APP_GIT_PROVIDER === SOURCE.VSCODE) { 5 | // We can't use web workers on vscode webview 6 | providers = { 7 | [SOURCE.VSCODE]: require("./vscode-provider").default 8 | }; 9 | } else { 10 | providers = { 11 | [SOURCE.CLI]: require("./cli-provider").default, 12 | [SOURCE.GITLAB]: require("./gitlab-provider").default, 13 | [SOURCE.GITHUB]: require("./github-provider").default, 14 | [SOURCE.BITBUCKET]: require("./bitbucket-provider").default 15 | }; 16 | } 17 | 18 | export default function getGitProvider(source) { 19 | source = source || getSource(); 20 | const provider = providers[source]; 21 | return provider; 22 | } 23 | -------------------------------------------------------------------------------- /src/git-providers/sources.js: -------------------------------------------------------------------------------- 1 | export const SOURCE = { 2 | GITHUB: "github", 3 | GITLAB: "gitlab", 4 | BITBUCKET: "bitbucket", 5 | CLI: "cli", 6 | VSCODE: "vscode" 7 | }; 8 | 9 | export function getSource() { 10 | if (process.env.REACT_APP_GIT_PROVIDER) 11 | return process.env.REACT_APP_GIT_PROVIDER; 12 | 13 | const [cloud] = window.location.host.split("."); 14 | if ([SOURCE.GITLAB, SOURCE.GITHUB, SOURCE.BITBUCKET].includes(cloud)) { 15 | return cloud; 16 | } 17 | const source = new URLSearchParams(window.location.search).get("source"); 18 | return source || SOURCE.GITHUB; 19 | } 20 | -------------------------------------------------------------------------------- /src/git-providers/tokenizer.js: -------------------------------------------------------------------------------- 1 | // https://github.com/PrismJS/prism/issues/1303#issuecomment-375353987 2 | global.Prism = { disableWorkerMessageHandler: true }; 3 | const Prism = require("prismjs"); 4 | 5 | const newlineRe = /\r\n|\r|\n/; 6 | 7 | // Take a list of nested tokens 8 | // (token.content may contain an array of tokens) 9 | // and flatten it so content is always a string 10 | // and type the type of the leaf 11 | function flattenTokens(tokens) { 12 | const flatList = []; 13 | tokens.forEach(token => { 14 | if (Array.isArray(token.content)) { 15 | flatList.push(...flattenTokens(token.content)); 16 | } else { 17 | flatList.push(token); 18 | } 19 | }); 20 | return flatList; 21 | } 22 | 23 | // Convert strings to tokens 24 | function tokenizeStrings(prismTokens, parentType = "plain") { 25 | return prismTokens.map(pt => 26 | typeof pt === "string" 27 | ? { type: parentType, content: pt } 28 | : { 29 | type: pt.type, 30 | content: Array.isArray(pt.content) 31 | ? tokenizeStrings(pt.content, pt.type) 32 | : pt.content 33 | } 34 | ); 35 | } 36 | 37 | export default function tokenize(code, language = "javascript") { 38 | const prismTokens = Prism.tokenize(code, Prism.languages[language]); 39 | const nestedTokens = tokenizeStrings(prismTokens); 40 | const tokens = flattenTokens(nestedTokens); 41 | 42 | let currentLine = []; 43 | const lines = [currentLine]; 44 | tokens.forEach(token => { 45 | const contentLines = token.content.split(newlineRe); 46 | 47 | const firstContent = contentLines.shift(); 48 | if (firstContent !== "") { 49 | currentLine.push({ type: token.type, content: firstContent }); 50 | } 51 | contentLines.forEach(content => { 52 | currentLine = []; 53 | lines.push(currentLine); 54 | if (content !== "") { 55 | currentLine.push({ type: token.type, content }); 56 | } 57 | }); 58 | }); 59 | return lines; 60 | } 61 | -------------------------------------------------------------------------------- /src/git-providers/versioner.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-webpack-loader-syntax */ 2 | import worker from "workerize-loader!./versioner.worker"; 3 | let versioner = worker(); 4 | 5 | export default versioner; 6 | -------------------------------------------------------------------------------- /src/git-providers/versioner.worker.js: -------------------------------------------------------------------------------- 1 | import { getLanguage, loadLanguage } from "./language-detector"; 2 | import { getSlides, getChanges } from "./differ"; 3 | 4 | import github from "./github-commit-fetcher"; 5 | import gitlab from "./gitlab-commit-fetcher"; 6 | import bitbucket from "./bitbucket-commit-fetcher"; 7 | import cli from "./cli-commit-fetcher"; 8 | import { SOURCE } from "./sources"; 9 | 10 | const fetchers = { 11 | [SOURCE.GITHUB]: github.getCommits, 12 | [SOURCE.GITLAB]: gitlab.getCommits, 13 | [SOURCE.BITBUCKET]: bitbucket.getCommits, 14 | [SOURCE.CLI]: cli.getCommits 15 | }; 16 | 17 | export async function getVersions(source, params) { 18 | const { path } = params; 19 | const lang = getLanguage(path); 20 | const langPromise = loadLanguage(lang); 21 | 22 | const getCommits = fetchers[source]; 23 | const commits = await getCommits(params); 24 | await langPromise; 25 | 26 | const codes = commits.map(commit => commit.content); 27 | const slides = getSlides(codes, lang); 28 | return commits.map((commit, i) => ({ 29 | commit, 30 | lines: slides[i], 31 | changes: getChanges(slides[i]) 32 | })); 33 | } 34 | -------------------------------------------------------------------------------- /src/git-providers/vscode-provider.js: -------------------------------------------------------------------------------- 1 | import { getLanguage, loadLanguage } from "./language-detector"; 2 | import { getSlides, getChanges } from "./differ"; 3 | 4 | const vscode = window.vscode; 5 | 6 | function getPath() { 7 | return window._PATH; 8 | } 9 | 10 | function showLanding() { 11 | return false; 12 | } 13 | 14 | function getCommits(path, last) { 15 | return new Promise((resolve, reject) => { 16 | window.addEventListener( 17 | "message", 18 | event => { 19 | const commits = event.data; 20 | commits.forEach(c => (c.date = new Date(c.date))); 21 | resolve(commits); 22 | }, 23 | { once: true } 24 | ); 25 | 26 | vscode.postMessage({ 27 | command: "commits", 28 | params: { 29 | path, 30 | last 31 | } 32 | }); 33 | }); 34 | } 35 | 36 | async function getVersions(last) { 37 | const path = getPath(); 38 | const lang = getLanguage(path); 39 | const langPromise = loadLanguage(lang); 40 | 41 | const commits = await getCommits(path, last); 42 | await langPromise; 43 | 44 | const codes = commits.map(commit => commit.content); 45 | const slides = getSlides(codes, lang); 46 | return commits.map((commit, i) => ({ 47 | commit, 48 | lines: slides[i], 49 | changes: getChanges(slides[i]) 50 | })); 51 | } 52 | 53 | export default { 54 | showLanding, 55 | getPath, 56 | getVersions 57 | }; 58 | -------------------------------------------------------------------------------- /src/history.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import useSpring from "react-use/lib/useSpring"; 3 | import Swipeable from "react-swipeable"; 4 | import Slide from "./slide"; 5 | import "./comment-box.css"; 6 | 7 | function CommitInfo({ commit, move, onClick }) { 8 | const message = commit.message.split("\n")[0].slice(0, 80); 9 | const isActive = Math.abs(move) < 0.5; 10 | return ( 11 |
20 |
30 | {commit.author.avatar && ( 31 | {commit.author.login} 38 | )} 39 |
40 |
41 | {commit.author.login} 42 |
43 |
44 | {isActive && commit.commitUrl ? ( 45 | 50 | on {commit.date.toDateString()} 51 | 52 | ) : ( 53 | `on ${commit.date.toDateString()}` 54 | )} 55 |
56 |
57 |
58 | {isActive && ( 59 |
64 | {message} 65 | {message !== commit.message ? " ..." : ""} 66 |
67 | )} 68 |
69 | ); 70 | } 71 | 72 | function CommitList({ commits, currentIndex, selectCommit }) { 73 | const mouseWheelEvent = e => { 74 | e.preventDefault(); 75 | selectCommit(currentIndex - (e.deltaX + e.deltaY) / 100); 76 | }; 77 | return ( 78 |
90 | {commits.map((commit, commitIndex) => ( 91 | selectCommit(commitIndex)} 96 | /> 97 | ))} 98 |
99 | ); 100 | } 101 | 102 | export default function History({ versions, loadMore }) { 103 | return ; 104 | } 105 | 106 | function Slides({ versions, loadMore }) { 107 | const [current, target, setTarget] = useSliderSpring(0); 108 | const commits = versions.map(v => v.commit); 109 | const setClampedTarget = newTarget => { 110 | setTarget(Math.min(commits.length - 0.75, Math.max(-0.25, newTarget))); 111 | if (newTarget >= commits.length - 5) { 112 | loadMore(); 113 | } 114 | }; 115 | const index = Math.round(current); 116 | const nextSlide = () => setClampedTarget(Math.round(target - 0.51)); 117 | const prevSlide = () => setClampedTarget(Math.round(target + 0.51)); 118 | useEffect(() => { 119 | document.body.onkeydown = function(e) { 120 | if (e.keyCode === 39) { 121 | nextSlide(); 122 | } else if (e.keyCode === 37) { 123 | prevSlide(); 124 | } else if (e.keyCode === 32) { 125 | setClampedTarget(current); 126 | } 127 | }; 128 | }); 129 | 130 | return ( 131 | 132 | setClampedTarget(index)} 136 | /> 137 | 142 | 143 | 144 | 145 | ); 146 | } 147 | 148 | // TODO use ./useSpring 149 | function useSliderSpring(initial) { 150 | const [target, setTarget] = useState(initial); 151 | const tension = 0; 152 | const friction = 10; 153 | const value = useSpring(target, tension, friction); 154 | return [Math.round(value * 100) / 100, target, setTarget]; 155 | } 156 | -------------------------------------------------------------------------------- /src/icons/chrome.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/icons/cli.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/icons/firefox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 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 | 120 | 121 | 122 | 123 | 124 | 126 | 146 | 147 | 149 | 150 | 151 | 174 | 179 | 181 | 186 | 188 | 193 | 198 | 200 | 201 | 202 | -------------------------------------------------------------------------------- /src/icons/vscode.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import App from "./app"; 2 | import React from "react"; 3 | import ReactDOM from "react-dom"; 4 | 5 | const root = document.getElementById("root"); 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | root 11 | ); 12 | -------------------------------------------------------------------------------- /src/landing.css: -------------------------------------------------------------------------------- 1 | .extensions { 2 | display: flex; 3 | justify-content: center; 4 | padding-bottom: 10px; 5 | } 6 | 7 | .extensions > * { 8 | padding: 4px 10px; 9 | } 10 | 11 | .landing a { 12 | color: inherit; 13 | } 14 | 15 | .landing { 16 | color: #222; 17 | background: #fafafa; 18 | } 19 | 20 | .landing > * { 21 | background: linear-gradient(rgba(255, 255, 255), rgba(220, 220, 220)); 22 | } 23 | 24 | .landing header { 25 | display: flex; 26 | padding: 100px 0px; 27 | } 28 | .landing h1 { 29 | margin-top: 10px; 30 | } 31 | 32 | .landing header a { 33 | color: rgb(1, 22, 39); 34 | } 35 | 36 | .landing header a.button { 37 | background: rgb(1, 22, 39); 38 | color: #fafafa; 39 | padding: 9px 16px; 40 | margin: 10px auto 15px; 41 | width: 80px; 42 | text-align: center; 43 | border-radius: 4px; 44 | text-decoration: none; 45 | display: block; 46 | } 47 | 48 | .landing header video { 49 | margin-right: 115px; 50 | } 51 | 52 | @media (max-width: 1130px) { 53 | .landing header video { 54 | margin-right: 0px; 55 | margin-bottom: 20px; 56 | max-width: 80%; 57 | height: auto !important; 58 | min-width: 350px; 59 | } 60 | .landing header { 61 | flex-direction: column; 62 | padding: 40px 0px 20px; 63 | } 64 | .landing header .summary { 65 | width: 560px; 66 | max-width: 80%; 67 | } 68 | } 69 | 70 | .landing .testimonies { 71 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 72 | display: grid; 73 | width: 800px; 74 | margin: 10px auto; 75 | grid-template-columns: 400px 400px; 76 | grid-template-rows: 180px 150px; 77 | grid-column-gap: 14px; 78 | grid-row-gap: 14px; 79 | } 80 | 81 | @media (max-width: 900px) { 82 | .landing .testimonies { 83 | grid-template-columns: 400px; 84 | grid-template-rows: 180px 180px 150px 150px; 85 | width: 400px; 86 | } 87 | } 88 | @media (max-width: 420px) { 89 | .landing .testimonies { 90 | grid-template-columns: auto; 91 | grid-template-rows: auto; 92 | width: 90%; 93 | } 94 | } 95 | 96 | .landing .testimonies > * { 97 | border: 1px solid #e1e8ed; 98 | display: inline-block; 99 | text-decoration: none; 100 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); 101 | border-radius: 5px; 102 | } 103 | 104 | .landing blockquote { 105 | display: flex; 106 | flex-direction: column; 107 | height: 100%; 108 | margin: 0; 109 | padding: 19px; 110 | box-sizing: border-box; 111 | } 112 | 113 | .landing blockquote p { 114 | flex: 1; 115 | margin: 0 0 12px 0; 116 | } 117 | 118 | .landing blockquote img { 119 | height: 36px; 120 | width: 36px; 121 | border-radius: 50%; 122 | } 123 | 124 | .landing .support > div { 125 | width: 800px; 126 | } 127 | 128 | @media (max-width: 900px) { 129 | .landing .support > div { 130 | width: 600px; 131 | } 132 | } 133 | @media (max-width: 600px) { 134 | .landing .support > div { 135 | width: 350px; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/landing.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import demoMp4 from "./demo.mp4"; 3 | import demoWebm from "./demo.webm"; 4 | import smashing from "./avatar.smashing.jpg"; 5 | import github from "./avatar.github.jpg"; 6 | import addy from "./avatar.addy.jpg"; 7 | import cssTricks from "./avatar.css-tricks.jpg"; 8 | import { ReactComponent as ChromeLogo } from "./icons/chrome.svg"; 9 | import { ReactComponent as FirefoxLogo } from "./icons/firefox.svg"; 10 | import { ReactComponent as CliLogo } from "./icons/cli.svg"; 11 | import { ReactComponent as VsCodeLogo } from "./icons/vscode.svg"; 12 | import "./landing.css"; 13 | 14 | export default function Landing() { 15 | const url = `${window.location.protocol}//${ 16 | window.location.host 17 | }/babel/babel/blob/master/packages/babel-core/test/browserify.js`; 18 | return ( 19 |
20 |
27 | 43 |
44 |

Git History

45 | Quickly browse the history of files in any git repo: 46 |
    47 |
  1. 48 | Go to a file in GitHub (or{" "} 49 | GitLab, or Bitbucket) 50 |
  2. 51 |
  3. 52 | Replace github.com with github.githistory.xyz 53 |
  4. 54 |
  5. There's no step three
  6. 55 |
56 | 57 | Try it 58 | 59 |

Also available as extensions:

60 |
61 | 66 | 67 | 68 | 73 | 74 | 75 | 76 | 81 | 82 | 83 | 84 | 89 | 90 | 91 |
92 |
99 |