Repository: pomber/git-history Branch: master Commit: a20f6085cf90 Files: 69 Total size: 100.3 KB Directory structure: gitextract_g0wxqlss/ ├── .github/ │ ├── FUNDING.yml │ └── opencollective.yml ├── .gitignore ├── .storybook/ │ ├── addons.js │ └── config.js ├── .travis.yml ├── cli/ │ ├── .gitignore │ ├── cli.js │ ├── git.js │ ├── package.json │ ├── readme.md │ └── server.js ├── craco.config.js ├── license ├── netlify.toml ├── package.json ├── public/ │ ├── index.html │ └── manifest.json ├── readme.md ├── src/ │ ├── airframe/ │ │ ├── airframe.js │ │ └── easing.js │ ├── animation.js │ ├── app-helpers.js │ ├── app.js │ ├── comment-box.css │ ├── 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 │ ├── 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 ├── jsconfig.json ├── package.json ├── readme.md ├── test/ │ ├── extension.test.js │ └── index.js ├── test-git.js └── vsc-extension-quickstart.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ open_collective: git-history github: [pomber] custom: https://www.paypal.me/pomber ================================================ FILE: .github/opencollective.yml ================================================ collective: git-history ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* /*/site ================================================ FILE: .storybook/addons.js ================================================ import "@storybook/addon-actions/register"; import "@storybook/addon-links/register"; ================================================ FILE: .storybook/config.js ================================================ import { configure } from "@storybook/react"; const req = require.context("../src", true, /\.story\.js$/); function loadStories() { req.keys().forEach(filename => req(filename)); } configure(loadStories, module); ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - "stable" cache: yarn: true ================================================ FILE: cli/.gitignore ================================================ node_modules site ================================================ FILE: cli/cli.js ================================================ #!/usr/bin/env node const runServer = require("./server"); const fs = require("fs"); let path = process.argv[2] || "./."; if (path === "--help") { console.log(`Usage: githistory some/file.ext `); process.exit(); } if (!fs.existsSync(path)) { console.log(`File not found: ${path}`); process.exit(); } runServer(path); ================================================ FILE: cli/git.js ================================================ const execa = require("execa"); const pather = require("path"); async function getCommits(path, last, before) { const format = `{"hash":"%h","author":{"login":"%aN"},"date":"%ad"},`; 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", `--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] })); return before ? result.slice(1) : result; } async function getContent(commit, path) { const { stdout } = await execa( "git", ["show", `${commit.hash}:./${pather.basename(path)}`], { cwd: pather.dirname(path) } ); return stdout; } 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); }) ); return commits; }; ================================================ FILE: cli/package.json ================================================ { "name": "git-file-history", "description": "Quickly browse the history of a file from any git repository", "version": "1.0.1", "repository": "pomber/git-history", "keywords": [ "cli", "git", "file", "history", "log", "commits", "change", "animation", "gui" ], "license": "MIT", "bin": { "git-file-history": "./cli.js", "githistory": "./cli.js", "git-history": "./cli.js" }, "files": [ "site", "*.js" ], "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", "opencollective-postinstall": "^2.0.2", "serve-handler": "^5.0.8", "yargs": "^13.2.2" }, "scripts": { "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", "postinstall": "opencollective-postinstall" }, "devDependencies": { "cross-env": "^5.2.0" }, "collective": { "type": "opencollective", "url": "https://opencollective.com/git-history" } } ================================================ FILE: cli/readme.md ================================================
demo
# 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 git-file-history path/to/file.ext ``` or ```bash $ npm install -g git-file-history $ git-file-history path/to/file.ext ``` ### Sponsors 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)] ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)] ================================================ FILE: cli/server.js ================================================ 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 router = require("koa-router")(); const argv = require("yargs") .usage("Usage: $0 [options]") .describe("port", "Port number (default = 5000)") .default("port", 5000).argv; const sitePath = pather.join(__dirname, "site/"); router.get("/api/commits", async ctx => { const query = ctx.query; const { path, last = 10, before = null } = query; const commits = await getCommits(path, last, before); ctx.body = commits; }); 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" ); }); module.exports = async function runServer(path) { const port = await getPort({ port: argv.port }); app.listen(port); console.log("Running at http://localhost:" + port); open(`http://localhost:${port}/?path=${encodeURIComponent(path)}`); }; ================================================ FILE: craco.config.js ================================================ module.exports = { webpack: { configure: { output: { // I need "this" for workerize-loader globalObject: "this" } } } }; ================================================ FILE: license ================================================ MIT License Copyright (c) 2019 Rodrigo Pombo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: netlify.toml ================================================ [[redirects]] from = "/*" to = "/index.html" status = 200 ================================================ FILE: package.json ================================================ { "name": "githistory-web", "version": "1.0.1", "repository": "pomber/git-history", "private": true, "dependencies": { "@craco/craco": "^3.5.0", "diff": "^4.0.1", "js-base64": "^2.5.1", "netlify-auth-providers": "^1.0.0-alpha5", "opencollective-postinstall": "^2.0.2", "prismjs": "^1.15.0", "react": "^16.8.4", "react-dom": "^16.8.4", "react-scripts": "2.1.3", "react-swipeable": "^4.3.2", "react-use": "^5.2.2", "rebound": "^0.1.0", "workerize-loader": "^1.0.4" }, "scripts": { "start": "craco --openssl-legacy-provider start", "build": "craco build", "format": "prettier --write \"**/*.{js,jsx,md,json,html,css,yml}\" --ignore-path .gitignore", "test-prettier": "prettier --check \"**/*.{js,jsx,md,json,html,css,yml}\" --ignore-path .gitignore", "test-cra": "react-scripts test", "test": "run-s test-prettier test-cra", "eject": "react-scripts eject", "postinstall": "opencollective-postinstall", "storybook": "start-storybook -p 9009 -s public", "build-storybook": "build-storybook -s public" }, "eslintConfig": { "extends": "react-app" }, "browserslist": [ ">0.2%", "not dead", "not ie <= 11", "not op_mini all" ], "devDependencies": { "@babel/core": "^7.3.4", "@storybook/addon-actions": "^4.1.13", "@storybook/addon-links": "^4.1.13", "@storybook/addons": "^4.1.13", "@storybook/react": "^4.1.13", "babel-loader": "8.0.4", "npm-run-all": "^4.1.5", "prettier": "^1.16.4" }, "collective": { "type": "opencollective", "url": "https://opencollective.com/git-history" } } ================================================ FILE: public/index.html ================================================ Git History
================================================ FILE: public/manifest.json ================================================ { "short_name": "Git History", "name": "Git History", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" } ], "start_url": ".", "display": "standalone", "theme_color": "#d6deeb", "background_color": "#011627" } ================================================ FILE: readme.md ================================================
demo
# [Git History](https://githistory.xyz) Quickly browse the history of files in any git repo: 1. Go to a file in **GitHub** (or **GitLab**, or **Bitbucket**) 1. Replace `github.com` with `github.githistory.xyz` 1. There's no step three [Try it](https://github.githistory.xyz/babel/babel/blob/master/packages/babel-core/test/browserify.js) > If you like this project consider [backing my open source work on Patreon!](https://patreon.com/pomber) > And follow [@pomber](https://twitter.com/pomber) on twitter for updates. ## Extensions ### Browsers 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.
Or you can use a bookmarklet. ```javascript javascript: (function() { var url = window.location.href; var regEx = /^(https?\:\/\/)(www\.)?(github|gitlab|bitbucket)\.(com|org)\/(.*)$/i; if (regEx.test(url)) { url = url.replace(regEx, "$1$3.githistory.xyz/$5"); window.open(url, "_blank"); } else { alert("Not a Git File URL"); } })(); ```
### Local Repos 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). ## Support Git History ### Sponsors 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)] selefra ### Backers Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)] ### Thanks

Browser testing via LambdaTest

### Credits Based on these amazing projects: - [Prism](https://github.com/PrismJS/prism) by [Lea Verou](https://twitter.com/leaverou) - [jsdiff](https://github.com/kpdecker/jsdiff) by [Kevin Decker](https://twitter.com/kpdecker) - [Night Owl](https://github.com/sdras/night-owl-vscode-theme) by [Sarah Drasner](https://twitter.com/sarah_edo) ## License MIT ================================================ FILE: src/airframe/airframe.js ================================================ import easing from "./easing"; const MULTIPLY = "multiply"; /* eslint-disable */ function mergeResults(results, composite) { const firstResult = results[0]; if (results.length < 2) { return firstResult; } if (Array.isArray(firstResult)) { return firstResult.map((_, i) => { return mergeResults(results.map(result => result[i]), composite); }); } else { const merged = Object.assign({}, ...results); if (composite === MULTIPLY) { const opacities = results.map(x => x.opacity).filter(x => x != null); if (opacities.length !== 0) { merged.opacity = opacities.reduce((a, b) => a * b); } } return merged; } } const airframe = { parallel: ({ children: fns }) => { return (t, ...args) => { const styles = fns.map(fn => fn(t, ...args)); const result = mergeResults(styles, MULTIPLY); return result; }; }, chain: ({ children: fns, durations }) => { return (t, ...args) => { let style = fns[0](0, ...args); let lowerDuration = 0; for (let i = 0; i < fns.length; i++) { const fn = fns[i]; const thisDuration = durations[i]; const upperDuration = lowerDuration + thisDuration; if (lowerDuration <= t && t <= upperDuration) { const innerT = (t - lowerDuration) / thisDuration; style = mergeResults([style, fn(innerT, ...args)]); } else if (upperDuration < t) { // merge the end of previous animation style = mergeResults([style, fn(1, ...args)]); } else if (t < lowerDuration) { // merge the start of future animation style = mergeResults([fn(0, ...args), style]); } lowerDuration = upperDuration; } return style; }; }, delay: () => () => ({}), tween: ({ from, to, ease = easing.linear }) => (t, targets) => { const style = {}; Object.keys(from).forEach(key => { const value = from[key] + (to[key] - from[key]) * ease(t); if (key === "x") { style["transform"] = `translateX(${value}px)`; } else { style[key] = value; } }); return style; } }; /* @jsx createAnimation */ export const Stagger = props => (t, targets) => { const filter = target => !props.filter || props.filter(target); const interval = targets.filter(filter).length < 2 ? 0 : props.interval / (targets.filter(filter).length - 1); let i = 0; return targets.map(target => { // console.log(target, props.filter(target)); if (!filter(target)) { return {}; } const animation = ( {props.children[0]} ); i++; const result = animation(t, target); // console.log("Stagger Result", t, result); return result; }); }; export function createAnimation(type, props, ...children) { const allProps = Object.assign({ children }, props); if (typeof type === "string") { if (window.LOG === "verbose") { return (t, ...args) => { console.groupCollapsed(type, t); const result = airframe[type](allProps)(t, ...args); console.log(result); console.groupEnd(); return result; }; } else { return airframe[type](allProps); } } else { if (window.LOG === "verbose") { return (t, ...args) => { console.groupCollapsed(type.name, t); const result = type(allProps)(t, ...args); console.log(result); console.groupEnd(); return result; }; } else { return type(allProps); } } } ================================================ FILE: src/airframe/easing.js ================================================ export default { // no easing, no acceleration linear: function(t) { return t; }, // accelerating from zero velocity easeInQuad: function(t) { return t * t; }, // decelerating to zero velocity easeOutQuad: function(t) { return t * (2 - t); }, // acceleration until halfway, then deceleration easeInOutQuad: function(t) { return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; }, // accelerating from zero velocity easeInCubic: function(t) { return t * t * t; }, // decelerating to zero velocity easeOutCubic: function(t) { return --t * t * t + 1; }, // acceleration until halfway, then deceleration easeInOutCubic: function(t) { return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1; }, // accelerating from zero velocity easeInQuart: function(t) { return t * t * t * t; }, // decelerating to zero velocity easeOutQuart: function(t) { return 1 - --t * t * t * t; }, // acceleration until halfway, then deceleration easeInOutQuart: function(t) { return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t; }, // accelerating from zero velocity easeInQuint: function(t) { return t * t * t * t * t; }, // decelerating to zero velocity easeOutQuint: function(t) { return 1 + --t * t * t * t * t; }, // acceleration until halfway, then deceleration easeInOutQuint: function(t) { return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t; } }; ================================================ FILE: src/animation.js ================================================ /* eslint-disable */ import { createAnimation, Stagger } from "./airframe/airframe"; import easing from "./airframe/easing"; const dx = 250; const offOpacity = 0.6; /* @jsx createAnimation */ // window.LOG = "verbose"; const SlideToLeft = () => ( ); function ShrinkHeight() { return ( ); } const SlideFromRight = () => ( ); function GrowHeight() { return ( ); } function SwitchLines({ filterExit, filterEnter, filterFadeOut }) { return ( !filterEnter(l) && !filterFadeOut(l)}> ); } export default ( line.left && !line.middle} filterEnter={line => !line.left && line.middle} filterFadeOut={line => false} /> line.middle && !line.right} filterEnter={line => !line.middle && line.right} filterFadeOut={line => !line.left && line.middle} /> ); ================================================ FILE: src/app-helpers.js ================================================ import React, { useEffect } from "react"; export function Center({ children }) { return (
{children}
); } export function Loading({ repo, path }) { return (

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

); } export function Error({ error, gitProvider }) { const { LogInButton } = gitProvider; if (error.status === 403) { // FIX bitbucket uses 403 for private repos return (

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

Sign in with GitHub for more:

); } if (error.status === 404) { return (

File not found.

{gitProvider.isLoggedIn && !gitProvider.isLoggedIn() && (

Is it from a private repo? Sign in:

)}
); } console.error(error); console.error( "Let us know of the error at https://github.com/pomber/git-history/issues" ); return (

Unexpected error. Check the console.

); } export function useDocumentTitle(title) { useEffect(() => { document.title = title; }, [title]); } ================================================ FILE: src/app.js ================================================ import React, { useState, useEffect } from "react"; import History from "./history"; import Landing from "./landing"; import { useDocumentTitle, Loading, Error } from "./app-helpers"; import getGitProvider from "./git-providers/providers"; export default function App() { const gitProvider = getGitProvider(); if (gitProvider.showLanding()) { return ; } else { return ( ); } } function InnerApp({ gitProvider }) { const path = gitProvider.getPath(); const fileName = path.split("/").pop(); useDocumentTitle(`Git History - ${fileName}`); const [versions, loading, error, loadMore] = useVersionsLoader( gitProvider, path ); if (error) { return ; } if (!versions && loading) { return ; } if (!versions.length) { return ; } return ; } function useVersionsLoader(gitProvider) { 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 .getVersions(state.last) .then(data => { setState(old => ({ data, loading: false, error: false, last: old.last, noMore: data.length < old.last })); }) .catch(error => { setState(old => ({ ...old, loading: false, error: error.message || error })); }); }, [state.last]); return [state.data, state.loading, state.error, loadMore]; } ================================================ FILE: src/comment-box.css ================================================ .comment-box { position: relative; border: 2px solid rgb(214, 222, 235, 0.5); color: rgb(214, 222, 235, 0.8); padding: 6px; min-width: 500px; white-space: nowrap; } .comment-box:after, .comment-box:before { bottom: 100%; left: 50%; border: solid transparent; content: " "; height: 0; width: 0; position: absolute; pointer-events: none; } .comment-box:after { border-color: rgba(1, 22, 39, 0); border-bottom-color: rgb(1, 22, 39); border-width: 13px; margin-left: -13px; } .comment-box:before { border-color: rgba(1, 22, 39, 0); border-bottom-color: rgb(214, 222, 235, 0.5); border-width: 14px; margin-left: -14px; margin-bottom: 2px; } ================================================ FILE: src/duotoneLight.js ================================================ // 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; ================================================ FILE: src/git-providers/bitbucket-commit-fetcher.js ================================================ const cache = {}; async function getCommits({ repo, sha, path, last, token }) { if (!cache[path]) { let fields = "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"; // fields = "*.*.*.*.*"; const commitsResponse = await fetch( `https://api.bitbucket.org/2.0/repositories/${repo}/filehistory/${sha}/${path}?fields=${fields}`, { headers: token ? { Authorization: `bearer ${token}` } : {} } ); if (!commitsResponse.ok) { throw { status: commitsResponse.status === 403 ? 404 : commitsResponse.status, body: commitsJson }; } const commitsJson = await commitsResponse.json(); cache[path] = commitsJson.values.map(({ commit }) => ({ sha: commit.hash, date: new Date(commit.date), author: { login: commit.author.user ? commit.author.user.nickname : commit.author.raw, avatar: commit.author.user && commit.author.user.links.avatar.href }, commitUrl: commit.links.html.href, message: 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, token); commit.content = info.content; } }) ); return commits; } async function getContent(repo, sha, path, token) { const contentResponse = await fetch( `https://api.bitbucket.org/2.0/repositories/${repo}/src/${sha}/${path}`, { headers: token ? { Authorization: `bearer ${token}` } : {} } ); if (contentResponse.status === 404) { return { content: "" }; } if (!contentResponse.ok) { throw { status: contentResponse.status, body: await contentResponse.json() }; } const content = await contentResponse.text(); return { content }; } export default { getCommits }; ================================================ FILE: src/git-providers/bitbucket-provider.js ================================================ import netlify from "netlify-auth-providers"; import React from "react"; import versioner from "./versioner"; import { SOURCE } from "./sources"; const TOKEN_KEY = "bitbucket-token"; function isLoggedIn() { return !!window.localStorage.getItem(TOKEN_KEY); } function getUrlParams() { const [, owner, reponame, , sha, ...paths] = window.location.pathname.split( "/" ); if (!sha) { return []; } return [owner + "/" + reponame, sha, paths.join("/")]; } function getPath() { const [, , path] = getUrlParams(); return path; } function showLanding() { const [repo, ,] = getUrlParams(); return !repo; } function logIn() { // return new Promise((resolve, reject) => { var authenticator = new netlify({ site_id: "ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6" }); authenticator.authenticate({ provider: "bitbucket" }, function(err, data) { if (err) { console.error(err); return; } window.localStorage.setItem(TOKEN_KEY, data.token); window.location.reload(false); }); // }); } function LogInButton() { return ( ); } function getParams() { const [repo, sha, path] = getUrlParams(); const token = window.localStorage.getItem(TOKEN_KEY); return { repo, sha, path, token }; } async function getVersions(last) { const params = { ...getParams(), last }; return await versioner.getVersions(SOURCE.BITBUCKET, params); } export default { showLanding, getPath, getVersions, logIn, isLoggedIn, LogInButton }; ================================================ FILE: src/git-providers/cli-commit-fetcher.js ================================================ 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 { getCommits }; ================================================ FILE: src/git-providers/cli-provider.js ================================================ import versioner from "./versioner"; import { SOURCE } from "./sources"; function getPath() { return new URLSearchParams(window.location.search).get("path"); } function showLanding() { return false; } async function getVersions(last) { const params = { path: getPath(), last }; return await versioner.getVersions(SOURCE.CLI, params); } export default { showLanding, getVersions, getPath }; ================================================ FILE: src/git-providers/differ.js ================================================ import { diffLines } from "diff"; import tokenize from "./tokenizer"; const newlineRe = /\r\n|\r|\n/; function myDiff(oldCode, newCode) { const changes = diffLines(oldCode || "", newCode); let oldIndex = -1; return changes.map(({ value, count, removed, added }) => { const lines = value.split(newlineRe); // check if last line is empty, if it is, remove it const lastLine = lines.pop(); if (lastLine) { lines.push(lastLine); } const result = { oldIndex, lines, count, removed, added }; if (!added) { oldIndex += count; } return result; }); } function insert(array, index, elements) { return array.splice(index, 0, ...elements); } function slideDiff(lines, codes, slideIndex, language) { const prevLines = lines.filter(l => l.slides.includes(slideIndex - 1)); const prevCode = codes[slideIndex - 1] || ""; const currCode = codes[slideIndex]; const changes = myDiff(prevCode, currCode); changes.forEach(change => { if (change.added) { const prevLine = prevLines[change.oldIndex]; const addAtIndex = lines.indexOf(prevLine) + 1; const addLines = change.lines.map(content => ({ content, slides: [slideIndex] })); insert(lines, addAtIndex, addLines); } else if (!change.removed) { for (let j = 1; j <= change.count; j++) { prevLines[change.oldIndex + j].slides.push(slideIndex); } } }); const tokenLines = tokenize(currCode, language); const currLines = lines.filter(l => l.slides.includes(slideIndex)); currLines.forEach((line, index) => (line.tokens = tokenLines[index])); } export function parseLines(codes, language) { const lines = []; for (let slideIndex = 0; slideIndex < codes.length; slideIndex++) { slideDiff(lines, codes, slideIndex, language); } return lines; } export function getSlides(codes, language) { // codes are in reverse cronological order const lines = parseLines(codes, language); // console.log("lines", lines); return codes.map((_, slideIndex) => { return lines .map((line, lineIndex) => ({ content: line.content, tokens: line.tokens, left: line.slides.includes(slideIndex + 1), middle: line.slides.includes(slideIndex), right: line.slides.includes(slideIndex - 1), key: lineIndex })) .filter(line => line.middle || line.left || line.right); }); } export function getChanges(lines) { const changes = []; let currentChange = null; let i = 0; const isNewLine = i => !lines[i].left && lines[i].middle; while (i < lines.length) { if (isNewLine(i)) { if (!currentChange) { currentChange = { start: i }; } } else { if (currentChange) { currentChange.end = i - 1; changes.push(currentChange); currentChange = null; } } i++; } if (currentChange) { currentChange.end = i - 1; changes.push(currentChange); currentChange = null; } return changes; } ================================================ FILE: src/git-providers/github-commit-fetcher.js ================================================ import { Base64 } from "js-base64"; const cache = {}; async function getCommits({ repo, sha, path, token, last }) { if (!cache[path]) { const commitsResponse = await fetch( `https://api.github.com/repos/${repo}/commits?sha=${sha}&path=${path}`, { headers: token ? { Authorization: `bearer ${token}` } : {} } ); if (!commitsResponse.ok) { throw { status: commitsResponse.status, body: commitsJson }; } 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, token); commit.content = info.content; commit.fileUrl = info.url; } }) ); return commits; } async function getContent(repo, sha, path, token) { const contentResponse = await fetch( `https://api.github.com/repos/${repo}/contents${path}?ref=${sha}`, { headers: token ? { Authorization: `bearer ${token}` } : {} } ); if (contentResponse.status === 404) { return { content: "" }; } const contentJson = await contentResponse.json(); if (!contentResponse.ok) { throw { status: contentResponse.status, body: contentJson }; } const content = Base64.decode(contentJson.content); return { content, url: contentJson.html_url }; } export default { getCommits }; ================================================ FILE: src/git-providers/github-provider.js ================================================ import netlify from "netlify-auth-providers"; import React from "react"; import versioner from "./versioner"; import { SOURCE } from "./sources"; const TOKEN_KEY = "github-token"; function isLoggedIn() { return !!window.localStorage.getItem(TOKEN_KEY); } 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; } 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); }); // }); } function LogInButton() { return ( ); } function getParams() { const [repo, sha, path] = getUrlParams(); const token = window.localStorage.getItem(TOKEN_KEY); return { repo, sha, path, token }; } async function getVersions(last) { const params = { ...getParams(), last }; return await versioner.getVersions(SOURCE.GITHUB, params); } export default { showLanding, getPath, getParams, getVersions, logIn, isLoggedIn, LogInButton }; ================================================ FILE: src/git-providers/gitlab-commit-fetcher.js ================================================ import { Base64 } from "js-base64"; const cache = {}; async function getCommits({ repo, sha, path, token, last }) { if (!cache[path]) { const commitsResponse = await fetch( `https://gitlab.com/api/v4/projects/${encodeURIComponent( repo )}/repository/commits?path=${encodeURIComponent(path)}&ref_name=${sha}`, { headers: token ? { Authorization: `bearer ${token}` } : {} } ); const commitsJson = await commitsResponse.json(); if (!commitsResponse.ok) { throw { status: commitsResponse.status, body: commitsJson }; } cache[path] = commitsJson.map(commit => ({ sha: commit.id, date: new Date(commit.authored_date), author: { login: commit.author_name }, // commitUrl: commit.html_url, message: commit.title })); } 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, token); commit.content = info.content; } }) ); return commits; } async function getContent(repo, sha, path, token) { const contentResponse = await fetch( `https://gitlab.com/api/v4/projects/${encodeURIComponent( repo )}/repository/files/${encodeURIComponent(path)}?ref=${sha}`, { headers: token ? { Authorization: `bearer ${token}` } : {} } ); if (contentResponse.status === 404) { return { content: "" }; } const contentJson = await contentResponse.json(); if (!contentResponse.ok) { throw { status: contentResponse.status, body: contentJson }; } const content = Base64.decode(contentJson.content); return { content }; } export default { getCommits }; ================================================ FILE: src/git-providers/gitlab-provider.js ================================================ import netlify from "netlify-auth-providers"; import React from "react"; import versioner from "./versioner"; import { SOURCE } from "./sources"; const TOKEN_KEY = "gitlab-token"; function isLoggedIn() { return !!window.localStorage.getItem(TOKEN_KEY); } 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; } function logIn() { // return new Promise((resolve, reject) => { var authenticator = new netlify({ site_id: "ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6" }); authenticator.authenticate({ provider: "gitlab", scope: "api" }, function( err, data ) { if (err) { console.error(err); return; } window.localStorage.setItem(TOKEN_KEY, data.token); window.location.reload(false); }); // }); } function LogInButton() { return ( ); } function getParams() { const [repo, sha, path] = getUrlParams(); const token = window.localStorage.getItem(TOKEN_KEY); return { repo, sha, path, token }; } async function getVersions(last) { const params = { ...getParams(), last }; return await versioner.getVersions(SOURCE.GITLAB, params); } export default { showLanding, getPath, getVersions, logIn, isLoggedIn, LogInButton }; ================================================ FILE: src/git-providers/language-detector.js ================================================ const filenameRegex = [ { lang: "js", regex: /\.js$/i }, { lang: "jsx", regex: /\.jsx$/i }, { lang: "typescript", regex: /\.ts$/i }, { lang: "tsx", regex: /\.tsx$/i }, { lang: "json", regex: /\.json$|.babelrc$/i }, { lang: "yaml", regex: /\.yaml$|.yml$/i }, { lang: "bash", regex: /\.sh$/i }, { lang: "python", regex: /\.py$/i }, { lang: "dart", regex: /\.dart$/i }, { lang: "perl", regex: /\.pl$|.pm$/i }, { lang: "assembly", regex: /\.asm$/i }, { lang: "groovy", regex: /\.groovy$/i }, { lang: "sql", regex: /\.sql$/i }, { lang: "css", regex: /\.css$/i }, { lang: "less", regex: /\.less$/i }, { lang: "scss", regex: /\.scss$/i }, { lang: "ini", regex: /\.ini$|.editorconfig$/i }, { lang: "markup", regex: /\.xml$|\.html$|\.htm$|\.svg$|\.mathml$/i }, { lang: "batch", regex: /\.bat$/i }, { lang: "clojure", regex: /\.clj$/i }, { lang: "coffeescript", regex: /\.coffee$/i }, { lang: "cpp", regex: /\.cpp$|\.cc$/i }, { lang: "csharp", regex: /\.cs$/i }, { lang: "csp", regex: /\.csp$/i }, { lang: "diff", regex: /\.diff$/i }, { lang: "docker", regex: /dockerfile$/i }, { lang: "fsharp", regex: /\.fsharp$/i }, { lang: "go", regex: /\.go$/i }, { lang: "handlebars", regex: /\.hbs$/i }, { lang: "haskell", regex: /\.hs$/i }, { lang: "java", regex: /\.java$/i }, { lang: "kotlin", regex: /\.kt$/i }, { lang: "lua", regex: /\.lua$/i }, { lang: "markdown", regex: /\.md$/i }, { lang: "msdax", regex: /\.msdax$/i }, { lang: "sql", regex: /\.mysql$/i }, { lang: "objective-c", regex: /\.objc$/i }, { lang: "pgsql", regex: /\.pgsql$/i }, { lang: "php", regex: /\.php$/i }, { lang: "postiats", regex: /\.postiats$/i }, { lang: "powershell", regex: /\.ps$/i }, { lang: "pug", regex: /\.pug$/i }, { lang: "r", regex: /\.r$/i }, { lang: "razor", regex: /\.razor$/i }, { lang: "reason", regex: /\.re$/i }, { lang: "ruby", regex: /\.rb$/i }, { lang: "rust", regex: /\.rs$/i }, { lang: "small basic", regex: /\.smallbasic$/i }, { lang: "scala", regex: /\.scala$/i }, { lang: "scheme", regex: /\.scheme$/i }, { lang: "solidity", regex: /\.solidity$/i }, { lang: "st", regex: /\.st$/i }, { lang: "swift", regex: /\.swift$/i }, // { lang: "toml", regex: /\.toml$/i }, { lang: "vb", regex: /\.vb$/i }, { lang: "wasm", regex: /\.wasm$/i }, // fallback { lang: "js", regex: /.*/i } ]; export function getLanguage(filename) { return filenameRegex.find(x => x.regex.test(filename)).lang; } const dependencies = { cpp: ["c"], tsx: ["jsx"], scala: ["java"] }; 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}`)); } ================================================ FILE: src/git-providers/language-detector.test.js ================================================ import { getLanguage, getLanguageDependencies } from "./language-detector"; describe("Can detect language", () => { test("javascript", () => { expect(getLanguage("my-file.js")).toBe("js"); }); test("jsx", () => { expect(getLanguage("my-file.jsx")).toBe("jsx"); }); test("typescript", () => { expect(getLanguage("my-file.ts")).toBe("typescript"); }); test("tsx", () => { expect(getLanguage("my-file.tsx")).toBe("tsx"); }); describe("json:", () => { test("json", () => { expect(getLanguage("my-file.json")).toBe("json"); }); test("babelrc", () => { expect(getLanguage("my-file.babelrc")).toBe("json"); }); }); describe("markup", () => { test("html", () => { expect(getLanguage("my-file.html")).toBe("markup"); }); test("htm", () => { expect(getLanguage("my-file.htm")).toBe("markup"); }); test("svg", () => { expect(getLanguage("my-file.svg")).toBe("markup"); }); test("xml", () => { expect(getLanguage("my-file.xml")).toBe("markup"); }); }); describe("yaml", () => { test("yaml", () => { expect(getLanguage("my-file.yaml")).toBe("yaml"); }); test("yml", () => { expect(getLanguage("my-file.yml")).toBe("yaml"); }); }); test("bash", () => { expect(getLanguage("my-file.sh")).toBe("bash"); }); test("pyhton", () => { expect(getLanguage("my-file.py")).toBe("python"); }); test("dart", () => { expect(getLanguage("my-file.dart")).toBe("dart"); }); describe("perl", () => { test("pl", () => { expect(getLanguage("my-file.pl")).toBe("perl"); }); test("pm", () => { expect(getLanguage("my-file.pm")).toBe("perl"); }); }); test("assembly", () => { expect(getLanguage("my-file.asm")).toBe("assembly"); }); test("groovy", () => { expect(getLanguage("my-file.groovy")).toBe("groovy"); }); test("sql", () => { expect(getLanguage("my-file.sql")).toBe("sql"); }); test("css", () => { expect(getLanguage("my-file.css")).toBe("css"); }); test("less", () => { expect(getLanguage("my-file.less")).toBe("less"); }); test("scss", () => { expect(getLanguage("my-file.scss")).toBe("scss"); }); describe("ini", () => { test("ini", () => { expect(getLanguage("my-file.ini")).toBe("ini"); }); test("editorconfig", () => { expect(getLanguage("my-file.editorconfig")).toBe("ini"); }); }); test("bat", () => { expect(getLanguage("my-file.bat")).toBe("batch"); }); test("clojure", () => { expect(getLanguage("my-file.clj")).toBe("clojure"); }); test("coffeescript", () => { expect(getLanguage("my-file.coffee")).toBe("coffeescript"); }); test("clojure", () => { expect(getLanguage("my-file.clj")).toBe("clojure"); }); describe("cpp", () => { test("cpp", () => { expect(getLanguage("my-file.cpp")).toBe("cpp"); }); test("cc", () => { expect(getLanguage("my-file.cc")).toBe("cpp"); }); }); test("csharp", () => { expect(getLanguage("my-file.cs")).toBe("csharp"); }); test("csp", () => { expect(getLanguage("my-file.csp")).toBe("csp"); }); test("diff", () => { expect(getLanguage("my-file.diff")).toBe("diff"); }); describe("docker", () => { test("long dockerfile", () => { expect(getLanguage("my-file.dockerfile")).toBe("docker"); }); test("dockerfile", () => { expect(getLanguage("Dockerfile")).toBe("docker"); }); }); test("fsharp", () => { expect(getLanguage("my-file.fsharp")).toBe("fsharp"); }); test("go", () => { expect(getLanguage("my-file.go")).toBe("go"); }); test("haskell", () => { expect(getLanguage("my-file.hs")).toBe("haskell"); }); test("java", () => { expect(getLanguage("my-file.java")).toBe("java"); }); test("kotlin", () => { expect(getLanguage("my-file.kt")).toBe("kotlin"); }); test("lua", () => { expect(getLanguage("my-file.lua")).toBe("lua"); }); test("markdown", () => { expect(getLanguage("my-file.md")).toBe("markdown"); }); test("msdax", () => { expect(getLanguage("my-file.msdax")).toBe("msdax"); }); test("sql", () => { expect(getLanguage("my-file.mysql")).toBe("sql"); }); test("objective-c", () => { expect(getLanguage("my-file.objc")).toBe("objective-c"); }); test("pgsql", () => { expect(getLanguage("my-file.pgsql")).toBe("pgsql"); }); test("php", () => { expect(getLanguage("my-file.php")).toBe("php"); }); test("postiats", () => { expect(getLanguage("my-file.postiats")).toBe("postiats"); }); test("powershell", () => { expect(getLanguage("my-file.ps")).toBe("powershell"); }); test("pug", () => { expect(getLanguage("my-file.pug")).toBe("pug"); }); test("r", () => { expect(getLanguage("my-file.r")).toBe("r"); }); test("razor", () => { expect(getLanguage("my-file.razor")).toBe("razor"); }); test("reason", () => { expect(getLanguage("my-file.re")).toBe("reason"); }); test("ruby", () => { expect(getLanguage("my-file.rb")).toBe("ruby"); }); test("rust", () => { expect(getLanguage("my-file.rs")).toBe("rust"); }); test("small basic", () => { expect(getLanguage("my-file.smallbasic")).toBe("small basic"); }); test("scala", () => { expect(getLanguage("my-file.scala")).toBe("scala"); }); test("scheme", () => { expect(getLanguage("my-file.scheme")).toBe("scheme"); }); test("solidity", () => { expect(getLanguage("my-file.solidity")).toBe("solidity"); }); test("swift", () => { expect(getLanguage("my-file.swift")).toBe("swift"); }); test("vb", () => { expect(getLanguage("my-file.vb")).toBe("vb"); }); test("wasm", () => { expect(getLanguage("my-file.wasm")).toBe("wasm"); }); }); describe("Fallback scenarios", () => { test("Random file extension", () => { expect(getLanguage("my-file.nonsense")).toBe("js"); }); test("No file extension", () => { expect(getLanguage("my-file")).toBe("js"); }); test("Empty string", () => { expect(getLanguage("")).toBe("js"); }); }); describe("Dependencies", () => { test("tsx", () => { expect(getLanguageDependencies("tsx")).toEqual(["jsx"]); }); test("cpp", () => { expect(getLanguageDependencies("cpp")).toEqual(["c"]); }); }); ================================================ FILE: src/git-providers/providers.js ================================================ const { SOURCE, getSource } = require("./sources"); let providers; if (process.env.REACT_APP_GIT_PROVIDER === SOURCE.VSCODE) { // We can't use web workers on vscode webview providers = { [SOURCE.VSCODE]: require("./vscode-provider").default }; } else { providers = { [SOURCE.CLI]: require("./cli-provider").default, [SOURCE.GITLAB]: require("./gitlab-provider").default, [SOURCE.GITHUB]: require("./github-provider").default, [SOURCE.BITBUCKET]: require("./bitbucket-provider").default }; } export default function getGitProvider(source) { source = source || getSource(); const provider = providers[source]; return provider; } ================================================ FILE: src/git-providers/sources.js ================================================ export const SOURCE = { GITHUB: "github", GITLAB: "gitlab", BITBUCKET: "bitbucket", CLI: "cli", VSCODE: "vscode" }; export function getSource() { if (process.env.REACT_APP_GIT_PROVIDER) return process.env.REACT_APP_GIT_PROVIDER; const [cloud] = window.location.host.split("."); if ([SOURCE.GITLAB, SOURCE.GITHUB, SOURCE.BITBUCKET].includes(cloud)) { return cloud; } const source = new URLSearchParams(window.location.search).get("source"); return source || SOURCE.GITHUB; } ================================================ FILE: src/git-providers/tokenizer.js ================================================ // https://github.com/PrismJS/prism/issues/1303#issuecomment-375353987 global.Prism = { disableWorkerMessageHandler: true }; const Prism = require("prismjs"); const newlineRe = /\r\n|\r|\n/; // Take a list of nested tokens // (token.content may contain an array of tokens) // and flatten it so content is always a string // and type the type of the leaf function flattenTokens(tokens) { const flatList = []; tokens.forEach(token => { if (Array.isArray(token.content)) { flatList.push(...flattenTokens(token.content)); } else { flatList.push(token); } }); return flatList; } // Convert strings to tokens function tokenizeStrings(prismTokens, parentType = "plain") { return prismTokens.map(pt => typeof pt === "string" ? { type: parentType, content: pt } : { type: pt.type, content: Array.isArray(pt.content) ? tokenizeStrings(pt.content, pt.type) : pt.content } ); } export default function tokenize(code, language = "javascript") { const prismTokens = Prism.tokenize(code, Prism.languages[language]); const nestedTokens = tokenizeStrings(prismTokens); const tokens = flattenTokens(nestedTokens); let currentLine = []; const lines = [currentLine]; tokens.forEach(token => { const contentLines = token.content.split(newlineRe); const firstContent = contentLines.shift(); if (firstContent !== "") { currentLine.push({ type: token.type, content: firstContent }); } contentLines.forEach(content => { currentLine = []; lines.push(currentLine); if (content !== "") { currentLine.push({ type: token.type, content }); } }); }); return lines; } ================================================ FILE: src/git-providers/versioner.js ================================================ /* eslint-disable import/no-webpack-loader-syntax */ import worker from "workerize-loader!./versioner.worker"; let versioner = worker(); export default versioner; ================================================ FILE: src/git-providers/versioner.worker.js ================================================ import { getLanguage, loadLanguage } from "./language-detector"; import { getSlides, getChanges } from "./differ"; import github from "./github-commit-fetcher"; import gitlab from "./gitlab-commit-fetcher"; import bitbucket from "./bitbucket-commit-fetcher"; import cli from "./cli-commit-fetcher"; import { SOURCE } from "./sources"; const fetchers = { [SOURCE.GITHUB]: github.getCommits, [SOURCE.GITLAB]: gitlab.getCommits, [SOURCE.BITBUCKET]: bitbucket.getCommits, [SOURCE.CLI]: cli.getCommits }; export async function getVersions(source, params) { const { path } = params; const lang = getLanguage(path); const langPromise = loadLanguage(lang); const getCommits = fetchers[source]; const commits = await getCommits(params); await langPromise; const codes = commits.map(commit => commit.content); const slides = getSlides(codes, lang); return commits.map((commit, i) => ({ commit, lines: slides[i], changes: getChanges(slides[i]) })); } ================================================ FILE: src/git-providers/vscode-provider.js ================================================ import { getLanguage, loadLanguage } from "./language-detector"; import { getSlides, getChanges } from "./differ"; 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 } }); }); } async function getVersions(last) { const path = getPath(); const lang = getLanguage(path); const langPromise = loadLanguage(lang); const commits = await getCommits(path, last); await langPromise; const codes = commits.map(commit => commit.content); const slides = getSlides(codes, lang); return commits.map((commit, i) => ({ commit, lines: slides[i], changes: getChanges(slides[i]) })); } export default { showLanding, getPath, getVersions }; ================================================ FILE: src/history.js ================================================ import React, { useEffect, useState } from "react"; import useSpring from "react-use/lib/useSpring"; import Swipeable from "react-swipeable"; import Slide from "./slide"; import "./comment-box.css"; function CommitInfo({ commit, move, onClick }) { const message = commit.message.split("\n")[0].slice(0, 80); const isActive = Math.abs(move) < 0.5; return (
{commit.author.avatar && ( {commit.author.login} )}
{commit.author.login}
{isActive && commit.commitUrl ? ( on {commit.date.toDateString()} ) : ( `on ${commit.date.toDateString()}` )}
{isActive && (
{message} {message !== commit.message ? " ..." : ""}
)}
); } function CommitList({ commits, currentIndex, selectCommit }) { const mouseWheelEvent = e => { e.preventDefault(); selectCommit(currentIndex - (e.deltaX + e.deltaY) / 100); }; return (
{commits.map((commit, commitIndex) => ( selectCommit(commitIndex)} /> ))}
); } export default function History({ versions, loadMore }) { return ; } function Slides({ versions, loadMore }) { const [current, target, setTarget] = useSliderSpring(0); const commits = versions.map(v => v.commit); 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)); useEffect(() => { document.body.onkeydown = function(e) { if (e.keyCode === 39) { nextSlide(); } else if (e.keyCode === 37) { prevSlide(); } else if (e.keyCode === 32) { setClampedTarget(current); } }; }); return ( setClampedTarget(index)} /> ); } // TODO use ./useSpring function useSliderSpring(initial) { const [target, setTarget] = useState(initial); const tension = 0; const friction = 10; const value = useSpring(target, tension, friction); return [Math.round(value * 100) / 100, target, setTarget]; } ================================================ FILE: src/index.js ================================================ import App from "./app"; import React from "react"; import ReactDOM from "react-dom"; const root = document.getElementById("root"); ReactDOM.render( , root ); ================================================ FILE: src/landing.css ================================================ .extensions { display: flex; justify-content: center; padding-bottom: 10px; } .extensions > * { padding: 4px 10px; } .landing a { color: inherit; } .landing { color: #222; background: #fafafa; } .landing > * { background: linear-gradient(rgba(255, 255, 255), rgba(220, 220, 220)); } .landing header { display: flex; padding: 100px 0px; } .landing h1 { margin-top: 10px; } .landing header a { color: rgb(1, 22, 39); } .landing header a.button { background: rgb(1, 22, 39); color: #fafafa; padding: 9px 16px; margin: 10px auto 15px; width: 80px; text-align: center; border-radius: 4px; text-decoration: none; display: block; } .landing header video { margin-right: 115px; } @media (max-width: 1130px) { .landing header video { margin-right: 0px; margin-bottom: 20px; max-width: 80%; height: auto !important; min-width: 350px; } .landing header { flex-direction: column; padding: 40px 0px 20px; } .landing header .summary { width: 560px; max-width: 80%; } } .landing .testimonies { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; display: grid; width: 800px; margin: 10px auto; grid-template-columns: 400px 400px; grid-template-rows: 180px 150px; grid-column-gap: 14px; grid-row-gap: 14px; } @media (max-width: 900px) { .landing .testimonies { grid-template-columns: 400px; grid-template-rows: 180px 180px 150px 150px; width: 400px; } } @media (max-width: 420px) { .landing .testimonies { grid-template-columns: auto; grid-template-rows: auto; width: 90%; } } .landing .testimonies > * { border: 1px solid #e1e8ed; display: inline-block; text-decoration: none; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); border-radius: 5px; } .landing blockquote { display: flex; flex-direction: column; height: 100%; margin: 0; padding: 19px; box-sizing: border-box; } .landing blockquote p { flex: 1; margin: 0 0 12px 0; } .landing blockquote img { height: 36px; width: 36px; border-radius: 50%; } .landing .support > div { width: 800px; } @media (max-width: 900px) { .landing .support > div { width: 600px; } } @media (max-width: 600px) { .landing .support > div { width: 350px; } } ================================================ FILE: src/landing.js ================================================ import React from "react"; import demoMp4 from "./demo.mp4"; import demoWebm from "./demo.webm"; import smashing from "./avatar.smashing.jpg"; import github from "./avatar.github.jpg"; import addy from "./avatar.addy.jpg"; import cssTricks from "./avatar.css-tricks.jpg"; import { ReactComponent as ChromeLogo } from "./icons/chrome.svg"; import { ReactComponent as FirefoxLogo } from "./icons/firefox.svg"; import { ReactComponent as CliLogo } from "./icons/cli.svg"; import { ReactComponent as VsCodeLogo } from "./icons/vscode.svg"; import "./landing.css"; export default function Landing() { const url = `${window.location.protocol}//${ window.location.host }/babel/babel/blob/master/packages/babel-core/test/browserify.js`; return (

Git History

Quickly browse the history of files in any git repo:
  1. Go to a file in GitHub (or{" "} GitLab, or Bitbucket)
  2. Replace github.com with github.githistory.xyz
  3. There's no step three
Try it

Also available as extensions: