Merge pull request #69 from pomber/cli

Add CLI
This commit is contained in:
Rodrigo Pombo 2019-02-10 17:53:24 -03:00 committed by GitHub
commit dfea410a01
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 2497 additions and 250 deletions

2
cli/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
site

24
cli/cli.js Executable file
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
{
"name": "githistory",
"name": "githistory-web",
"version": "0.1.0",
"private": true,
"dependencies": {

View File

@ -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>

View File

@ -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
View 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
View 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]);
}

View File

@ -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}`));
}

View File

@ -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}

View File

@ -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
View 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>
);
}

View File

@ -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}`));
}