mirror of
https://github.com/pomber/git-history.git
synced 2024-09-11 14:35:57 +03:00
commit
a453fc4266
@ -1,12 +1,11 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const runServer = require("./server");
|
||||
const getCommits = require("./git");
|
||||
const fs = require("fs");
|
||||
|
||||
const path = process.argv[2];
|
||||
let path = process.argv[2] || "./.";
|
||||
|
||||
if (!path || path === "--help") {
|
||||
if (path === "--help") {
|
||||
console.log(`Usage:
|
||||
|
||||
githistory some/file.ext
|
||||
@ -19,6 +18,4 @@ if (!fs.existsSync(path)) {
|
||||
process.exit();
|
||||
}
|
||||
|
||||
const commitsPromise = getCommits(path);
|
||||
|
||||
runServer(path, commitsPromise);
|
||||
runServer(path);
|
||||
|
69
cli/git.js
69
cli/git.js
@ -1,47 +1,58 @@
|
||||
const execa = require("execa");
|
||||
const pather = require("path");
|
||||
|
||||
async function getCommits(path) {
|
||||
async function getCommits(path, last, before) {
|
||||
const format = `{"hash":"%h","author":{"login":"%aN"},"date":"%ad"},`;
|
||||
const { stdout } = await execa("git", [
|
||||
"log",
|
||||
// "--follow",
|
||||
"--reverse",
|
||||
`--pretty=format:${format}`,
|
||||
"--date=iso",
|
||||
"--",
|
||||
path
|
||||
]);
|
||||
const { stdout } = await execa(
|
||||
"git",
|
||||
[
|
||||
"log",
|
||||
`--max-count=${before ? last + 1 : last}`,
|
||||
`--pretty=format:${format}`,
|
||||
"--date=iso",
|
||||
`${before || "HEAD"}`,
|
||||
"--",
|
||||
pather.basename(path)
|
||||
],
|
||||
{ cwd: pather.dirname(path) }
|
||||
);
|
||||
const json = `[${stdout.slice(0, -1)}]`;
|
||||
|
||||
const messagesOutput = await execa("git", [
|
||||
"log",
|
||||
// "--follow",
|
||||
"--reverse",
|
||||
`--pretty=format:%s`,
|
||||
"--",
|
||||
path
|
||||
]);
|
||||
const messagesOutput = await execa(
|
||||
"git",
|
||||
[
|
||||
"log",
|
||||
`--max-count=${last}`,
|
||||
`--pretty=format:%s`,
|
||||
`${before || "HEAD"}`,
|
||||
"--",
|
||||
pather.basename(path)
|
||||
],
|
||||
{ cwd: pather.dirname(path) }
|
||||
);
|
||||
|
||||
const messages = messagesOutput.stdout.replace('"', '\\"').split(/\r?\n/);
|
||||
|
||||
const result = JSON.parse(json)
|
||||
.map((commit, i) => ({
|
||||
...commit,
|
||||
date: new Date(commit.date),
|
||||
message: messages[i]
|
||||
}))
|
||||
.slice(-20);
|
||||
const result = JSON.parse(json).map((commit, i) => ({
|
||||
...commit,
|
||||
date: new Date(commit.date),
|
||||
message: messages[i]
|
||||
}));
|
||||
|
||||
return result;
|
||||
return before ? result.slice(1) : result;
|
||||
}
|
||||
|
||||
async function getContent(commit, path) {
|
||||
const { stdout } = await execa("git", ["show", `${commit.hash}:${path}`]);
|
||||
const { stdout } = await execa(
|
||||
"git",
|
||||
["show", `${commit.hash}:./${pather.basename(path)}`],
|
||||
{ cwd: pather.dirname(path) }
|
||||
);
|
||||
return stdout;
|
||||
}
|
||||
|
||||
module.exports = async function(path) {
|
||||
const commits = await getCommits(path);
|
||||
module.exports = async function(path, last, before) {
|
||||
const commits = await getCommits(path, last, before);
|
||||
await Promise.all(
|
||||
commits.map(async commit => {
|
||||
commit.content = await getContent(commit, path);
|
||||
|
@ -27,15 +27,18 @@
|
||||
"dependencies": {
|
||||
"execa": "^1.0.0",
|
||||
"get-port": "^4.1.0",
|
||||
"koa": "^2.7.0",
|
||||
"koa-router": "^7.4.0",
|
||||
"koa-static": "^5.0.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-site": "cd .. && cross-env REACT_APP_GIT_PROVIDER=cli yarn build && rm -fr cli/site/ && cp -r build/ cli/site/",
|
||||
"build": "yarn build-site",
|
||||
"ls-package": "npm pack && tar -xvzf *.tgz && rm -rf package *.tgz"
|
||||
},
|
||||
"devDependencies": {
|
||||
"np": "^4.0.2"
|
||||
"cross-env": "^5.2.0"
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,35 @@
|
||||
const fs = require("fs");
|
||||
const serve = require("koa-static");
|
||||
const Koa = require("koa");
|
||||
const pather = require("path");
|
||||
const getCommits = require("./git");
|
||||
const getPort = require("get-port");
|
||||
const open = require("open");
|
||||
const handler = require("serve-handler");
|
||||
const http = require("http");
|
||||
const pather = require("path");
|
||||
const router = require("koa-router")();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
router.get("/api/commits", async ctx => {
|
||||
const query = ctx.query;
|
||||
const { path, last = 10, before = null } = query;
|
||||
|
||||
const indexPromise = getIndex();
|
||||
const commits = await getCommits(path, last, before);
|
||||
|
||||
const portPromise = getPort({ port: 3000 });
|
||||
ctx.body = commits;
|
||||
});
|
||||
|
||||
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 app = new Koa();
|
||||
app.use(router.routes());
|
||||
app.use(serve(sitePath));
|
||||
app.on("error", err => {
|
||||
console.error("Server error", err);
|
||||
console.error(
|
||||
"Let us know of the error at https://github.com/pomber/git-history/issues"
|
||||
);
|
||||
});
|
||||
|
||||
const port = await portPromise;
|
||||
|
||||
return new Promise(resolve => {
|
||||
server.listen(port, () => {
|
||||
console.log("Running at http://localhost:" + port);
|
||||
open("http://localhost:" + port);
|
||||
});
|
||||
});
|
||||
module.exports = async function runServer(path) {
|
||||
const port = await getPort({ port: 5000 });
|
||||
app.listen(port);
|
||||
console.log("Running at http://localhost:" + port);
|
||||
open(`http://localhost:${port}/?path=${encodeURIComponent(path)}`);
|
||||
};
|
||||
|
1866
cli/yarn.lock
1866
cli/yarn.lock
File diff suppressed because it is too large
Load Diff
@ -59,10 +59,6 @@
|
||||
</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
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { getLanguage, loadLanguage } from "./language-detector";
|
||||
import { auth, isLoggedIn, getCommits } from "./github";
|
||||
|
||||
export function Center({ children }) {
|
||||
return (
|
||||
@ -29,7 +28,7 @@ export function Loading({ repo, path }) {
|
||||
);
|
||||
}
|
||||
|
||||
export function Error({ error }) {
|
||||
export function Error({ error, gitProvider }) {
|
||||
if (error.status === 403) {
|
||||
return (
|
||||
<Center>
|
||||
@ -37,7 +36,7 @@ export function Error({ error }) {
|
||||
GitHub API rate limit exceeded for your IP (60 requests per hour).
|
||||
</p>
|
||||
<p>Sign in with GitHub for more:</p>
|
||||
<GitHubButton onClick={login} />
|
||||
<GitHubButton onClick={gitProvider.logIn} />
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
@ -46,10 +45,10 @@ export function Error({ error }) {
|
||||
return (
|
||||
<Center>
|
||||
<p>File not found.</p>
|
||||
{!isLoggedIn() && (
|
||||
{gitProvider.isLoggedIn && !gitProvider.isLoggedIn() && (
|
||||
<React.Fragment>
|
||||
<p>Is it from a private repo? Sign in with GitHub:</p>
|
||||
<GitHubButton onClick={login} />
|
||||
<GitHubButton onClick={gitProvider.login} />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</Center>
|
||||
@ -92,7 +91,7 @@ function GitHubButton({ onClick }) {
|
||||
);
|
||||
}
|
||||
|
||||
function useLoader(promiseFactory, deps) {
|
||||
export function useLoader(promiseFactory, deps) {
|
||||
const [state, setState] = useState({
|
||||
data: null,
|
||||
loading: true,
|
||||
@ -127,37 +126,8 @@ export function useLanguageLoader(path) {
|
||||
}, [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);
|
||||
}
|
||||
|
101
src/app.js
101
src/app.js
@ -1,76 +1,91 @@
|
||||
import React from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import History from "./history";
|
||||
import Landing from "./landing";
|
||||
import {
|
||||
getUrlParams,
|
||||
useLanguageLoader,
|
||||
useCommitsFetcher,
|
||||
useDocumentTitle,
|
||||
Loading,
|
||||
Error
|
||||
} from "./app-helpers";
|
||||
|
||||
const cli = window._CLI;
|
||||
import getGitProvider from "./git-providers/providers";
|
||||
|
||||
export default function App() {
|
||||
if (cli) {
|
||||
return <CliApp data={cli} />;
|
||||
}
|
||||
const gitProvider = getGitProvider();
|
||||
|
||||
const [repo, sha, path] = getUrlParams();
|
||||
|
||||
if (!repo) {
|
||||
if (gitProvider.showLanding()) {
|
||||
return <Landing />;
|
||||
} else {
|
||||
return <GitHubApp repo={repo} sha={sha} path={path} />;
|
||||
return <InnerApp gitProvider={gitProvider} />;
|
||||
}
|
||||
}
|
||||
|
||||
function CliApp({ data }) {
|
||||
let { commits, path } = data;
|
||||
|
||||
function InnerApp({ gitProvider }) {
|
||||
const path = gitProvider.getPath();
|
||||
const fileName = path.split("/").pop();
|
||||
|
||||
useDocumentTitle(`Git History - ${fileName}`);
|
||||
|
||||
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 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,
|
||||
const [commits, commitsLoading, commitsError, loadMore] = useCommitsLoader(
|
||||
gitProvider,
|
||||
path
|
||||
});
|
||||
);
|
||||
const [lang, langLoading, langError] = useLanguageLoader(path);
|
||||
|
||||
const loading = langLoading || commitsLoading;
|
||||
const error = langError || commitsError;
|
||||
|
||||
if (error) {
|
||||
return <Error error={error} />;
|
||||
return <Error error={error} gitProvider={gitProvider} />;
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading repo={repo} path={path} />;
|
||||
if (!commits && loading) {
|
||||
return <Loading path={path} />;
|
||||
}
|
||||
|
||||
if (!commits.length) {
|
||||
return <Error error={{ status: 404 }} />;
|
||||
return <Error error={{ status: 404 }} gitProvider={gitProvider} />;
|
||||
}
|
||||
|
||||
return <History commits={commits} language={lang} />;
|
||||
return <History commits={commits} language={lang} loadMore={loadMore} />;
|
||||
}
|
||||
|
||||
function useCommitsLoader(gitProvider, path) {
|
||||
const [state, setState] = useState({
|
||||
data: null,
|
||||
loading: true,
|
||||
error: null,
|
||||
last: 10,
|
||||
noMore: false
|
||||
});
|
||||
|
||||
const loadMore = () => {
|
||||
setState(old => {
|
||||
const shouldFetchMore = !old.loading && !old.noMore;
|
||||
return shouldFetchMore
|
||||
? { ...old, last: old.last + 10, loading: true }
|
||||
: old;
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
gitProvider
|
||||
.getCommits(path, state.last)
|
||||
.then(data => {
|
||||
setState(old => ({
|
||||
data,
|
||||
loading: false,
|
||||
error: false,
|
||||
last: old.last,
|
||||
noMore: data.length < old.last
|
||||
}));
|
||||
})
|
||||
.catch(error => {
|
||||
setState({
|
||||
loading: false,
|
||||
error
|
||||
});
|
||||
});
|
||||
}, [path, state.last]);
|
||||
|
||||
return [state.data, state.loading, state.error, loadMore];
|
||||
}
|
||||
|
@ -68,6 +68,7 @@ export function parseLines(codes, language) {
|
||||
}
|
||||
|
||||
export function getSlides(codes, language) {
|
||||
// codes are in reverse cronological order
|
||||
const lines = parseLines(codes, language);
|
||||
// console.log("lines", lines);
|
||||
return codes.map((_, slideIndex) => {
|
||||
@ -75,9 +76,9 @@ export function getSlides(codes, language) {
|
||||
.map((line, lineIndex) => ({
|
||||
content: line.content,
|
||||
tokens: line.tokens,
|
||||
left: line.slides.includes(slideIndex - 1),
|
||||
left: line.slides.includes(slideIndex + 1),
|
||||
middle: line.slides.includes(slideIndex),
|
||||
right: line.slides.includes(slideIndex + 1),
|
||||
right: line.slides.includes(slideIndex - 1),
|
||||
key: lineIndex
|
||||
}))
|
||||
.filter(line => line.middle || line.left || line.right);
|
||||
|
106
src/duotoneLight.js
Normal file
106
src/duotoneLight.js
Normal file
@ -0,0 +1,106 @@
|
||||
// Duotone Light
|
||||
// Author: Simurai, adapted from DuoTone themes for Atom (http://simurai.com/projects/2016/01/01/duotone-themes)
|
||||
// Conversion: Bram de Haan (http://atelierbram.github.io/Base2Tone-prism/output/prism/prism-base2tone-evening-dark.css)
|
||||
// Generated with Base16 Builder (https://github.com/base16-builder/base16-builder)
|
||||
|
||||
var theme /*: PrismTheme */ = {
|
||||
plain: {
|
||||
backgroundColor: "#faf8f5",
|
||||
color: "#728fcb"
|
||||
},
|
||||
styles: [
|
||||
{
|
||||
types: ["comment", "prolog", "doctype", "cdata", "punctuation"],
|
||||
style: {
|
||||
color: "#b6ad9a"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["namespace"],
|
||||
style: {
|
||||
opacity: 0.7
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["tag", "operator", "number"],
|
||||
style: {
|
||||
color: "#063289"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["property", "function"],
|
||||
style: {
|
||||
color: "#b29762"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["tag-id", "selector", "atrule-id"],
|
||||
style: {
|
||||
color: "#2d2006"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["attr-name"],
|
||||
style: {
|
||||
color: "#896724"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: [
|
||||
"boolean",
|
||||
"string",
|
||||
"entity",
|
||||
"url",
|
||||
"attr-value",
|
||||
"keyword",
|
||||
"control",
|
||||
"directive",
|
||||
"unit",
|
||||
"statement",
|
||||
"regex",
|
||||
"at-rule"
|
||||
],
|
||||
style: {
|
||||
color: "#728fcb"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["placeholder", "variable"],
|
||||
style: {
|
||||
color: "#93abdc"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["deleted"],
|
||||
style: {
|
||||
textDecorationLine: "line-through"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["inserted"],
|
||||
style: {
|
||||
textDecorationLine: "underline"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["italic"],
|
||||
style: {
|
||||
fontStyle: "italic"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["important", "bold"],
|
||||
style: {
|
||||
fontWeight: "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
types: ["important"],
|
||||
style: {
|
||||
color: "#896724"
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
module.exports = theme;
|
24
src/git-providers/cli-provider.js
Normal file
24
src/git-providers/cli-provider.js
Normal file
@ -0,0 +1,24 @@
|
||||
function getPath() {
|
||||
return new URLSearchParams(window.location.search).get("path");
|
||||
}
|
||||
|
||||
function showLanding() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async function getCommits(path, last) {
|
||||
// TODO cache
|
||||
const response = await fetch(
|
||||
`/api/commits?path=${encodeURIComponent(path)}&last=${last}`
|
||||
);
|
||||
const commits = await response.json();
|
||||
commits.forEach(c => (c.date = new Date(c.date)));
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
export default {
|
||||
showLanding,
|
||||
getPath,
|
||||
getCommits
|
||||
};
|
124
src/git-providers/github-provider.js
Normal file
124
src/git-providers/github-provider.js
Normal file
@ -0,0 +1,124 @@
|
||||
import netlify from "netlify-auth-providers";
|
||||
import { Base64 } from "js-base64";
|
||||
const TOKEN_KEY = "github-token";
|
||||
|
||||
function getHeaders() {
|
||||
const token = window.localStorage.getItem(TOKEN_KEY);
|
||||
return token ? { Authorization: `bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
function isLoggedIn() {
|
||||
return !!window.localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
async function getContent(repo, sha, path) {
|
||||
const contentResponse = await fetch(
|
||||
`https://api.github.com/repos/${repo}/contents${path}?ref=${sha}`,
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
|
||||
if (!contentResponse.ok) {
|
||||
throw contentResponse;
|
||||
}
|
||||
const contentJson = await contentResponse.json();
|
||||
const content = Base64.decode(contentJson.content);
|
||||
return { content, url: contentJson.html_url };
|
||||
}
|
||||
|
||||
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 getPath() {
|
||||
const [, , path] = getUrlParams();
|
||||
return path;
|
||||
}
|
||||
|
||||
function showLanding() {
|
||||
const [repo, ,] = getUrlParams();
|
||||
return !repo;
|
||||
}
|
||||
|
||||
const cache = {};
|
||||
|
||||
async function getCommits(path, last) {
|
||||
const [repo, sha] = getUrlParams();
|
||||
|
||||
if (!cache[path]) {
|
||||
const commitsResponse = await fetch(
|
||||
`https://api.github.com/repos/${repo}/commits?sha=${sha}&path=${path}`,
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
if (!commitsResponse.ok) {
|
||||
throw commitsResponse;
|
||||
}
|
||||
const commitsJson = await commitsResponse.json();
|
||||
|
||||
cache[path] = commitsJson.map(commit => ({
|
||||
sha: commit.sha,
|
||||
date: new Date(commit.commit.author.date),
|
||||
author: {
|
||||
login: commit.author ? commit.author.login : commit.commit.author.name,
|
||||
avatar: commit.author
|
||||
? commit.author.avatar_url
|
||||
: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
|
||||
},
|
||||
commitUrl: commit.html_url,
|
||||
message: commit.commit.message
|
||||
}));
|
||||
}
|
||||
|
||||
const commits = cache[path].slice(0, last);
|
||||
|
||||
await Promise.all(
|
||||
commits.map(async commit => {
|
||||
if (!commit.content) {
|
||||
const info = await getContent(repo, commit.sha, path);
|
||||
commit.content = info.content;
|
||||
commit.fileUrl = info.url;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
function logIn() {
|
||||
// return new Promise((resolve, reject) => {
|
||||
var authenticator = new netlify({
|
||||
site_id: "ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6"
|
||||
});
|
||||
authenticator.authenticate({ provider: "github", scope: "repo" }, function(
|
||||
err,
|
||||
data
|
||||
) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
window.localStorage.setItem(TOKEN_KEY, data.token);
|
||||
window.location.reload(false);
|
||||
});
|
||||
// });
|
||||
}
|
||||
|
||||
export default {
|
||||
showLanding,
|
||||
getPath,
|
||||
getCommits,
|
||||
logIn,
|
||||
isLoggedIn
|
||||
};
|
14
src/git-providers/providers.js
Normal file
14
src/git-providers/providers.js
Normal file
@ -0,0 +1,14 @@
|
||||
import cliProvider from "./cli-provider";
|
||||
import githubProvider from "./github-provider";
|
||||
import vscodeProvider from "./vscode-provider";
|
||||
|
||||
export default function getGitProvider() {
|
||||
switch (process.env.REACT_APP_GIT_PROVIDER) {
|
||||
case "cli":
|
||||
return cliProvider;
|
||||
case "vscode":
|
||||
return vscodeProvider;
|
||||
default:
|
||||
return githubProvider;
|
||||
}
|
||||
}
|
37
src/git-providers/vscode-provider.js
Normal file
37
src/git-providers/vscode-provider.js
Normal file
@ -0,0 +1,37 @@
|
||||
const vscode = window.vscode;
|
||||
|
||||
function getPath() {
|
||||
return window._PATH;
|
||||
}
|
||||
|
||||
function showLanding() {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getCommits(path, last) {
|
||||
return new Promise((resolve, reject) => {
|
||||
window.addEventListener(
|
||||
"message",
|
||||
event => {
|
||||
const commits = event.data;
|
||||
commits.forEach(c => (c.date = new Date(c.date)));
|
||||
resolve(commits);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
|
||||
vscode.postMessage({
|
||||
command: "commits",
|
||||
params: {
|
||||
path,
|
||||
last
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
showLanding,
|
||||
getPath,
|
||||
getCommits
|
||||
};
|
@ -1,83 +0,0 @@
|
||||
import netlify from "netlify-auth-providers";
|
||||
import { Base64 } from "js-base64";
|
||||
const TOKEN_KEY = "github-token";
|
||||
|
||||
function getHeaders() {
|
||||
const token = window.localStorage.getItem(TOKEN_KEY);
|
||||
return token ? { Authorization: `bearer ${token}` } : {};
|
||||
}
|
||||
|
||||
export function isLoggedIn() {
|
||||
return !!window.localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
|
||||
async function getContent(repo, sha, path) {
|
||||
const contentResponse = await fetch(
|
||||
`https://api.github.com/repos/${repo}/contents${path}?ref=${sha}`,
|
||||
{ headers: getHeaders() }
|
||||
);
|
||||
|
||||
if (!contentResponse.ok) {
|
||||
throw contentResponse;
|
||||
}
|
||||
const contentJson = await contentResponse.json();
|
||||
const content = Base64.decode(contentJson.content);
|
||||
return { content, url: contentJson.html_url };
|
||||
}
|
||||
|
||||
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() }
|
||||
);
|
||||
if (!commitsResponse.ok) {
|
||||
throw commitsResponse;
|
||||
}
|
||||
const commitsJson = await commitsResponse.json();
|
||||
|
||||
const commits = commitsJson
|
||||
.slice(0, top)
|
||||
.map(commit => ({
|
||||
sha: commit.sha,
|
||||
date: new Date(commit.commit.author.date),
|
||||
author: {
|
||||
login: commit.author ? commit.author.login : commit.commit.author.name,
|
||||
avatar: commit.author
|
||||
? commit.author.avatar_url
|
||||
: "https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png"
|
||||
},
|
||||
commitUrl: commit.html_url,
|
||||
message: commit.commit.message
|
||||
}))
|
||||
.sort(function(a, b) {
|
||||
return a.date - b.date;
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
commits.map(async commit => {
|
||||
const info = await getContent(repo, commit.sha, path);
|
||||
commit.content = info.content;
|
||||
commit.fileUrl = info.url;
|
||||
})
|
||||
);
|
||||
|
||||
return commits;
|
||||
}
|
||||
|
||||
export function auth() {
|
||||
return new Promise((resolve, reject) => {
|
||||
var authenticator = new netlify({
|
||||
site_id: "ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6"
|
||||
});
|
||||
authenticator.authenticate({ provider: "github", scope: "repo" }, function(
|
||||
err,
|
||||
data
|
||||
) {
|
||||
if (err) {
|
||||
reject(err);
|
||||
}
|
||||
window.localStorage.setItem(TOKEN_KEY, data.token);
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
}
|
@ -69,7 +69,7 @@ function CommitInfo({ commit, move, onClick }) {
|
||||
function CommitList({ commits, currentIndex, selectCommit }) {
|
||||
const mouseWheelEvent = e => {
|
||||
e.preventDefault();
|
||||
selectCommit(currentIndex + (e.deltaX + e.deltaY) / 100);
|
||||
selectCommit(currentIndex - (e.deltaX + e.deltaY) / 100);
|
||||
};
|
||||
return (
|
||||
<div
|
||||
@ -87,7 +87,7 @@ function CommitList({ commits, currentIndex, selectCommit }) {
|
||||
{commits.map((commit, commitIndex) => (
|
||||
<CommitInfo
|
||||
commit={commit}
|
||||
move={commitIndex - currentIndex}
|
||||
move={currentIndex - commitIndex}
|
||||
key={commitIndex}
|
||||
onClick={() => selectCommit(commitIndex)}
|
||||
/>
|
||||
@ -96,20 +96,26 @@ function CommitList({ commits, currentIndex, selectCommit }) {
|
||||
);
|
||||
}
|
||||
|
||||
export default function History({ commits, language }) {
|
||||
export default function History({ commits, language, loadMore }) {
|
||||
const codes = commits.map(commit => commit.content);
|
||||
const slideLines = getSlides(codes, language);
|
||||
return <Slides slideLines={slideLines} commits={commits} />;
|
||||
|
||||
return (
|
||||
<Slides slideLines={slideLines} commits={commits} loadMore={loadMore} />
|
||||
);
|
||||
}
|
||||
|
||||
function Slides({ commits, slideLines }) {
|
||||
const [current, target, setTarget] = useSliderSpring(commits.length - 1);
|
||||
const setClampedTarget = newTarget =>
|
||||
function Slides({ commits, slideLines, loadMore }) {
|
||||
const [current, target, setTarget] = useSliderSpring(0);
|
||||
const setClampedTarget = newTarget => {
|
||||
setTarget(Math.min(commits.length - 0.75, Math.max(-0.25, newTarget)));
|
||||
|
||||
if (newTarget >= commits.length - 5) {
|
||||
loadMore();
|
||||
}
|
||||
};
|
||||
const index = Math.round(current);
|
||||
const nextSlide = () => setClampedTarget(Math.round(target + 0.51));
|
||||
const prevSlide = () => setClampedTarget(Math.round(target - 0.51));
|
||||
const nextSlide = () => setClampedTarget(Math.round(target - 0.51));
|
||||
const prevSlide = () => setClampedTarget(Math.round(target + 0.51));
|
||||
useEffect(() => {
|
||||
document.body.onkeydown = function(e) {
|
||||
if (e.keyCode === 39) {
|
||||
@ -130,7 +136,7 @@ function Slides({ commits, slideLines }) {
|
||||
selectCommit={index => setClampedTarget(index)}
|
||||
/>
|
||||
<Swipeable onSwipedLeft={nextSlide} onSwipedRight={prevSlide}>
|
||||
<Slide time={current - index} lines={slideLines[index]} />
|
||||
<Slide time={index - current} lines={slideLines[index]} />
|
||||
</Swipeable>
|
||||
</React.Fragment>
|
||||
);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import React from "react";
|
||||
import demo from "./demo.gif";
|
||||
import { Center } from "./app-helpers";
|
||||
|
||||
export default function Landing() {
|
||||
const url = `${window.location.protocol}//${
|
||||
@ -74,6 +73,7 @@ export default function Landing() {
|
||||
<img
|
||||
src="https://opencollective.com/git-history/donate/button.png?color=white"
|
||||
width="300"
|
||||
alt="donate"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
@ -43,28 +43,37 @@ function activate(context) {
|
||||
);
|
||||
|
||||
const index = fs.readFileSync(indexPath, "utf-8");
|
||||
const newIndex = index
|
||||
.replace(
|
||||
"<body>",
|
||||
`<body><script>/*<!--*/window.vscode=acquireVsCodeApi();window._PATH='${currentPath}'/*-->*/</script>`
|
||||
)
|
||||
.replace(
|
||||
"<head>",
|
||||
`<head><base href="${vscode.Uri.file(
|
||||
path.join(context.extensionPath, "site")
|
||||
).with({
|
||||
scheme: "vscode-resource"
|
||||
})}/"/>`
|
||||
);
|
||||
|
||||
getCommits(currentPath)
|
||||
.then(commits => {
|
||||
const newIndex = index
|
||||
.replace(
|
||||
"<script>window._CLI=null</script>",
|
||||
`<script>/*<!--*/window._CLI={commits:${JSON.stringify(
|
||||
commits
|
||||
)},path:'${currentPath}'}/*-->*/</script>`
|
||||
)
|
||||
.replace(
|
||||
"<head>",
|
||||
`<head><base href="${vscode.Uri.file(
|
||||
path.join(context.extensionPath, "site")
|
||||
).with({
|
||||
scheme: "vscode-resource"
|
||||
})}/"/>`
|
||||
);
|
||||
panel.webview.html = newIndex;
|
||||
|
||||
panel.webview.html = newIndex;
|
||||
})
|
||||
.catch(console.error);
|
||||
panel.webview.onDidReceiveMessage(
|
||||
message => {
|
||||
switch (message.command) {
|
||||
case "commits":
|
||||
const { path, last = 15, before = null } = message.params;
|
||||
getCommits(path, last, before)
|
||||
.then(commits => {
|
||||
panel.webview.postMessage(commits);
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
},
|
||||
undefined,
|
||||
context.subscriptions
|
||||
);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
throw e;
|
||||
|
@ -1,16 +1,16 @@
|
||||
const execa = require("execa");
|
||||
const pather = require("path");
|
||||
|
||||
async function getCommits(path) {
|
||||
async function getCommits(path, last, before) {
|
||||
const format = `{"hash":"%h","author":{"login":"%aN"},"date":"%ad"},`;
|
||||
const { stdout } = await execa(
|
||||
"git",
|
||||
[
|
||||
"log",
|
||||
// "--follow",
|
||||
"--reverse",
|
||||
`--max-count=${before ? last + 1 : last}`,
|
||||
`--pretty=format:${format}`,
|
||||
"--date=iso",
|
||||
`${before || "HEAD"}`,
|
||||
"--",
|
||||
pather.basename(path)
|
||||
],
|
||||
@ -22,9 +22,9 @@ async function getCommits(path) {
|
||||
"git",
|
||||
[
|
||||
"log",
|
||||
// "--follow",
|
||||
"--reverse",
|
||||
`--max-count=${last}`,
|
||||
`--pretty=format:%s`,
|
||||
`${before || "HEAD"}`,
|
||||
"--",
|
||||
pather.basename(path)
|
||||
],
|
||||
@ -33,15 +33,13 @@ async function getCommits(path) {
|
||||
|
||||
const messages = messagesOutput.stdout.replace('"', '\\"').split(/\r?\n/);
|
||||
|
||||
const result = JSON.parse(json)
|
||||
.map((commit, i) => ({
|
||||
...commit,
|
||||
date: new Date(commit.date),
|
||||
message: messages[i]
|
||||
}))
|
||||
.slice(-20);
|
||||
const result = JSON.parse(json).map((commit, i) => ({
|
||||
...commit,
|
||||
date: new Date(commit.date),
|
||||
message: messages[i]
|
||||
}));
|
||||
|
||||
return result;
|
||||
return before ? result.slice(1) : result;
|
||||
}
|
||||
|
||||
async function getContent(commit, path) {
|
||||
@ -53,8 +51,8 @@ async function getContent(commit, path) {
|
||||
return stdout;
|
||||
}
|
||||
|
||||
module.exports = async function(path) {
|
||||
const commits = await getCommits(path);
|
||||
module.exports = async function(path, last, before) {
|
||||
const commits = await getCommits(path, last, before);
|
||||
await Promise.all(
|
||||
commits.map(async commit => {
|
||||
commit.content = await getContent(commit, path);
|
||||
|
@ -38,7 +38,7 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"build-site": "cd .. && cross-env PUBLIC_URL=. yarn build && rm -fr vscode-ext/site/ && cp -r build/ vscode-ext/site/",
|
||||
"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/",
|
||||
"build": " yarn build-site",
|
||||
"postinstall": "node ./node_modules/vscode/bin/install",
|
||||
"test": "node ./node_modules/vscode/bin/test",
|
||||
|
17
vscode-ext/test-git.js
Normal file
17
vscode-ext/test-git.js
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// node test-git.js extension.js 2 94c91d9
|
||||
|
||||
const getCommits = require("./git");
|
||||
|
||||
const [, , path, last, before] = process.argv;
|
||||
|
||||
getCommits(path, last, before).then(cs =>
|
||||
console.log(
|
||||
cs
|
||||
.map(c => {
|
||||
return `${c.hash} ${c.date.toDateString()} ${c.message}`;
|
||||
})
|
||||
.join("\n")
|
||||
)
|
||||
);
|
Loading…
Reference in New Issue
Block a user