mirror of
https://github.com/pomber/git-history.git
synced 2024-11-26 08:35:07 +03:00
commit
dfea410a01
2
cli/.gitignore
vendored
Normal file
2
cli/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
site
|
24
cli/cli.js
Executable file
24
cli/cli.js
Executable file
@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const runServer = require("./server");
|
||||
const getCommits = require("./git");
|
||||
const fs = require("fs");
|
||||
|
||||
const path = process.argv[2];
|
||||
|
||||
if (!path || path === "--help") {
|
||||
console.log(`Usage:
|
||||
|
||||
githistory some/file.ext
|
||||
`);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
if (!fs.existsSync(path)) {
|
||||
console.log(`File not found: ${path}`);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const commitsPromise = getCommits(path);
|
||||
|
||||
runServer(path, commitsPromise);
|
36
cli/git.js
Normal file
36
cli/git.js
Normal file
@ -0,0 +1,36 @@
|
||||
const execa = require("execa");
|
||||
|
||||
async function getCommits(path) {
|
||||
const format = `{"hash":"%h","author":{"login":"%aN"},"date":"%ad","message":"%f"},`;
|
||||
const { stdout } = await execa("git", [
|
||||
"log",
|
||||
"--follow",
|
||||
"--reverse",
|
||||
"--abbrev-commit",
|
||||
`--pretty=format:${format}`,
|
||||
"--date=iso",
|
||||
"--",
|
||||
path
|
||||
]);
|
||||
const json = `[${stdout.slice(0, -1)}]`;
|
||||
const result = JSON.parse(json).map(commit => ({
|
||||
...commit,
|
||||
date: new Date(commit.date)
|
||||
}));
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getContent(commit, path) {
|
||||
const { stdout } = await execa("git", ["show", `${commit.hash}:${path}`]);
|
||||
return stdout;
|
||||
}
|
||||
|
||||
module.exports = async function(path) {
|
||||
const commits = await getCommits(path);
|
||||
await Promise.all(
|
||||
commits.map(async commit => {
|
||||
commit.content = await getContent(commit, path);
|
||||
})
|
||||
);
|
||||
return commits;
|
||||
};
|
244
cli/package-lock.json
generated
Normal file
244
cli/package-lock.json
generated
Normal file
@ -0,0 +1,244 @@
|
||||
{
|
||||
"name": "githistory",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"bytes": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
|
||||
"integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
},
|
||||
"content-disposition": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
|
||||
"integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "6.0.5",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
|
||||
"integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
|
||||
"requires": {
|
||||
"nice-try": "^1.0.4",
|
||||
"path-key": "^2.0.1",
|
||||
"semver": "^5.5.0",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
|
||||
"integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
|
||||
"requires": {
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
"integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
|
||||
"requires": {
|
||||
"cross-spawn": "^6.0.0",
|
||||
"get-stream": "^4.0.0",
|
||||
"is-stream": "^1.1.0",
|
||||
"npm-run-path": "^2.0.0",
|
||||
"p-finally": "^1.0.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"strip-eof": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"fast-url-parser": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz",
|
||||
"integrity": "sha1-9K8+qfNNiicc9YrSs3WfQx8LMY0=",
|
||||
"requires": {
|
||||
"punycode": "^1.3.2"
|
||||
}
|
||||
},
|
||||
"get-port": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-port/-/get-port-4.1.0.tgz",
|
||||
"integrity": "sha512-4/fqAYrzrzOiqDrdeZRKXGdTGgbkfTEumGlNQPeP6Jy8w0PzN9mzeNQ3XgHaTNie8pQ3hOUkrwlZt2Fzk5H9mA=="
|
||||
},
|
||||
"get-stream": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
|
||||
"integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
|
||||
"requires": {
|
||||
"pump": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
|
||||
},
|
||||
"mime-db": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
|
||||
"integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ=="
|
||||
},
|
||||
"mime-types": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
|
||||
"integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
|
||||
"requires": {
|
||||
"mime-db": "~1.33.0"
|
||||
}
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"nice-try": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
|
||||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
"integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
|
||||
"requires": {
|
||||
"path-key": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"open": {
|
||||
"version": "0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz",
|
||||
"integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw="
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
|
||||
"integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM="
|
||||
},
|
||||
"path-key": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
|
||||
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
|
||||
},
|
||||
"path-to-regexp": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz",
|
||||
"integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ=="
|
||||
},
|
||||
"pump": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
|
||||
"integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
|
||||
"requires": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
"once": "^1.3.1"
|
||||
}
|
||||
},
|
||||
"punycode": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
|
||||
"integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
|
||||
},
|
||||
"range-parser": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
|
||||
"integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
||||
"integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
|
||||
},
|
||||
"serve-handler": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-5.0.8.tgz",
|
||||
"integrity": "sha512-pqk0SChbBLLHfMIxQ55czjdiW7tj2cFy53svvP8e5VqEN/uB/QpfiTJ8k1uIYeFTDVoi+FGi5aqXScuu88bymg==",
|
||||
"requires": {
|
||||
"bytes": "3.0.0",
|
||||
"content-disposition": "0.5.2",
|
||||
"fast-url-parser": "1.1.3",
|
||||
"mime-types": "2.1.18",
|
||||
"minimatch": "3.0.4",
|
||||
"path-is-inside": "1.0.2",
|
||||
"path-to-regexp": "2.2.1",
|
||||
"range-parser": "1.2.0"
|
||||
}
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
|
||||
"integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
|
||||
"requires": {
|
||||
"shebang-regex": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
|
||||
"integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
|
||||
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
|
||||
},
|
||||
"strip-eof": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
|
||||
"integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
}
|
||||
}
|
||||
}
|
22
cli/package.json
Normal file
22
cli/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "githistory",
|
||||
"version": "0.0.0",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"githistory": "./cli.js",
|
||||
"git-history": "./cli.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"execa": "^1.0.0",
|
||||
"get-port": "^4.1.0",
|
||||
"open": "^0.0.5",
|
||||
"serve-handler": "^5.0.8"
|
||||
},
|
||||
"scripts": {
|
||||
"build-site": "cd .. && yarn build && rm -fr cli/site/ && cp -r build/ cli/site/",
|
||||
"build": "yarn build-site"
|
||||
},
|
||||
"devDependencies": {
|
||||
"np": "^4.0.2"
|
||||
}
|
||||
}
|
16
cli/readme.md
Normal file
16
cli/readme.md
Normal file
@ -0,0 +1,16 @@
|
||||
# Git History CLI
|
||||
|
||||
Quickly browse the history of a file from any git repository.
|
||||
|
||||
> You need [node](https://nodejs.org/en/) to run this
|
||||
|
||||
```bash
|
||||
$ npx githistory path/to/file.ext
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```bash
|
||||
$ npm install -g githistory
|
||||
$ githistory path/to/file.ext
|
||||
```
|
52
cli/server.js
Normal file
52
cli/server.js
Normal file
@ -0,0 +1,52 @@
|
||||
const fs = require("fs");
|
||||
const getPort = require("get-port");
|
||||
const open = require("open");
|
||||
const handler = require("serve-handler");
|
||||
const http = require("http");
|
||||
const pather = require("path");
|
||||
|
||||
const sitePath = pather.join(__dirname, "site/");
|
||||
const indexPath = pather.join(sitePath, "index.html");
|
||||
|
||||
function getIndex() {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(indexPath, "utf8", (err, data) => {
|
||||
if (err) reject(err);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const indexPromise = getIndex();
|
||||
|
||||
const portPromise = getPort({ port: 3000 });
|
||||
|
||||
module.exports = async function runServer(path, commitsPromise) {
|
||||
const server = http.createServer((request, response) => {
|
||||
if (request.url === "/") {
|
||||
Promise.all([indexPromise, commitsPromise]).then(([index, commits]) => {
|
||||
const newIndex = index.replace(
|
||||
"<script>window._CLI=null</script>",
|
||||
`<script>window._CLI={commits:${JSON.stringify(
|
||||
commits
|
||||
)},path:'${path}'}</script>`
|
||||
);
|
||||
var headers = { "Content-Type": "text/html" };
|
||||
response.writeHead(200, headers);
|
||||
response.write(newIndex);
|
||||
response.end();
|
||||
});
|
||||
} else {
|
||||
return handler(request, response, { public: sitePath });
|
||||
}
|
||||
});
|
||||
|
||||
const port = await portPromise;
|
||||
|
||||
return new Promise(resolve => {
|
||||
server.listen(port, () => {
|
||||
console.log("Running at http://localhost:" + port);
|
||||
open("http://localhost:" + port);
|
||||
});
|
||||
});
|
||||
};
|
1802
cli/yarn.lock
Normal file
1802
cli/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "githistory",
|
||||
"name": "githistory-web",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
|
@ -59,20 +59,14 @@
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- when served with githistory cli, commits are injected here: -->
|
||||
<script>
|
||||
window._CLI = null;
|
||||
</script>
|
||||
<div id="root"></div>
|
||||
<footer>
|
||||
<a href="https://github.com/pomber/git-history">Git History</a><br />by
|
||||
<a href="https://twitter.com/pomber">@pomber</a>
|
||||
</footer>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
|
@ -10,6 +10,6 @@
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
"theme_color": "#d6deeb",
|
||||
"background_color": "#011627"
|
||||
}
|
||||
|
163
src/app-helpers.js
Normal file
163
src/app-helpers.js
Normal file
@ -0,0 +1,163 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { getLanguage, loadLanguage } from "./language-detector";
|
||||
import { auth, isLoggedIn, getCommits } from "./github";
|
||||
|
||||
export function Center({ children }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
padding: "0 40px"
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Loading({ repo, path }) {
|
||||
return (
|
||||
<Center>
|
||||
<p>
|
||||
Loading <strong>{path}</strong> history {repo ? "from " + repo : ""}...
|
||||
</p>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
export function Error({ error }) {
|
||||
if (error.status === 403) {
|
||||
return (
|
||||
<Center>
|
||||
<p>
|
||||
GitHub API rate limit exceeded for your IP (60 requests per hour).
|
||||
</p>
|
||||
<p>Sign in with GitHub for more:</p>
|
||||
<GitHubButton onClick={login} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (error.status === 404) {
|
||||
return (
|
||||
<Center>
|
||||
<p>File not found.</p>
|
||||
{!isLoggedIn() && (
|
||||
<React.Fragment>
|
||||
<p>Is it from a private repo? Sign in with GitHub:</p>
|
||||
<GitHubButton onClick={login} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
console.error(
|
||||
"Let us know of the error at https://github.com/pomber/git-history/issues"
|
||||
);
|
||||
return (
|
||||
<Center>
|
||||
<p>Unexpected error. Check the console.</p>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubButton({ onClick }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
style={{ fontWeight: 600, padding: "0.5em 0.7em", cursor: "pointer" }}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
height="1em"
|
||||
width="1em"
|
||||
viewBox="0 0 40 40"
|
||||
style={{ verticalAlign: "middle", marginRight: "0.5rem" }}
|
||||
>
|
||||
<g>
|
||||
<path d="m20 0c-11 0-20 9-20 20 0 8.8 5.7 16.3 13.7 19 1 0.2 1.3-0.5 1.3-1 0-0.5 0-2 0-3.7-5.5 1.2-6.7-2.4-6.7-2.4-0.9-2.3-2.2-2.9-2.2-2.9-1.9-1.2 0.1-1.2 0.1-1.2 2 0.1 3.1 2.1 3.1 2.1 1.7 3 4.6 2.1 5.8 1.6 0.2-1.3 0.7-2.2 1.3-2.7-4.5-0.5-9.2-2.2-9.2-9.8 0-2.2 0.8-4 2.1-5.4-0.2-0.5-0.9-2.6 0.2-5.3 0 0 1.7-0.5 5.5 2 1.6-0.4 3.3-0.6 5-0.6 1.7 0 3.4 0.2 5 0.7 3.8-2.6 5.5-2.1 5.5-2.1 1.1 2.8 0.4 4.8 0.2 5.3 1.3 1.4 2.1 3.2 2.1 5.4 0 7.6-4.7 9.3-9.2 9.8 0.7 0.6 1.4 1.9 1.4 3.7 0 2.7 0 4.9 0 5.5 0 0.6 0.3 1.2 1.3 1 8-2.7 13.7-10.2 13.7-19 0-11-9-20-20-20z" />
|
||||
</g>
|
||||
</svg>
|
||||
Sign in with GitHub
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function useLoader(promiseFactory, deps) {
|
||||
const [state, setState] = useState({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
promiseFactory()
|
||||
.then(data => {
|
||||
setState({
|
||||
data,
|
||||
loading: false,
|
||||
error: false
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
setState({
|
||||
loading: false,
|
||||
error
|
||||
});
|
||||
});
|
||||
}, deps);
|
||||
|
||||
return [state.data, state.loading, state.error];
|
||||
}
|
||||
|
||||
export function useLanguageLoader(path) {
|
||||
return useLoader(async () => {
|
||||
const lang = getLanguage(path);
|
||||
await loadLanguage(lang);
|
||||
return lang;
|
||||
}, [path]);
|
||||
}
|
||||
|
||||
export function useCommitsFetcher({ repo, sha, path }) {
|
||||
return useLoader(async () => getCommits(repo, sha, path), [repo, sha, path]);
|
||||
}
|
||||
|
||||
export function useDocumentTitle(title) {
|
||||
useEffect(() => {
|
||||
document.title = title;
|
||||
}, [title]);
|
||||
}
|
||||
|
||||
export function getUrlParams() {
|
||||
const [
|
||||
,
|
||||
owner,
|
||||
reponame,
|
||||
action,
|
||||
sha,
|
||||
...paths
|
||||
] = window.location.pathname.split("/");
|
||||
|
||||
if (action !== "commits" && action !== "blob") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [owner + "/" + reponame, sha, "/" + paths.join("/")];
|
||||
}
|
||||
|
||||
function login() {
|
||||
auth()
|
||||
.then(data => {
|
||||
window.location.reload(false);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
233
src/app.js
Executable file → Normal file
233
src/app.js
Executable file → Normal file
@ -1,91 +1,71 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import React from "react";
|
||||
import History from "./history";
|
||||
import { getHistory, auth, isLoggedIn } from "./github";
|
||||
import demo from "./demo.gif";
|
||||
import Landing from "./landing";
|
||||
import {
|
||||
getUrlParams,
|
||||
useLanguageLoader,
|
||||
useCommitsFetcher,
|
||||
useDocumentTitle,
|
||||
Loading,
|
||||
Error
|
||||
} from "./app-helpers";
|
||||
|
||||
console.log(demo);
|
||||
const cli = window._CLI;
|
||||
|
||||
export default function AppWrapper(props) {
|
||||
if (props.repo) {
|
||||
return <App {...props} />;
|
||||
} else {
|
||||
export default function App() {
|
||||
if (cli) {
|
||||
return <CliApp data={cli} />;
|
||||
}
|
||||
|
||||
const [repo, sha, path] = getUrlParams();
|
||||
|
||||
if (!repo) {
|
||||
return <Landing />;
|
||||
} else {
|
||||
return <GitHubApp repo={repo} sha={sha} path={path} />;
|
||||
}
|
||||
}
|
||||
|
||||
function Center({ children }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
height: "100%",
|
||||
padding: "0 40px"
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function CliApp({ data }) {
|
||||
let { commits, path } = data;
|
||||
|
||||
function Landing() {
|
||||
const url = `${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/babel/babel/blob/master/packages/babel-core/test/browserify.js`;
|
||||
return (
|
||||
<Center>
|
||||
<img src={demo} alt="demo" style={{ width: 900, maxWidth: "100%" }} />
|
||||
<h1>Git History</h1>
|
||||
<div>
|
||||
<p>
|
||||
Quickly browse the history of any GitHub file:
|
||||
<ol>
|
||||
<li>
|
||||
Replace <strong>github.com</strong> with{" "}
|
||||
<strong>github.githistory.xyz</strong> in any file url
|
||||
</li>
|
||||
<li>There's no step two</li>
|
||||
</ol>
|
||||
<a href={url}>Try it</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
You can also add a <strong>Open in Git History</strong> button to
|
||||
GitHub with the{" "}
|
||||
<a href="https://chrome.google.com/webstore/detail/github-history-browser-ex/laghnmifffncfonaoffcndocllegejnf">
|
||||
Chrome
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="https://addons.mozilla.org/es/firefox/addon/github-history/">
|
||||
Firefox
|
||||
</a>{" "}
|
||||
extensions.
|
||||
</p>
|
||||
</div>
|
||||
<div style={{ height: "20%" }} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
function App({ repo, sha, path, lang }) {
|
||||
const fileName = path.split("/").pop();
|
||||
useDocumentTitle(`Git History - ${fileName}`);
|
||||
|
||||
const { commits, loading, error } = useCommitsFetcher({
|
||||
repo,
|
||||
sha,
|
||||
path,
|
||||
lang
|
||||
});
|
||||
commits = commits.map(commit => ({ ...commit, date: new Date(commit.date) }));
|
||||
const [lang, loading, error] = useLanguageLoader(path);
|
||||
|
||||
if (error) {
|
||||
return <Error error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading repo={repo} sha={sha} path={path} />;
|
||||
return <Loading path={path} />;
|
||||
}
|
||||
|
||||
return <History commits={commits} language={lang} />;
|
||||
}
|
||||
|
||||
function GitHubApp({ repo, sha, path }) {
|
||||
const fileName = path.split("/").pop();
|
||||
useDocumentTitle(`Git History - ${fileName}`);
|
||||
|
||||
const [lang, langLoading, langError] = useLanguageLoader(path);
|
||||
const [commits, commitsLoading, commitsError] = useCommitsFetcher({
|
||||
repo,
|
||||
sha,
|
||||
path
|
||||
});
|
||||
|
||||
const loading = langLoading || commitsLoading;
|
||||
const error = langError || commitsError;
|
||||
|
||||
if (error) {
|
||||
return <Error error={error} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading repo={repo} path={path} />;
|
||||
}
|
||||
|
||||
if (!commits.length) {
|
||||
@ -94,114 +74,3 @@ function App({ repo, sha, path, lang }) {
|
||||
|
||||
return <History commits={commits} language={lang} />;
|
||||
}
|
||||
|
||||
function Error({ error }) {
|
||||
if (error.status === 403) {
|
||||
return (
|
||||
<Center>
|
||||
<p>
|
||||
GitHub API rate limit exceeded for your IP (60 requests per hour).
|
||||
</p>
|
||||
<p>Sign in with GitHub for more:</p>
|
||||
<GitHubButton onClick={login} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
if (error.status === 404) {
|
||||
return (
|
||||
<Center>
|
||||
<p>File not found.</p>
|
||||
{!isLoggedIn() && (
|
||||
<React.Fragment>
|
||||
<p>Is it from a private repo? Sign in with GitHub:</p>
|
||||
<GitHubButton onClick={login} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
console.error(error);
|
||||
return (
|
||||
<Center>
|
||||
<p>Unexpected error. Check the console.</p>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
function Loading({ repo, sha, path }) {
|
||||
return (
|
||||
<Center>
|
||||
<p>
|
||||
Loading <strong>{repo}</strong> <strong>{path} history...</strong>
|
||||
</p>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
function GitHubButton({ onClick }) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
style={{ fontWeight: 600, padding: "0.5em 0.7em", cursor: "pointer" }}
|
||||
>
|
||||
<div>
|
||||
<svg
|
||||
fill="currentColor"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
height="1em"
|
||||
width="1em"
|
||||
viewBox="0 0 40 40"
|
||||
style={{ verticalAlign: "middle", marginRight: "0.5rem" }}
|
||||
>
|
||||
<g>
|
||||
<path d="m20 0c-11 0-20 9-20 20 0 8.8 5.7 16.3 13.7 19 1 0.2 1.3-0.5 1.3-1 0-0.5 0-2 0-3.7-5.5 1.2-6.7-2.4-6.7-2.4-0.9-2.3-2.2-2.9-2.2-2.9-1.9-1.2 0.1-1.2 0.1-1.2 2 0.1 3.1 2.1 3.1 2.1 1.7 3 4.6 2.1 5.8 1.6 0.2-1.3 0.7-2.2 1.3-2.7-4.5-0.5-9.2-2.2-9.2-9.8 0-2.2 0.8-4 2.1-5.4-0.2-0.5-0.9-2.6 0.2-5.3 0 0 1.7-0.5 5.5 2 1.6-0.4 3.3-0.6 5-0.6 1.7 0 3.4 0.2 5 0.7 3.8-2.6 5.5-2.1 5.5-2.1 1.1 2.8 0.4 4.8 0.2 5.3 1.3 1.4 2.1 3.2 2.1 5.4 0 7.6-4.7 9.3-9.2 9.8 0.7 0.6 1.4 1.9 1.4 3.7 0 2.7 0 4.9 0 5.5 0 0.6 0.3 1.2 1.3 1 8-2.7 13.7-10.2 13.7-19 0-11-9-20-20-20z" />
|
||||
</g>
|
||||
</svg>
|
||||
Sign in with GitHub
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function login() {
|
||||
auth()
|
||||
.then(data => {
|
||||
window.location.reload(false);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
|
||||
function useCommitsFetcher({ repo, sha, path, lang }) {
|
||||
const [state, setState] = useState({
|
||||
commits: null,
|
||||
loading: true,
|
||||
error: null
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
getHistory(repo, sha, path, lang)
|
||||
.then(commits => {
|
||||
setState({
|
||||
commits,
|
||||
loading: false,
|
||||
error: false
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
setState({
|
||||
loading: false,
|
||||
error
|
||||
});
|
||||
});
|
||||
}, [repo, sha, path, lang]);
|
||||
|
||||
return state;
|
||||
}
|
||||
|
||||
function useDocumentTitle(title) {
|
||||
useEffect(() => {
|
||||
document.title = title;
|
||||
}, [title]);
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
import netlify from "netlify-auth-providers";
|
||||
import { Base64 } from "js-base64";
|
||||
import { getLanguageDependencies } from "./language-detector";
|
||||
const TOKEN_KEY = "github-token";
|
||||
|
||||
function getHeaders() {
|
||||
@ -26,7 +25,7 @@ async function getContent(repo, sha, path) {
|
||||
return { content, url: contentJson.html_url };
|
||||
}
|
||||
|
||||
async function getCommits(repo, sha, path, top = 10) {
|
||||
export async function getCommits(repo, sha, path, top = 10) {
|
||||
const commitsResponse = await fetch(
|
||||
`https://api.github.com/repos/${repo}/commits?sha=${sha}&path=${path}`,
|
||||
{ headers: getHeaders() }
|
||||
@ -65,12 +64,6 @@ async function getCommits(repo, sha, path, top = 10) {
|
||||
return commits;
|
||||
}
|
||||
|
||||
export function getHistory(repo, sha, path, lang) {
|
||||
return Promise.all([getCommits(repo, sha, path), loadLanguage(lang)]).then(
|
||||
([commits]) => commits
|
||||
);
|
||||
}
|
||||
|
||||
export function auth() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var authenticator = new netlify({
|
||||
@ -88,21 +81,3 @@ export function auth() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadLanguage(lang) {
|
||||
if (["js", "css", "html"].includes(lang)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const deps = getLanguageDependencies(lang);
|
||||
|
||||
let depPromise = import("prismjs");
|
||||
|
||||
if (deps) {
|
||||
depPromise = depPromise.then(() =>
|
||||
Promise.all(deps.map(dep => import(`prismjs/components/prism-${dep}`)))
|
||||
);
|
||||
}
|
||||
|
||||
return depPromise.then(() => import(`prismjs/components/prism-${lang}`));
|
||||
}
|
||||
|
@ -28,13 +28,15 @@ function CommitInfo({ commit, move, onClick }) {
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<img
|
||||
src={commit.author.avatar}
|
||||
alt={commit.author.login}
|
||||
height={40}
|
||||
width={40}
|
||||
style={{ borderRadius: "4px" }}
|
||||
/>
|
||||
{commit.author.avatar && (
|
||||
<img
|
||||
src={commit.author.avatar}
|
||||
alt={commit.author.login}
|
||||
height={40}
|
||||
width={40}
|
||||
style={{ borderRadius: "4px" }}
|
||||
/>
|
||||
)}
|
||||
<div style={{ paddingLeft: "6px" }}>
|
||||
<div style={{ fontSize: "1.1rem", fontWeight: "500" }}>
|
||||
{commit.author.login}
|
||||
|
23
src/index.js
23
src/index.js
@ -1,27 +1,6 @@
|
||||
import { getLanguage } from "./language-detector";
|
||||
import App from "./app";
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
const [repo, sha, path] = getParams();
|
||||
const lang = getLanguage(path);
|
||||
const root = document.getElementById("root");
|
||||
|
||||
ReactDOM.render(<App repo={repo} sha={sha} path={path} lang={lang} />, root);
|
||||
|
||||
function getParams() {
|
||||
const [
|
||||
,
|
||||
owner,
|
||||
reponame,
|
||||
action,
|
||||
sha,
|
||||
...paths
|
||||
] = window.location.pathname.split("/");
|
||||
|
||||
if (action !== "commits" && action !== "blob") {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [owner + "/" + reponame, sha, "/" + paths.join("/")];
|
||||
}
|
||||
ReactDOM.render(<App />, root);
|
||||
|
49
src/landing.js
Normal file
49
src/landing.js
Normal file
@ -0,0 +1,49 @@
|
||||
import React from "react";
|
||||
import demo from "./demo.gif";
|
||||
|
||||
export default function Landing() {
|
||||
const url = `${window.location.protocol}//${
|
||||
window.location.host
|
||||
}/babel/babel/blob/master/packages/babel-core/test/browserify.js`;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
padding: "10px 20px 0",
|
||||
boxSizing: "border-box"
|
||||
}}
|
||||
>
|
||||
<img src={demo} alt="demo" style={{ width: 900, maxWidth: "100%" }} />
|
||||
<h1>Git History</h1>
|
||||
<div>
|
||||
<div>
|
||||
Quickly browse the history of any GitHub file:
|
||||
<ol>
|
||||
<li>
|
||||
Replace <strong>github.com</strong> with{" "}
|
||||
<strong>github.githistory.xyz</strong> in any file url
|
||||
</li>
|
||||
<li>There's no step two</li>
|
||||
</ol>
|
||||
<a href={url}>Try it</a>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
You can also add an <strong>Open in Git History</strong> button to
|
||||
GitHub with the{" "}
|
||||
<a href="https://chrome.google.com/webstore/detail/github-history-browser-ex/laghnmifffncfonaoffcndocllegejnf">
|
||||
Chrome
|
||||
</a>{" "}
|
||||
and{" "}
|
||||
<a href="https://addons.mozilla.org/es/firefox/addon/github-history/">
|
||||
Firefox
|
||||
</a>{" "}
|
||||
extensions.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -68,3 +68,21 @@ const dependencies = {
|
||||
export function getLanguageDependencies(lang) {
|
||||
return dependencies[lang];
|
||||
}
|
||||
|
||||
export function loadLanguage(lang) {
|
||||
if (["js", "css", "html"].includes(lang)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const deps = getLanguageDependencies(lang);
|
||||
|
||||
let depPromise = import("prismjs");
|
||||
|
||||
if (deps) {
|
||||
depPromise = depPromise.then(() =>
|
||||
Promise.all(deps.map(dep => import(`prismjs/components/prism-${dep}`)))
|
||||
);
|
||||
}
|
||||
|
||||
return depPromise.then(() => import(`prismjs/components/prism-${lang}`));
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user