├── .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 |
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 |
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 |
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 |
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 |
57 | Sign in with Bitbucket
58 |
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 |
63 |
64 |
72 |
73 |
74 |
75 |
76 | Sign in with GitHub
77 |
78 |
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 |
65 | Sign in with GitLab
66 |
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 |
38 | )}
39 |
40 |
41 | {commit.author.login}
42 |
43 |
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 |
116 | );
117 | }
118 |
119 | function Testimonies() {
120 | return (
121 |
122 | What people are saying...
123 |
124 |
129 | Git History caught our eye with a beautiful way to tour the history of
130 | a file in a GitHub repo. ... there’s nothing to download and install:
131 | point Git History to a repository file URL to start traveling through
132 | time. Great Scott!
133 |
134 |
139 | Ahh you know when you need to browse your Git history but it takes a
140 | while to find what you are looking for? Git History lets you browse
141 | the history in no-time. Useful.
142 |
143 |
148 | I love little apps like this that copy the URL structure of another
149 | app, so you can replace just the TLD and it does something useful.
150 |
151 |
156 | There's something really satisfying about browsing file history with
157 | this timeline UI. It's super nice.
158 |
159 |
160 |
161 | );
162 | }
163 |
164 | function Testimony({ link, avatar, name, children }) {
165 | return (
166 |
167 |
168 | {children}
169 |
170 |
171 |
172 | {name}
173 |
174 |
175 |
176 |
177 | );
178 | }
179 |
180 | function ResponsivePicture({ link, src, alt, append = "" }) {
181 | return (
182 |
183 |
184 |
188 |
192 |
193 |
194 |
195 | );
196 | }
197 |
198 | function Backers() {
199 | return (
200 |
207 | Support Git History
208 |
273 |
274 | );
275 | }
276 |
--------------------------------------------------------------------------------
/src/nightOwl.js:
--------------------------------------------------------------------------------
1 | const theme = {
2 | plain: {
3 | color: "#d6deeb",
4 | backgroundColor: "#011627"
5 | },
6 | styles: [
7 | {
8 | types: ["changed"],
9 | style: {
10 | color: "rgb(162, 191, 252)",
11 | fontStyle: "italic"
12 | }
13 | },
14 | {
15 | types: ["deleted"],
16 | style: {
17 | color: "rgba(239, 83, 80, 0.56)",
18 | fontStyle: "italic"
19 | }
20 | },
21 | {
22 | types: ["inserted", "attr-name"],
23 | style: {
24 | color: "rgb(173, 219, 103)",
25 | fontStyle: "italic"
26 | }
27 | },
28 | {
29 | types: ["comment"],
30 | style: {
31 | color: "rgb(99, 119, 119)",
32 | fontStyle: "italic"
33 | }
34 | },
35 | {
36 | types: ["string", "url"],
37 | style: {
38 | color: "rgb(173, 219, 103)"
39 | }
40 | },
41 | {
42 | types: ["variable"],
43 | style: {
44 | color: "rgb(214, 222, 235)"
45 | }
46 | },
47 | {
48 | types: ["number"],
49 | style: {
50 | color: "rgb(247, 140, 108)"
51 | }
52 | },
53 | {
54 | types: ["builtin", "char", "constant", "function"],
55 | style: {
56 | color: "rgb(130, 170, 255)"
57 | }
58 | },
59 | {
60 | // This was manually added after the auto-generation
61 | // so that punctuations are not italicised
62 | types: ["punctuation"],
63 | style: {
64 | color: "rgb(199, 146, 234)"
65 | }
66 | },
67 | {
68 | types: ["selector", "doctype"],
69 | style: {
70 | color: "rgb(199, 146, 234)",
71 | fontStyle: "italic"
72 | }
73 | },
74 | {
75 | types: ["class-name"],
76 | style: {
77 | color: "rgb(255, 203, 139)"
78 | }
79 | },
80 | {
81 | types: ["tag", "operator", "keyword"],
82 | style: {
83 | color: "rgb(127, 219, 202)"
84 | }
85 | },
86 | {
87 | types: ["boolean"],
88 | style: {
89 | color: "rgb(255, 88, 116)"
90 | }
91 | },
92 | {
93 | types: ["property"],
94 | style: {
95 | color: "rgb(128, 203, 196)"
96 | }
97 | },
98 | {
99 | types: ["namespace"],
100 | style: {
101 | color: "rgb(178, 204, 214)"
102 | }
103 | }
104 | ]
105 | };
106 |
107 | module.exports = theme;
108 |
--------------------------------------------------------------------------------
/src/scroller.css:
--------------------------------------------------------------------------------
1 | .scroller::-webkit-scrollbar {
2 | background-color: rgba(255, 255, 255, 0.01);
3 | }
4 |
5 | .scroller::-webkit-scrollbar-thumb {
6 | background-color: rgb(173, 219, 103, 0.3);
7 | }
8 |
--------------------------------------------------------------------------------
/src/scroller.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import useChildren from "./use-virtual-children";
3 | import "./scroller.css";
4 | import useSpring from "./use-spring";
5 | import { nextIndex, prevIndex, getScrollTop } from "./utils";
6 |
7 | const initialState = {
8 | snap: false,
9 | targetTop: 0,
10 | currentTop: 0,
11 | areaIndex: 0
12 | };
13 |
14 | export default function Scroller({
15 | items,
16 | getRow,
17 | getRowHeight,
18 | data,
19 | snapAreas
20 | }) {
21 | const ref = React.useRef();
22 | const height = useHeight(ref);
23 |
24 | const reducer = (prevState, action) => {
25 | switch (action.type) {
26 | case "unsnap":
27 | return !prevState.snap ? prevState : { ...prevState, snap: false };
28 | case "change-area":
29 | if (snapAreas.length === 0) {
30 | return prevState;
31 | }
32 |
33 | const { changeIndex, recalculate } = action;
34 | const movingFromUnknownIndex = !prevState.snap || recalculate;
35 |
36 | // TODO memo
37 | const heights = items.map((item, i) => getRowHeight(item, i, data));
38 |
39 | let newIndex;
40 | if (movingFromUnknownIndex) {
41 | //todo memo
42 | const oldIndex = getAreaIndex(
43 | prevState.targetTop,
44 | snapAreas,
45 | heights,
46 | height
47 | );
48 |
49 | newIndex = changeIndex(snapAreas, oldIndex);
50 | } else {
51 | newIndex = changeIndex(snapAreas, prevState.areaIndex);
52 | }
53 |
54 | if (newIndex === prevState.areaIndex && !movingFromUnknownIndex) {
55 | return prevState;
56 | }
57 |
58 | // TODO memo
59 | let contentHeight = heights.reduce((a, b) => a + b, 0);
60 |
61 | const targetTop = getScrollTop(
62 | snapAreas[newIndex],
63 | contentHeight,
64 | height,
65 | heights
66 | );
67 |
68 | return {
69 | ...prevState,
70 | areaIndex: newIndex,
71 | snap: true,
72 | currentTop: null,
73 | targetTop
74 | };
75 | case "manual-scroll":
76 | const { newTop } = action;
77 | if (newTop === prevState.currentTop && !prevState.snap) {
78 | return prevState;
79 | }
80 | // console.log("manual scroll", newTop);
81 | return {
82 | ...prevState,
83 | snap: false,
84 | currentTop: newTop,
85 | targetTop: newTop
86 | };
87 | default:
88 | throw Error();
89 | }
90 | };
91 |
92 | const [{ snap, targetTop, currentTop }, dispatch] = React.useReducer(
93 | reducer,
94 | initialState
95 | );
96 |
97 | const top = useSpring({
98 | target: targetTop,
99 | current: currentTop,
100 | round: Math.round
101 | });
102 | // console.log("render", targetTop, top);
103 |
104 | const children = useChildren({
105 | height,
106 | top,
107 | items,
108 | getRow,
109 | getRowHeight,
110 | data
111 | });
112 |
113 | React.useEffect(() => {
114 | document.body.addEventListener("keydown", e => {
115 | if (e.keyCode === 38) {
116 | dispatch({ type: "change-area", changeIndex: prevIndex });
117 | e.preventDefault();
118 | } else if (e.keyCode === 40) {
119 | dispatch({ type: "change-area", changeIndex: nextIndex });
120 | e.preventDefault();
121 | }
122 | });
123 | }, []);
124 |
125 | // Auto-scroll to closest change when changing versions:
126 | // React.useLayoutEffect(() => {
127 | // dispatch({
128 | // type: "change-area",
129 | // recalculate: true,
130 | // changeIndex: closestIndex
131 | // });
132 | // }, [snapAreas]);
133 |
134 | React.useEffect(() => {
135 | dispatch({
136 | type: "unsnap"
137 | });
138 | }, [snapAreas]);
139 |
140 | React.useLayoutEffect(() => {
141 | if (snap) {
142 | ref.current.scrollTop = top;
143 | }
144 | }, [snap, top]);
145 |
146 | return (
147 | {
156 | const newTop = e.target.scrollTop;
157 | if (newTop === top) {
158 | return;
159 | }
160 | dispatch({ type: "manual-scroll", newTop });
161 | }}
162 | >
163 |
175 |
176 | );
177 | }
178 |
179 | function getAreaIndex(scrollTop, areas, heights, containerHeight) {
180 | if (areas.length === 0) {
181 | return 0;
182 | }
183 |
184 | const scrollMiddle = scrollTop + containerHeight / 2;
185 |
186 | let h = 0;
187 | let i = 0;
188 | while (scrollMiddle > h) {
189 | h += heights[i++];
190 | }
191 | const middleRow = i;
192 |
193 | const areaCenters = areas.map(a => (a.start + a.end) / 2);
194 | areaCenters.unshift(0);
195 | for (let a = 0; a < areas.length; a++) {
196 | if (middleRow < areaCenters[a + 1]) {
197 | return (
198 | a -
199 | (areaCenters[a + 1] - middleRow) / (areaCenters[a + 1] - areaCenters[a])
200 | );
201 | }
202 | }
203 |
204 | return areas.length - 0.9;
205 | }
206 |
207 | function useHeight(ref) {
208 | let [height, setHeight] = React.useState(null);
209 |
210 | function handleResize() {
211 | setHeight(ref.current.clientHeight);
212 | }
213 |
214 | React.useEffect(() => {
215 | handleResize();
216 | window.addEventListener("resize", handleResize);
217 | return () => {
218 | window.removeEventListener("resize", handleResize);
219 | };
220 | }, []);
221 |
222 | return height;
223 | }
224 |
--------------------------------------------------------------------------------
/src/scroller.story.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { storiesOf } from "@storybook/react";
3 | import Scroller from "./scroller";
4 |
5 | const snapAreas1 = [
6 | { start: 1, end: 5 },
7 | { start: 15, end: 26 },
8 | { start: 50, end: 100 },
9 | { start: 300, end: 302 }
10 | ];
11 |
12 | const snapAreas2 = [
13 | { start: 8, end: 12 },
14 | { start: 30, end: 32 },
15 | { start: 550, end: 552 },
16 | { start: 595, end: 599 }
17 | ];
18 |
19 | const items = Array(600)
20 | .fill(0)
21 | .map((_, i) => {
22 | const a1 = snapAreas1.find(a => a.start <= i && i <= a.end);
23 | const a2 = snapAreas2.find(a => a.start <= i && i <= a.end);
24 | return {
25 | content: `Row ${i}${
26 | a1
27 | ? ` - Area1 [${a1.start}, ${a1.end}]`
28 | : a2
29 | ? ` - Area2 [${a2.start}, ${a2.end}]`
30 | : ""
31 | }`,
32 | key: i,
33 | height: 22
34 | };
35 | });
36 |
37 | function getRow(item) {
38 | return (
39 |
40 | {item.content}
41 |
42 | );
43 | }
44 |
45 | function getRowHeight(item) {
46 | return item.height;
47 | }
48 |
49 | function BasicScroller({ areas }) {
50 | const [top, setTop] = React.useState(40);
51 | return (
52 |
60 | );
61 | }
62 |
63 | storiesOf("Scroller", module).add("single", () => (
64 |
76 | ));
77 |
78 | function DoubleScroller() {
79 | const [flag, setFlag] = React.useState(false);
80 | return (
81 |
82 |
83 |
84 |
85 |
86 | {flag ? "Areas 1" : "Areas 2"}
87 | setFlag(flag => !flag)}>Toggle
88 |
89 |
90 | );
91 | }
92 |
93 | storiesOf("Scroller", module).add("multiple", () => (
94 |
102 |
103 |
104 | ));
105 |
--------------------------------------------------------------------------------
/src/slide.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import animation from "./animation";
3 | import theme from "./nightOwl";
4 | import Scroller from "./scroller";
5 |
6 | const themeStylesByType = Object.create(null);
7 | theme.styles.forEach(({ types, style }) => {
8 | types.forEach(type => {
9 | themeStylesByType[type] = Object.assign(
10 | themeStylesByType[type] || {},
11 | style
12 | );
13 | });
14 | });
15 |
16 | function getLineHeight(line, i, { styles }) {
17 | return styles[i].height != null ? styles[i].height : 15;
18 | }
19 |
20 | function getLine(line, i, { styles }) {
21 | const style = styles[i];
22 | return (
23 |
27 | {!line.tokens.length && }
28 | {line.tokens.map((token, i) => {
29 | const style = themeStylesByType[token.type] || {};
30 | return (
31 |
32 | {token.content}
33 |
34 | );
35 | })}
36 |
37 | );
38 | }
39 |
40 | function Slide({ lines, styles, changes }) {
41 | return (
42 |
53 |
60 |
61 | );
62 | }
63 |
64 | export default function SlideWrapper({ time, version }) {
65 | const { lines, changes } = version;
66 | const styles = animation((time + 1) / 2, lines);
67 | return ;
68 | }
69 |
--------------------------------------------------------------------------------
/src/use-spring.js:
--------------------------------------------------------------------------------
1 | // based on https://github.com/streamich/react-use/blob/master/src/useSpring.ts
2 | import { SpringSystem } from "rebound";
3 | import { useState, useEffect } from "react";
4 |
5 | export default function useSpring({
6 | target = 0,
7 | current = null,
8 | tension = 0,
9 | friction = 10,
10 | round = x => x
11 | }) {
12 | const [spring, setSpring] = useState(null);
13 | const [value, setValue] = useState(target);
14 |
15 | useEffect(() => {
16 | const listener = {
17 | onSpringUpdate: spring => {
18 | const value = spring.getCurrentValue();
19 | setValue(round(value));
20 | }
21 | };
22 |
23 | if (!spring) {
24 | const newSpring = new SpringSystem().createSpring(tension, friction);
25 | newSpring.setCurrentValue(target);
26 | setSpring(newSpring);
27 | newSpring.addListener(listener);
28 | return;
29 | }
30 |
31 | return () => {
32 | spring.removeListener(listener);
33 | setSpring(null);
34 | };
35 | }, [tension, friction]);
36 |
37 | useEffect(() => {
38 | if (spring) {
39 | spring.setEndValue(target);
40 | if (current != null) {
41 | spring.setCurrentValue(current);
42 | }
43 | }
44 | }, [target, current]);
45 |
46 | return value;
47 | }
48 |
--------------------------------------------------------------------------------
/src/use-spring.story.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { storiesOf } from "@storybook/react";
3 | import useSpring from "./use-spring";
4 |
5 | function Test() {
6 | const [{ target, current }, setState] = React.useState({
7 | target: 0,
8 | current: null
9 | });
10 | const value = useSpring({ target, current });
11 |
12 | return (
13 |
50 | );
51 | }
52 |
53 | storiesOf("useSpring", module).add("test", () => (
54 |
66 | ));
67 |
--------------------------------------------------------------------------------
/src/use-virtual-children.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default function useChildren({
4 | items,
5 | getRow,
6 | getRowHeight,
7 | height,
8 | top,
9 | data
10 | }) {
11 | const children = [];
12 |
13 | const extraRender = 1000;
14 |
15 | const topT = top - extraRender;
16 | const bottomT = top + height + extraRender;
17 | let h = 0;
18 |
19 | let topPlaceHolderH = 0;
20 | let bottomPlaceholderH = 0;
21 |
22 | // This is the bottleneck
23 | items.forEach((item, i) => {
24 | const itemH = getRowHeight(item, i, data);
25 | const nextH = h + itemH;
26 | const isOverTop = nextH < topT;
27 | const isUnderBottom = h > bottomT;
28 |
29 | if (isOverTop) {
30 | topPlaceHolderH += itemH;
31 | } else if (isUnderBottom) {
32 | bottomPlaceholderH += itemH;
33 | } else {
34 | children.push(getRow(item, i, data));
35 | }
36 |
37 | h = nextH;
38 | });
39 |
40 | children.unshift(
);
41 | children.push(
);
42 | return children;
43 | }
44 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | export function nextIndex(list, currentIndex) {
2 | return Math.min(list.length - 1, Math.floor(currentIndex + 1));
3 | }
4 |
5 | export function prevIndex(list, currentIndex) {
6 | return Math.max(0, Math.ceil(currentIndex - 1));
7 | }
8 |
9 | export function closestIndex(list, currentIndex) {
10 | return Math.min(Math.max(0, Math.round(currentIndex)), list.length - 1);
11 | }
12 |
13 | export function getScrollTop(area, contentHeight, containerHeight, heights) {
14 | const start = heights.slice(0, area.start).reduce((a, b) => a + b, 0);
15 | const end =
16 | start + heights.slice(area.start, area.end + 1).reduce((a, b) => a + b, 0);
17 | const middle = (end + start) / 2;
18 | const halfContainer = containerHeight / 2;
19 | const bestTop =
20 | end - start > containerHeight ? start : middle - halfContainer;
21 | if (bestTop < 0) return 0;
22 | if (bestTop + containerHeight > contentHeight) {
23 | return contentHeight - containerHeight;
24 | }
25 | return bestTop;
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils.test.js:
--------------------------------------------------------------------------------
1 | import { nextIndex, prevIndex } from "./utils";
2 |
3 | describe("nextIndex", () => {
4 | const fiveItems = [1, 2, 3, 4, 5];
5 | test("works with middle index", () => {
6 | expect(nextIndex(fiveItems, 2)).toBe(3);
7 | });
8 | test("works with last index", () => {
9 | expect(nextIndex(fiveItems, 4)).toBe(4);
10 | });
11 | test("works with fractions", () => {
12 | expect(nextIndex(fiveItems, 1.1)).toBe(2);
13 | expect(nextIndex(fiveItems, 1.9)).toBe(2);
14 | });
15 | });
16 |
17 | describe("prevIndex", () => {
18 | const fiveItems = [1, 2, 3, 4, 5];
19 | test("works with middle index", () => {
20 | expect(prevIndex(fiveItems, 2)).toBe(1);
21 | });
22 | test("works with start index", () => {
23 | expect(prevIndex(fiveItems, 0)).toBe(0);
24 | });
25 | test("works with fractions", () => {
26 | expect(prevIndex(fiveItems, 1.1)).toBe(1);
27 | expect(prevIndex(fiveItems, 1.9)).toBe(1);
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/vscode-ext/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | site
--------------------------------------------------------------------------------
/vscode-ext/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | // A launch configuration that launches the extension inside a new window
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | {
6 | "version": "0.2.0",
7 | "configurations": [
8 | {
9 | "name": "Extension",
10 | "type": "extensionHost",
11 | "request": "launch",
12 | "runtimeExecutable": "${execPath}",
13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"]
14 | },
15 | {
16 | "name": "Extension Tests",
17 | "type": "extensionHost",
18 | "request": "launch",
19 | "runtimeExecutable": "${execPath}",
20 | "args": [
21 | "--extensionDevelopmentPath=${workspaceFolder}",
22 | "--extensionTestsPath=${workspaceFolder}/test"
23 | ]
24 | }
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/vscode-ext/extension.js:
--------------------------------------------------------------------------------
1 | const vscode = require("vscode");
2 | const path = require("path");
3 | const fs = require("fs");
4 | const getCommits = require("./git");
5 |
6 | // this method is called when your extension is activated
7 | // your extension is activated the very first time the command is executed
8 |
9 | /**
10 | * @param {vscode.ExtensionContext} context
11 | */
12 | function activate(context) {
13 | // The command has been defined in the package.json file
14 | // Now provide the implementation of the command with registerCommand
15 | // The commandId parameter must match the command field in package.json
16 | let disposable = vscode.commands.registerCommand(
17 | "extension.git-file-history",
18 | function() {
19 | // The code you place here will be executed every time your command is executed
20 | try {
21 | const currentPath = getCurrentPath();
22 | if (!currentPath) {
23 | vscode.window.showInformationMessage("No active file");
24 | return;
25 | }
26 |
27 | const panel = vscode.window.createWebviewPanel(
28 | "gfh",
29 | `${path.basename(currentPath)} (Git History)`,
30 | vscode.ViewColumn.One,
31 | {
32 | enableScripts: true,
33 | retainContextWhenHidden: true,
34 | localResourceRoots: [
35 | vscode.Uri.file(path.join(context.extensionPath, "site"))
36 | ]
37 | }
38 | );
39 | const indexPath = path.join(
40 | context.extensionPath,
41 | "site",
42 | "index.html"
43 | );
44 |
45 | const index = fs.readFileSync(indexPath, "utf-8");
46 | const newIndex = index
47 | .replace(
48 | "",
49 | ``
52 | )
53 | .replace(
54 | "",
55 | ` `
60 | );
61 |
62 | panel.webview.html = newIndex;
63 |
64 | panel.webview.onDidReceiveMessage(
65 | message => {
66 | switch (message.command) {
67 | case "commits":
68 | const { path, last = 15, before = null } = message.params;
69 | getCommits(path, last, before)
70 | .then(commits => {
71 | panel.webview.postMessage(commits);
72 | })
73 | .catch(console.error);
74 | }
75 | },
76 | undefined,
77 | context.subscriptions
78 | );
79 | } catch (e) {
80 | console.error(e);
81 | throw e;
82 | }
83 | }
84 | );
85 |
86 | context.subscriptions.push(disposable);
87 | }
88 |
89 | function getCurrentPath() {
90 | return (
91 | vscode.window.activeTextEditor &&
92 | vscode.window.activeTextEditor.document &&
93 | vscode.window.activeTextEditor.document.fileName
94 | );
95 | }
96 |
97 | exports.activate = activate;
98 |
99 | // this method is called when your extension is deactivated
100 | function deactivate() {}
101 |
102 | module.exports = {
103 | activate,
104 | deactivate
105 | };
106 |
--------------------------------------------------------------------------------
/vscode-ext/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 |
--------------------------------------------------------------------------------
/vscode-ext/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pomber/git-history/a20f6085cf9055b350a4db13cea6e013936da9cd/vscode-ext/images/icon.png
--------------------------------------------------------------------------------
/vscode-ext/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "checkJs": false /* Typecheck .js files. */,
6 | "lib": ["es6"]
7 | },
8 | "exclude": ["node_modules"]
9 | }
10 |
--------------------------------------------------------------------------------
/vscode-ext/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "git-file-history",
3 | "displayName": "Git File History",
4 | "description": "Modern, fast and intuitive tool for browsing the history and files in any git repository",
5 | "version": "1.0.1",
6 | "repository": "pomber/git-history",
7 | "publisher": "pomber",
8 | "license": "MIT",
9 | "keywords": [
10 | "git",
11 | "history",
12 | "log",
13 | "file",
14 | "commit",
15 | "show"
16 | ],
17 | "engines": {
18 | "vscode": "^1.30.2"
19 | },
20 | "categories": [
21 | "Other"
22 | ],
23 | "icon": "images/icon.png",
24 | "galleryBanner": {
25 | "color": "#011627",
26 | "theme": "dark"
27 | },
28 | "activationEvents": [
29 | "onCommand:extension.git-file-history"
30 | ],
31 | "main": "./extension.js",
32 | "contributes": {
33 | "commands": [
34 | {
35 | "command": "extension.git-file-history",
36 | "title": "Git File History"
37 | }
38 | ]
39 | },
40 | "scripts": {
41 | "build-site": "cd .. && cross-env PUBLIC_URL=. REACT_APP_GIT_PROVIDER=vscode yarn build && rm -fr vscode-ext/site/ && cp -r build/ vscode-ext/site/",
42 | "build": " yarn build-site",
43 | "postinstall": "node ./node_modules/vscode/bin/install",
44 | "test": "node ./node_modules/vscode/bin/test",
45 | "vscode:prepublish": "yarn build"
46 | },
47 | "devDependencies": {
48 | "@types/mocha": "^2.2.42",
49 | "@types/node": "^10.12.21",
50 | "cross-env": "^5.2.0",
51 | "eslint": "^5.13.0",
52 | "typescript": "^3.3.1",
53 | "vscode": "^1.1.28"
54 | },
55 | "dependencies": {
56 | "execa": "^1.0.0"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/vscode-ext/readme.md:
--------------------------------------------------------------------------------
1 | # Git File History
2 |
3 | Quickly browse the history of a file from any git repository
4 |
5 | 
6 |
7 | ### Sponsors
8 |
9 | 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)]
10 |
11 |
12 |
13 |
14 |
15 | ### Backers
16 |
17 | Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)]
18 |
19 |
20 |
--------------------------------------------------------------------------------
/vscode-ext/test-git.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | // node test-git.js extension.js 2 94c91d9
4 |
5 | const getCommits = require("./git");
6 |
7 | const [, , path, last, before] = process.argv;
8 |
9 | getCommits(path, last, before).then(cs =>
10 | console.log(
11 | cs
12 | .map(c => {
13 | return `${c.hash} ${c.date.toDateString()} ${c.message}`;
14 | })
15 | .join("\n")
16 | )
17 | );
18 |
--------------------------------------------------------------------------------
/vscode-ext/test/extension.test.js:
--------------------------------------------------------------------------------
1 | /* global suite, test */
2 |
3 | //
4 | // Note: This example test is leveraging the Mocha test framework.
5 | // Please refer to their documentation on https://mochajs.org/ for help.
6 | //
7 |
8 | // The module 'assert' provides assertion methods from node
9 | const assert = require("assert");
10 |
11 | // You can import and use all API from the 'vscode' module
12 | // as well as import your extension to test it
13 | // const vscode = require('vscode');
14 | // const myExtension = require('../extension');
15 |
16 | // Defines a Mocha test suite to group tests of similar kind together
17 | suite("Extension Tests", function() {
18 | // Defines a Mocha unit test
19 | test("Something 1", function() {
20 | assert.equal(-1, [1, 2, 3].indexOf(5));
21 | assert.equal(-1, [1, 2, 3].indexOf(0));
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/vscode-ext/test/index.js:
--------------------------------------------------------------------------------
1 | //
2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
3 | //
4 | // This file is providing the test runner to use when running extension tests.
5 | // By default the test runner in use is Mocha based.
6 | //
7 | // You can provide your own test runner if you want to override it by exporting
8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension
9 | // host can call to run the tests. The test runner is expected to use console.log
10 | // to report the results back to the caller. When the tests are finished, return
11 | // a possible error to the callback or null if none.
12 |
13 | const testRunner = require("vscode/lib/testrunner");
14 |
15 | // You can directly control Mocha options by configuring the test runner below
16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options
17 | // for more info
18 | testRunner.configure({
19 | ui: "tdd", // the TDD UI is being used in extension.test.js (suite, test, etc.)
20 | useColors: true // colored output from test results
21 | });
22 |
23 | module.exports = testRunner;
24 |
--------------------------------------------------------------------------------
/vscode-ext/vsc-extension-quickstart.md:
--------------------------------------------------------------------------------
1 | ## What's in the folder
2 |
3 | - This folder contains all of the files necessary for your extension.
4 | - `package.json` - this is the manifest file in which you declare your extension and command.
5 | - The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
6 | - `extension.js` - this is the main file where you will provide the implementation of your command.
7 | - The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
8 | - We pass the function containing the implementation of the command as the second parameter to `registerCommand`.
9 |
10 | ## Get up and running straight away
11 |
12 | - Press `F5` to open a new window with your extension loaded.
13 | - Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Git File History`.
14 | - Set breakpoints in your code inside `extension.js` to debug your extension.
15 | - Find output from your extension in the debug console.
16 |
17 | ## Make changes
18 |
19 | - You can relaunch the extension from the debug toolbar after changing code in `extension.js`.
20 | - You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.
21 |
22 | ## Explore the API
23 |
24 | - You can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`.
25 |
26 | ## Run tests
27 |
28 | - Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.
29 | - Press `F5` to run the tests in a new window with your extension loaded.
30 | - See the output of the test result in the debug console.
31 | - Make changes to `test/extension.test.js` or create new test files inside the `test` folder.
32 | - By convention, the test runner will only consider files matching the name pattern `**.test.js`.
33 | - You can create folders inside the `test` folder to structure your tests any way you want.
34 |
--------------------------------------------------------------------------------