Full Code of pomber/git-history for AI

master a20f6085cf90 cached
69 files
100.3 KB
29.6k tokens
106 symbols
1 requests
Download .txt
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
================================================
<div align="center">
<img alt="demo" src="https://user-images.githubusercontent.com/1911623/52544866-51102b00-2d92-11e9-9d9a-323470c1b0af.gif" />
</div>

# 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)]

<a href="https://opencollective.com/git-history/sponsor/0/website" target="_blank"><img src="https://opencollective.com/git-history/sponsor/0/avatar.svg"></a>

### Backers

Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)]

<a href="https://opencollective.com/git-history#backers" target="_blank"><img src="https://opencollective.com/git-history/backers.svg?width=890"></a>


================================================
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 <some/file.ext> [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
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
    <link rel="preconnect" href="https://api.github.com/" crossorigin />
    <meta
      name="viewport"
      content="width=device-width, initial-scale=1, shrink-to-fit=no"
    />
    <meta name="theme-color" content="#000000" />
    <!--
      manifest.json provides metadata used when your web app is added to the
      homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/
    -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <meta
      name="description"
      content="Quickly browse the history of a file from GitHub, GitLab, Bitbucket or any git repository"
    />
    <!--
      Notice the use of %PUBLIC_URL% in the tags above.
      It will be replaced with the URL of the `public` folder during the build.
      Only files inside the `public` folder can be referenced from the HTML.

      Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
      work correctly both with client-side routing and a non-root public URL.
      Learn how to configure a non-root public URL by running `npm run build`.
    -->
    <title>Git History</title>
    <style>
      html {
        height: 100%;
      }
      body {
        margin: 0;
        padding: 0;
        height: 100%;
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
          "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
          "Helvetica Neue", sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        background-color: rgb(1, 22, 39);
        color: rgb(214, 222, 235);
      }

      #root {
        height: 100%;
      }

      footer {
        position: fixed;
        right: 10px;
        bottom: 8px;
        opacity: 0.4;
        text-align: center;
      }

      a {
        color: rgb(173, 219, 103);
      }
    </style>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>


================================================
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
================================================
<div align="center">
<a href="https://github.githistory.xyz/torvalds/linux/blob/master/kernel/up.c">
<img alt="demo" src="https://user-images.githubusercontent.com/1911623/54575634-9b10b000-49d3-11e9-8a19-56e40636e45d.gif" width="600" />
</a>
</div>

# [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.

<details><summary>Or you can use a bookmarklet.</summary>

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

</details>

### 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)]

<a href="https://github.com/selefra/selefra" target="_blank"><img src="https://github.com/selefra.png" style="border-radius: 50%" alt="selefra" title="Selefra" width="100"></a>

<a href="https://opencollective.com/git-history/sponsor/0/website" target="_blank"><img src="https://opencollective.com/git-history/sponsor/0/avatar.svg"></a>

### Backers

Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)]

<a href="https://opencollective.com/git-history#backers" target="_blank"><img src="https://opencollective.com/git-history/backers.svg?width=890"></a>

### Thanks

<p>Browser testing via <a href="https://www.lambdatest.com/" target="_blank"><picture>
  <source media="(prefers-color-scheme: dark)" srcset="https://www.lambdatest.com/resources/images/logo-white.svg">
  <img alt="LambdaTest" src="https://www.lambdatest.com/resources/images/logos/logo.svg" style="vertical-align: middle;margin-left:5px" width="147" height="26" >
</picture></a></p>

### 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 = (
      <parallel>
        <chain durations={[i * interval, 1 - props.interval]}>
          <delay />
          {props.children[0]}
        </chain>
      </parallel>
    );
    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 = () => (
  <tween
    from={{ x: 0, opacity: 1 }}
    to={{ x: -dx, opacity: 0 }}
    ease={easing.easeInQuad}
  />
);

function ShrinkHeight() {
  return (
    <tween
      from={{ height: 15 }}
      to={{ height: 0 }}
      ease={easing.easeInOutQuad}
    />
  );
}

const SlideFromRight = () => (
  <tween
    from={{ x: dx, opacity: 0 }}
    to={{ x: 0, opacity: 1 }}
    ease={easing.easeOutQuad}
  />
);
function GrowHeight() {
  return (
    <tween
      from={{ height: 0 }}
      to={{ height: 15 }}
      ease={easing.easeInOutQuad}
    />
  );
}

function SwitchLines({ filterExit, filterEnter, filterFadeOut }) {
  return (
    <parallel>
      <Stagger interval={0.2} filter={filterExit}>
        <chain durations={[0.35, 0.3, 0.35]}>
          <SlideToLeft />
          <ShrinkHeight />
        </chain>
      </Stagger>
      <Stagger interval={0.2} filter={filterEnter}>
        <chain durations={[0.35, 0.3, 0.35]}>
          <delay />
          <GrowHeight />
          <SlideFromRight />
        </chain>
      </Stagger>
      <Stagger interval={0} filter={filterEnter}>
        <tween from={{ opacity: offOpacity }} to={{ opacity: 1 }} />
      </Stagger>
      <Stagger interval={0} filter={filterFadeOut}>
        <tween
          from={{ opacity: 1 }}
          to={{ opacity: offOpacity }}
          ease={easing.easeOutCubic}
        />
      </Stagger>
      <Stagger interval={0} filter={l => !filterEnter(l) && !filterFadeOut(l)}>
        <tween from={{ opacity: offOpacity }} to={{ opacity: offOpacity }} />
      </Stagger>
    </parallel>
  );
}

export default (
  <chain durations={[0.5, 0.5]}>
    <SwitchLines
      filterExit={line => line.left && !line.middle}
      filterEnter={line => !line.left && line.middle}
      filterFadeOut={line => false}
    />
    <SwitchLines
      filterExit={line => line.middle && !line.right}
      filterEnter={line => !line.middle && line.right}
      filterFadeOut={line => !line.left && line.middle}
    />
  </chain>
);


================================================
FILE: src/app-helpers.js
================================================
import React, { useEffect } from "react";

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, gitProvider }) {
  const { LogInButton } = gitProvider;
  if (error.status === 403) {
    // FIX bitbucket uses 403 for private repos
    return (
      <Center>
        <p>
          GitHub API rate limit exceeded for your IP (60 requests per hour).
        </p>
        <p>Sign in with GitHub for more:</p>
        <LogInButton />
      </Center>
    );
  }

  if (error.status === 404) {
    return (
      <Center>
        <p>File not found.</p>
        {gitProvider.isLoggedIn && !gitProvider.isLoggedIn() && (
          <React.Fragment>
            <p>Is it from a private repo? Sign in:</p>
            <LogInButton />
          </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>
  );
}

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 <Landing />;
  } else {
    return (
      <React.Fragment>
        <InnerApp gitProvider={gitProvider} />
        <footer>
          <a href="https://github.com/pomber/git-history">Git History</a>
          <br />
          by
          <a href="https://twitter.com/pomber">@pomber</a>
        </footer>
      </React.Fragment>
    );
  }
}

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 <Error error={error} gitProvider={gitProvider} />;
  }

  if (!versions && loading) {
    return <Loading path={path} />;
  }

  if (!versions.length) {
    return <Error error={{ status: 404 }} gitProvider={gitProvider} />;
  }

  return <History versions={versions} loadMore={loadMore} />;
}

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 (
    <button
      onClick={logIn}
      style={{ fontWeight: 600, padding: "0.5em 0.7em", cursor: "pointer" }}
    >
      <div>Sign in with Bitbucket</div>
    </button>
  );
}

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 (
    <button
      onClick={logIn}
      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 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 (
    <button
      onClick={logIn}
      style={{ fontWeight: 600, padding: "0.5em 0.7em", cursor: "pointer" }}
    >
      <div>Sign in with GitLab</div>
    </button>
  );
}

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 (
    <div
      style={{
        position: "absolute",
        left: "50%",
        transform: `translateX(-50%) translateX(${250 * move}px)`,
        opacity: 1 / (1 + Math.min(0.8, Math.abs(move))),
        zIndex: !isActive && 2
      }}
    >
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          cursor: "pointer",
          padding: "5px 0 15px"
        }}
        onClick={onClick}
      >
        {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}
          </div>
          <div style={{ fontSize: "0.85rem", opacity: "0.9" }}>
            {isActive && commit.commitUrl ? (
              <a
                href={commit.commitUrl}
                target="_blank"
                rel="noopener noreferrer"
              >
                on {commit.date.toDateString()}
              </a>
            ) : (
              `on ${commit.date.toDateString()}`
            )}
          </div>
        </div>
      </div>
      {isActive && (
        <div
          className="comment-box"
          title={commit.message}
          style={{ opacity: 1 - 2 * Math.abs(move) }}
        >
          {message}
          {message !== commit.message ? " ..." : ""}
        </div>
      )}
    </div>
  );
}

function CommitList({ commits, currentIndex, selectCommit }) {
  const mouseWheelEvent = e => {
    e.preventDefault();
    selectCommit(currentIndex - (e.deltaX + e.deltaY) / 100);
  };
  return (
    <div
      onWheel={mouseWheelEvent}
      style={{
        overflow: "hidden",
        width: "100%",
        height: "100px",
        position: "fixed",
        top: 0,
        background: "rgb(1, 22, 39)",
        zIndex: 1
      }}
    >
      {commits.map((commit, commitIndex) => (
        <CommitInfo
          commit={commit}
          move={currentIndex - commitIndex}
          key={commitIndex}
          onClick={() => selectCommit(commitIndex)}
        />
      ))}
    </div>
  );
}

export default function History({ versions, loadMore }) {
  return <Slides versions={versions} loadMore={loadMore} />;
}

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 (
    <React.Fragment>
      <CommitList
        commits={commits}
        currentIndex={current}
        selectCommit={index => setClampedTarget(index)}
      />
      <Swipeable
        onSwipedLeft={nextSlide}
        onSwipedRight={prevSlide}
        style={{ height: "100%" }}
      >
        <Slide time={index - current} version={versions[index]} />
      </Swipeable>
    </React.Fragment>
  );
}

// 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(
  <React.unstable_ConcurrentMode>
    <App />
  </React.unstable_ConcurrentMode>,
  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 (
    <div className="landing">
      <header
        style={{
          alignItems: "center",
          justifyContent: "center",
          color: "#222",
        }}
      >
        <video
          autoPlay
          loop
          muted
          playsInline
          width="560"
          height="350"
          style={{
            borderRadius: "3px",
            height: "350",
            boxShadow: "0 20px 50px 0 rgba(0,0,0,0.2)",
          }}
        >
          <source src={demoWebm} type="video/webm" />
          <source src={demoMp4} type="video/mp4" />
        </video>
        <div className="summary">
          <h1>Git History</h1>
          Quickly browse the history of files in any git repo:
          <ol>
            <li>
              Go to a file in <strong>GitHub</strong> (or{" "}
              <strong>GitLab</strong>, or <strong>Bitbucket</strong>)
            </li>
            <li>
              Replace <i>github.com</i> with <i>github.githistory.xyz</i>
            </li>
            <li>There's no step three</li>
          </ol>
          <a className="button" href={url}>
            Try it
          </a>
          <p style={{ marginBottom: "7px" }}>Also available as extensions:</p>
          <div className="extensions">
            <a
              href="https://chrome.google.com/webstore/detail/github-history-browser-ex/laghnmifffncfonaoffcndocllegejnf"
              target="_blank"
              rel="noopener noreferrer"
            >
              <ChromeLogo height={44} width={44} />
            </a>
            <a
              href="https://addons.mozilla.org/firefox/addon/github-history/"
              target="_blank"
              rel="noopener noreferrer"
            >
              <FirefoxLogo height={44} width={44} />
            </a>

            <a
              href="https://github.com/pomber/git-history/tree/master/cli"
              target="_blank"
              rel="noopener noreferrer"
            >
              <CliLogo height={44} width={44} />
            </a>

            <a
              href="https://marketplace.visualstudio.com/items?itemName=pomber.git-file-history"
              target="_blank"
              rel="noopener noreferrer"
            >
              <VsCodeLogo height={44} width={44} />
            </a>
          </div>
          <div
            style={{
              display: "flex",
              alignItems: "center",
              justifyContent: "space-between",
            }}
          >
            <iframe
              src="https://ghbtns.com/github-btn.html?user=pomber&repo=git-history&type=star&count=true&size=large"
              title="GitHub Stars"
              frameBorder="0"
              scrolling="0"
              width="160px"
              height="30px"
            />
            <i>
              by <a href="https://twitter.com/pomber">@pomber</a>
            </i>
          </div>
        </div>
      </header>
      <Testimonies />
      <Backers />
    </div>
  );
}

function Testimonies() {
  return (
    <section style={{ margin: "45px 0px 60px", background: "#fafafa" }}>
      <h2 style={{ textAlign: "center" }}>What people are saying...</h2>
      <div className="testimonies">
        <Testimony
          name="GitHub"
          link="https://github.blog/2019-03-01-release-radar-february-2019/#git-history"
          avatar={github}
        >
          Git History caught our eye with a beautiful way to tour the history of
          a file in a GitHub repo. ... there’s nothing to download and install:
          point Git History to a repository file URL to start traveling through
          time. Great Scott!
        </Testimony>
        <Testimony
          name="Smashing Magazine"
          link="https://twitter.com/smashingmag/status/1094865325974261761"
          avatar={smashing}
        >
          Ahh you know when you need to browse your Git history but it takes a
          while to find what you are looking for? Git History lets you browse
          the history in no-time. Useful.
        </Testimony>
        <Testimony
          name="CSS-Tricks"
          link="https://twitter.com/css/status/1105999990814662656"
          avatar={cssTricks}
        >
          I love little apps like this that copy the URL structure of another
          app, so you can replace just the TLD and it does something useful.
        </Testimony>
        <Testimony
          name="Addy Osmani"
          link="https://twitter.com/addyosmani/status/1093970927413387264"
          avatar={addy}
        >
          There's something really satisfying about browsing file history with
          this timeline UI. It's super nice.
        </Testimony>
      </div>
    </section>
  );
}

function Testimony({ link, avatar, name, children }) {
  return (
    <a href={link} target="_blank" rel="noopener noreferrer">
      <blockquote>
        <p>{children}</p>
        <cite style={{ display: "flex", alignItems: "center" }}>
          <img src={avatar} alt="avatar" />
          <strong style={{ paddingLeft: "10px", fontStyle: "normal" }}>
            {name}
          </strong>
        </cite>
      </blockquote>
    </a>
  );
}

function ResponsivePicture({ link, src, alt, append = "" }) {
  return (
    <a href={link} target="_blank" rel="noopener noreferrer">
      <picture>
        <source
          srcSet={src + "?width=800" + append}
          media="(min-width: 900px)"
        />
        <source
          srcSet={src + "?width=600" + append}
          media="(min-width: 600px)"
        />
        <img src={src + "?width=350" + append} alt={alt} />
      </picture>
    </a>
  );
}

function Backers() {
  return (
    <section
      style={{
        padding: "28px 0px",
        background: "linear-gradient(rgba(220, 220, 220),rgba(255, 255, 255))",
      }}
      className="support"
    >
      <h2 style={{ textAlign: "center" }}>Support Git History</h2>
      <div style={{ margin: "20px auto" }}>
        <h3>Sponsors</h3>
        <p>
          Support this project by becoming a sponsor. Your logo will show up
          here with a link to your website.{" "}
          <a href="https://opencollective.com/git-history#sponsor">
            Become a sponsor
          </a>
        </p>
        <a
          href="https://github.com/selefra/selefra"
          target="_blank"
          rel="noopener noreferrer"
        >
          <img
            src="https://github.com/selefra.png"
            style={{ borderRadius: "50%" }}
            alt="selefra"
            title="Selefra"
            width="100"
          />
        </a>
        <br />
        <ResponsivePicture
          link="https://opencollective.com/git-history/sponsor/0/website"
          alt="sponsors"
          src="https://opencollective.com/git-history/sponsor/0/avatar.svg"
        />
        <h3>Backers</h3>
        <p>
          Thank you to all our backers!{" "}
          <span role="img" aria-label="thanks">
            🙏
          </span>
          .{" "}
          <a href="https://opencollective.com/git-history#sponsor">
            Become a backer to help us ship more features!
          </a>
        </p>
        <ResponsivePicture
          link="https://opencollective.com/git-history#backers"
          alt="Backers"
          src="https://opencollective.com/git-history/backers.svg"
        />
        <h3>Thanks</h3>
        <p>
          Browser testing via{" "}
          <a
            href="https://www.lambdatest.com/"
            target="_blank"
            rel="noopener noreferrer"
          >
            <img
              src="https://www.lambdatest.com/resources/images/logos/logo.svg"
              style={{
                verticalAlign: "middle",
                marginLeft: "5px",
              }}
              width="147"
              height="26"
              alt="LambdaTest"
            />
          </a>
        </p>
      </div>
    </section>
  );
}


================================================
FILE: src/nightOwl.js
================================================
const theme = {
  plain: {
    color: "#d6deeb",
    backgroundColor: "#011627"
  },
  styles: [
    {
      types: ["changed"],
      style: {
        color: "rgb(162, 191, 252)",
        fontStyle: "italic"
      }
    },
    {
      types: ["deleted"],
      style: {
        color: "rgba(239, 83, 80, 0.56)",
        fontStyle: "italic"
      }
    },
    {
      types: ["inserted", "attr-name"],
      style: {
        color: "rgb(173, 219, 103)",
        fontStyle: "italic"
      }
    },
    {
      types: ["comment"],
      style: {
        color: "rgb(99, 119, 119)",
        fontStyle: "italic"
      }
    },
    {
      types: ["string", "url"],
      style: {
        color: "rgb(173, 219, 103)"
      }
    },
    {
      types: ["variable"],
      style: {
        color: "rgb(214, 222, 235)"
      }
    },
    {
      types: ["number"],
      style: {
        color: "rgb(247, 140, 108)"
      }
    },
    {
      types: ["builtin", "char", "constant", "function"],
      style: {
        color: "rgb(130, 170, 255)"
      }
    },
    {
      // This was manually added after the auto-generation
      // so that punctuations are not italicised
      types: ["punctuation"],
      style: {
        color: "rgb(199, 146, 234)"
      }
    },
    {
      types: ["selector", "doctype"],
      style: {
        color: "rgb(199, 146, 234)",
        fontStyle: "italic"
      }
    },
    {
      types: ["class-name"],
      style: {
        color: "rgb(255, 203, 139)"
      }
    },
    {
      types: ["tag", "operator", "keyword"],
      style: {
        color: "rgb(127, 219, 202)"
      }
    },
    {
      types: ["boolean"],
      style: {
        color: "rgb(255, 88, 116)"
      }
    },
    {
      types: ["property"],
      style: {
        color: "rgb(128, 203, 196)"
      }
    },
    {
      types: ["namespace"],
      style: {
        color: "rgb(178, 204, 214)"
      }
    }
  ]
};

module.exports = theme;


================================================
FILE: src/scroller.css
================================================
.scroller::-webkit-scrollbar {
  background-color: rgba(255, 255, 255, 0.01);
}

.scroller::-webkit-scrollbar-thumb {
  background-color: rgb(173, 219, 103, 0.3);
}


================================================
FILE: src/scroller.js
================================================
import React from "react";
import useChildren from "./use-virtual-children";
import "./scroller.css";
import useSpring from "./use-spring";
import { nextIndex, prevIndex, getScrollTop } from "./utils";

const initialState = {
  snap: false,
  targetTop: 0,
  currentTop: 0,
  areaIndex: 0
};

export default function Scroller({
  items,
  getRow,
  getRowHeight,
  data,
  snapAreas
}) {
  const ref = React.useRef();
  const height = useHeight(ref);

  const reducer = (prevState, action) => {
    switch (action.type) {
      case "unsnap":
        return !prevState.snap ? prevState : { ...prevState, snap: false };
      case "change-area":
        if (snapAreas.length === 0) {
          return prevState;
        }

        const { changeIndex, recalculate } = action;
        const movingFromUnknownIndex = !prevState.snap || recalculate;

        // TODO memo
        const heights = items.map((item, i) => getRowHeight(item, i, data));

        let newIndex;
        if (movingFromUnknownIndex) {
          //todo memo
          const oldIndex = getAreaIndex(
            prevState.targetTop,
            snapAreas,
            heights,
            height
          );

          newIndex = changeIndex(snapAreas, oldIndex);
        } else {
          newIndex = changeIndex(snapAreas, prevState.areaIndex);
        }

        if (newIndex === prevState.areaIndex && !movingFromUnknownIndex) {
          return prevState;
        }

        // TODO  memo
        let contentHeight = heights.reduce((a, b) => a + b, 0);

        const targetTop = getScrollTop(
          snapAreas[newIndex],
          contentHeight,
          height,
          heights
        );

        return {
          ...prevState,
          areaIndex: newIndex,
          snap: true,
          currentTop: null,
          targetTop
        };
      case "manual-scroll":
        const { newTop } = action;
        if (newTop === prevState.currentTop && !prevState.snap) {
          return prevState;
        }
        // console.log("manual scroll", newTop);
        return {
          ...prevState,
          snap: false,
          currentTop: newTop,
          targetTop: newTop
        };
      default:
        throw Error();
    }
  };

  const [{ snap, targetTop, currentTop }, dispatch] = React.useReducer(
    reducer,
    initialState
  );

  const top = useSpring({
    target: targetTop,
    current: currentTop,
    round: Math.round
  });
  // console.log("render", targetTop, top);

  const children = useChildren({
    height,
    top,
    items,
    getRow,
    getRowHeight,
    data
  });

  React.useEffect(() => {
    document.body.addEventListener("keydown", e => {
      if (e.keyCode === 38) {
        dispatch({ type: "change-area", changeIndex: prevIndex });
        e.preventDefault();
      } else if (e.keyCode === 40) {
        dispatch({ type: "change-area", changeIndex: nextIndex });
        e.preventDefault();
      }
    });
  }, []);

  // Auto-scroll to closest change when changing versions:
  // React.useLayoutEffect(() => {
  //   dispatch({
  //     type: "change-area",
  //     recalculate: true,
  //     changeIndex: closestIndex
  //   });
  // }, [snapAreas]);

  React.useEffect(() => {
    dispatch({
      type: "unsnap"
    });
  }, [snapAreas]);

  React.useLayoutEffect(() => {
    if (snap) {
      ref.current.scrollTop = top;
    }
  }, [snap, top]);

  return (
    <div
      style={{
        height: "100%",
        overflowY: "auto",
        overflowX: "hidden"
      }}
      className="scroller"
      ref={ref}
      onScroll={e => {
        const newTop = e.target.scrollTop;
        if (newTop === top) {
          return;
        }
        dispatch({ type: "manual-scroll", newTop });
      }}
    >
      <code
        style={{
          display: "block",
          width: "calc(100% - 20px)",
          maxWidth: "900px",
          margin: "auto",
          padding: "10px",
          boxSizing: "border-box",
          height: "100%"
        }}
        children={children}
      />
    </div>
  );
}

function getAreaIndex(scrollTop, areas, heights, containerHeight) {
  if (areas.length === 0) {
    return 0;
  }

  const scrollMiddle = scrollTop + containerHeight / 2;

  let h = 0;
  let i = 0;
  while (scrollMiddle > h) {
    h += heights[i++];
  }
  const middleRow = i;

  const areaCenters = areas.map(a => (a.start + a.end) / 2);
  areaCenters.unshift(0);
  for (let a = 0; a < areas.length; a++) {
    if (middleRow < areaCenters[a + 1]) {
      return (
        a -
        (areaCenters[a + 1] - middleRow) / (areaCenters[a + 1] - areaCenters[a])
      );
    }
  }

  return areas.length - 0.9;
}

function useHeight(ref) {
  let [height, setHeight] = React.useState(null);

  function handleResize() {
    setHeight(ref.current.clientHeight);
  }

  React.useEffect(() => {
    handleResize();
    window.addEventListener("resize", handleResize);
    return () => {
      window.removeEventListener("resize", handleResize);
    };
  }, []);

  return height;
}


================================================
FILE: src/scroller.story.js
================================================
import React from "react";
import { storiesOf } from "@storybook/react";
import Scroller from "./scroller";

const snapAreas1 = [
  { start: 1, end: 5 },
  { start: 15, end: 26 },
  { start: 50, end: 100 },
  { start: 300, end: 302 }
];

const snapAreas2 = [
  { start: 8, end: 12 },
  { start: 30, end: 32 },
  { start: 550, end: 552 },
  { start: 595, end: 599 }
];

const items = Array(600)
  .fill(0)
  .map((_, i) => {
    const a1 = snapAreas1.find(a => a.start <= i && i <= a.end);
    const a2 = snapAreas2.find(a => a.start <= i && i <= a.end);
    return {
      content: `Row ${i}${
        a1
          ? ` - Area1 [${a1.start}, ${a1.end}]`
          : a2
          ? ` - Area2 [${a2.start}, ${a2.end}]`
          : ""
      }`,
      key: i,
      height: 22
    };
  });

function getRow(item) {
  return (
    <div key={item.key} style={{ height: item.height }}>
      {item.content}
    </div>
  );
}

function getRowHeight(item) {
  return item.height;
}

function BasicScroller({ areas }) {
  const [top, setTop] = React.useState(40);
  return (
    <Scroller
      items={items}
      snapAreas={areas}
      getRow={getRow}
      getRowHeight={getRowHeight}
      top={top}
      setTop={setTop}
    />
  );
}

storiesOf("Scroller", module).add("single", () => (
  <div
    style={{
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      height: "90vh"
    }}
  >
    <div style={{ width: "60%", height: "80vh", border: "1px solid black" }}>
      <BasicScroller areas={snapAreas1} />
    </div>
  </div>
));

function DoubleScroller() {
  const [flag, setFlag] = React.useState(false);
  return (
    <div>
      <div style={{ height: "80vh", border: "1px solid black", width: "60vw" }}>
        <BasicScroller areas={flag ? snapAreas1 : snapAreas2} />
      </div>
      <div>
        {flag ? "Areas 1" : "Areas 2"}
        <button onClick={() => setFlag(flag => !flag)}>Toggle</button>
      </div>
    </div>
  );
}

storiesOf("Scroller", module).add("multiple", () => (
  <div
    style={{
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      height: "90vh"
    }}
  >
    <DoubleScroller />
  </div>
));


================================================
FILE: src/slide.js
================================================
import React from "react";
import animation from "./animation";
import theme from "./nightOwl";
import Scroller from "./scroller";

const themeStylesByType = Object.create(null);
theme.styles.forEach(({ types, style }) => {
  types.forEach(type => {
    themeStylesByType[type] = Object.assign(
      themeStylesByType[type] || {},
      style
    );
  });
});

function getLineHeight(line, i, { styles }) {
  return styles[i].height != null ? styles[i].height : 15;
}

function getLine(line, i, { styles }) {
  const style = styles[i];
  return (
    <div
      style={Object.assign({ overflow: "hidden", height: "15px" }, style)}
      key={line.key}
    >
      {!line.tokens.length && <br />}
      {line.tokens.map((token, i) => {
        const style = themeStylesByType[token.type] || {};
        return (
          <span style={style} key={i}>
            {token.content}
          </span>
        );
      })}
    </div>
  );
}

function Slide({ lines, styles, changes }) {
  return (
    <pre
      style={{
        backgroundColor: theme.plain.backgroundColor,
        color: theme.plain.color,
        paddingTop: "100px",
        margin: 0,
        height: "100%",
        width: "100%",
        boxSizing: "border-box"
      }}
    >
      <Scroller
        items={lines}
        getRow={getLine}
        getRowHeight={getLineHeight}
        data={{ styles }}
        snapAreas={changes}
      />
    </pre>
  );
}

export default function SlideWrapper({ time, version }) {
  const { lines, changes } = version;
  const styles = animation((time + 1) / 2, lines);
  return <Slide lines={lines} styles={styles} changes={changes} />;
}


================================================
FILE: src/use-spring.js
================================================
// based on https://github.com/streamich/react-use/blob/master/src/useSpring.ts
import { SpringSystem } from "rebound";
import { useState, useEffect } from "react";

export default function useSpring({
  target = 0,
  current = null,
  tension = 0,
  friction = 10,
  round = x => x
}) {
  const [spring, setSpring] = useState(null);
  const [value, setValue] = useState(target);

  useEffect(() => {
    const listener = {
      onSpringUpdate: spring => {
        const value = spring.getCurrentValue();
        setValue(round(value));
      }
    };

    if (!spring) {
      const newSpring = new SpringSystem().createSpring(tension, friction);
      newSpring.setCurrentValue(target);
      setSpring(newSpring);
      newSpring.addListener(listener);
      return;
    }

    return () => {
      spring.removeListener(listener);
      setSpring(null);
    };
  }, [tension, friction]);

  useEffect(() => {
    if (spring) {
      spring.setEndValue(target);
      if (current != null) {
        spring.setCurrentValue(current);
      }
    }
  }, [target, current]);

  return value;
}


================================================
FILE: src/use-spring.story.js
================================================
import React from "react";
import { storiesOf } from "@storybook/react";
import useSpring from "./use-spring";

function Test() {
  const [{ target, current }, setState] = React.useState({
    target: 0,
    current: null
  });
  const value = useSpring({ target, current });

  return (
    <div>
      <div style={{ display: "flex" }}>
        <span style={{ flex: 0.3 }}>Target</span>
        <input
          value={target}
          onChange={e =>
            setState({
              target: +e.target.value,
              current: null
            })
          }
          style={{ flex: 1 }}
          type="range"
        />
        <span style={{ flex: 0.3 }}>{target}</span>
      </div>
      <div style={{ display: "flex" }}>
        <span style={{ flex: 0.3 }}>Current</span>
        <input
          value={current}
          onChange={e =>
            setState({
              target: +e.target.value,
              current: +e.target.value
            })
          }
          style={{ flex: 1 }}
          type="range"
        />
        <span style={{ flex: 0.3 }}>{current}</span>
      </div>
      <div style={{ display: "flex" }}>
        <span style={{ flex: 0.3 }}>Value</span>
        <input value={value} type="range" readOnly style={{ flex: 1 }} />
        <span style={{ flex: 0.3 }}>{Math.round(value * 1000) / 1000}</span>
      </div>
    </div>
  );
}

storiesOf("useSpring", module).add("test", () => (
  <div
    style={{
      display: "flex",
      alignItems: "center",
      justifyContent: "center",
      height: "90vh"
    }}
  >
    <div style={{ width: "60%" }}>
      <Test />
    </div>
  </div>
));


================================================
FILE: src/use-virtual-children.js
================================================
import React from "react";

export default function useChildren({
  items,
  getRow,
  getRowHeight,
  height,
  top,
  data
}) {
  const children = [];

  const extraRender = 1000;

  const topT = top - extraRender;
  const bottomT = top + height + extraRender;
  let h = 0;

  let topPlaceHolderH = 0;
  let bottomPlaceholderH = 0;

  // This is the bottleneck
  items.forEach((item, i) => {
    const itemH = getRowHeight(item, i, data);
    const nextH = h + itemH;
    const isOverTop = nextH < topT;
    const isUnderBottom = h > bottomT;

    if (isOverTop) {
      topPlaceHolderH += itemH;
    } else if (isUnderBottom) {
      bottomPlaceholderH += itemH;
    } else {
      children.push(getRow(item, i, data));
    }

    h = nextH;
  });

  children.unshift(<div style={{ height: topPlaceHolderH }} key="top-ph" />);
  children.push(<div style={{ height: bottomPlaceholderH }} key="bottom-ph" />);
  return children;
}


================================================
FILE: src/utils.js
================================================
export function nextIndex(list, currentIndex) {
  return Math.min(list.length - 1, Math.floor(currentIndex + 1));
}

export function prevIndex(list, currentIndex) {
  return Math.max(0, Math.ceil(currentIndex - 1));
}

export function closestIndex(list, currentIndex) {
  return Math.min(Math.max(0, Math.round(currentIndex)), list.length - 1);
}

export function getScrollTop(area, contentHeight, containerHeight, heights) {
  const start = heights.slice(0, area.start).reduce((a, b) => a + b, 0);
  const end =
    start + heights.slice(area.start, area.end + 1).reduce((a, b) => a + b, 0);
  const middle = (end + start) / 2;
  const halfContainer = containerHeight / 2;
  const bestTop =
    end - start > containerHeight ? start : middle - halfContainer;
  if (bestTop < 0) return 0;
  if (bestTop + containerHeight > contentHeight) {
    return contentHeight - containerHeight;
  }
  return bestTop;
}


================================================
FILE: src/utils.test.js
================================================
import { nextIndex, prevIndex } from "./utils";

describe("nextIndex", () => {
  const fiveItems = [1, 2, 3, 4, 5];
  test("works with middle index", () => {
    expect(nextIndex(fiveItems, 2)).toBe(3);
  });
  test("works with last index", () => {
    expect(nextIndex(fiveItems, 4)).toBe(4);
  });
  test("works with fractions", () => {
    expect(nextIndex(fiveItems, 1.1)).toBe(2);
    expect(nextIndex(fiveItems, 1.9)).toBe(2);
  });
});

describe("prevIndex", () => {
  const fiveItems = [1, 2, 3, 4, 5];
  test("works with middle index", () => {
    expect(prevIndex(fiveItems, 2)).toBe(1);
  });
  test("works with start index", () => {
    expect(prevIndex(fiveItems, 0)).toBe(0);
  });
  test("works with fractions", () => {
    expect(prevIndex(fiveItems, 1.1)).toBe(1);
    expect(prevIndex(fiveItems, 1.9)).toBe(1);
  });
});


================================================
FILE: vscode-ext/.gitignore
================================================
node_modules
site

================================================
FILE: vscode-ext/.vscode/launch.json
================================================
// A launch configuration that launches the extension inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Extension",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": ["--extensionDevelopmentPath=${workspaceFolder}"]
    },
    {
      "name": "Extension Tests",
      "type": "extensionHost",
      "request": "launch",
      "runtimeExecutable": "${execPath}",
      "args": [
        "--extensionDevelopmentPath=${workspaceFolder}",
        "--extensionTestsPath=${workspaceFolder}/test"
      ]
    }
  ]
}


================================================
FILE: vscode-ext/extension.js
================================================
const vscode = require("vscode");
const path = require("path");
const fs = require("fs");
const getCommits = require("./git");

// this method is called when your extension is activated
// your extension is activated the very first time the command is executed

/**
 * @param {vscode.ExtensionContext} context
 */
function activate(context) {
  // The command has been defined in the package.json file
  // Now provide the implementation of the command with  registerCommand
  // The commandId parameter must match the command field in package.json
  let disposable = vscode.commands.registerCommand(
    "extension.git-file-history",
    function() {
      // The code you place here will be executed every time your command is executed
      try {
        const currentPath = getCurrentPath();
        if (!currentPath) {
          vscode.window.showInformationMessage("No active file");
          return;
        }

        const panel = vscode.window.createWebviewPanel(
          "gfh",
          `${path.basename(currentPath)} (Git History)`,
          vscode.ViewColumn.One,
          {
            enableScripts: true,
            retainContextWhenHidden: true,
            localResourceRoots: [
              vscode.Uri.file(path.join(context.extensionPath, "site"))
            ]
          }
        );
        const indexPath = path.join(
          context.extensionPath,
          "site",
          "index.html"
        );

        const index = fs.readFileSync(indexPath, "utf-8");
        const newIndex = index
          .replace(
            "<body>",
            `<body><script>/*<!--*/window.vscode=acquireVsCodeApi();window._PATH=${JSON.stringify(
              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.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;
      }
    }
  );

  context.subscriptions.push(disposable);
}

function getCurrentPath() {
  return (
    vscode.window.activeTextEditor &&
    vscode.window.activeTextEditor.document &&
    vscode.window.activeTextEditor.document.fileName
  );
}

exports.activate = activate;

// this method is called when your extension is deactivated
function deactivate() {}

module.exports = {
  activate,
  deactivate
};


================================================
FILE: vscode-ext/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: vscode-ext/jsconfig.json
================================================
{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es6",
    "checkJs": false /* Typecheck .js files. */,
    "lib": ["es6"]
  },
  "exclude": ["node_modules"]
}


================================================
FILE: vscode-ext/package.json
================================================
{
  "name": "git-file-history",
  "displayName": "Git File History",
  "description": "Modern, fast and intuitive tool for browsing the history and files in any git repository",
  "version": "1.0.1",
  "repository": "pomber/git-history",
  "publisher": "pomber",
  "license": "MIT",
  "keywords": [
    "git",
    "history",
    "log",
    "file",
    "commit",
    "show"
  ],
  "engines": {
    "vscode": "^1.30.2"
  },
  "categories": [
    "Other"
  ],
  "icon": "images/icon.png",
  "galleryBanner": {
    "color": "#011627",
    "theme": "dark"
  },
  "activationEvents": [
    "onCommand:extension.git-file-history"
  ],
  "main": "./extension.js",
  "contributes": {
    "commands": [
      {
        "command": "extension.git-file-history",
        "title": "Git File History"
      }
    ]
  },
  "scripts": {
    "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",
    "vscode:prepublish": "yarn build"
  },
  "devDependencies": {
    "@types/mocha": "^2.2.42",
    "@types/node": "^10.12.21",
    "cross-env": "^5.2.0",
    "eslint": "^5.13.0",
    "typescript": "^3.3.1",
    "vscode": "^1.1.28"
  },
  "dependencies": {
    "execa": "^1.0.0"
  }
}


================================================
FILE: vscode-ext/readme.md
================================================
# Git File History

Quickly browse the history of a file from any git repository

![feature X](https://user-images.githubusercontent.com/1911623/52807635-d5043480-306a-11e9-9b03-351b7cda4936.gif)

### 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)]

<a href="https://github.com/selefra/selefra" target="_blank"><img src="https://github.com/selefra.png" style="border-radius: 50%" alt="selefra" width="100"></a>

<a href="https://opencollective.com/git-history/sponsor/0/website" target="_blank"><img src="https://opencollective.com/git-history/sponsor/0/avatar.svg"></a>

### Backers

Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)]

<a href="https://opencollective.com/git-history#backers" target="_blank"><img src="https://opencollective.com/git-history/backers.svg?width=890"></a>


================================================
FILE: vscode-ext/test/extension.test.js
================================================
/* global suite, test */

//
// Note: This example test is leveraging the Mocha test framework.
// Please refer to their documentation on https://mochajs.org/ for help.
//

// The module 'assert' provides assertion methods from node
const assert = require("assert");

// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
// const vscode = require('vscode');
// const myExtension = require('../extension');

// Defines a Mocha test suite to group tests of similar kind together
suite("Extension Tests", function() {
  // Defines a Mocha unit test
  test("Something 1", function() {
    assert.equal(-1, [1, 2, 3].indexOf(5));
    assert.equal(-1, [1, 2, 3].indexOf(0));
  });
});


================================================
FILE: vscode-ext/test/index.js
================================================
//
// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING
//
// This file is providing the test runner to use when running extension tests.
// By default the test runner in use is Mocha based.
//
// You can provide your own test runner if you want to override it by exporting
// a function run(testRoot: string, clb: (error:Error) => void) that the extension
// host can call to run the tests. The test runner is expected to use console.log
// to report the results back to the caller. When the tests are finished, return
// a possible error to the callback or null if none.

const testRunner = require("vscode/lib/testrunner");

// You can directly control Mocha options by configuring the test runner below
// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options
// for more info
testRunner.configure({
  ui: "tdd", // the TDD UI is being used in extension.test.js (suite, test, etc.)
  useColors: true // colored output from test results
});

module.exports = testRunner;


================================================
FILE: vscode-ext/test-git.js
================================================
#!/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")
  )
);


================================================
FILE: vscode-ext/vsc-extension-quickstart.md
================================================
## What's in the folder

- This folder contains all of the files necessary for your extension.
- `package.json` - this is the manifest file in which you declare your extension and command.
  - The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin.
- `extension.js` - this is the main file where you will provide the implementation of your command.
  - The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`.
  - We pass the function containing the implementation of the command as the second parameter to `registerCommand`.

## Get up and running straight away

- Press `F5` to open a new window with your extension loaded.
- Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Git File History`.
- Set breakpoints in your code inside `extension.js` to debug your extension.
- Find output from your extension in the debug console.

## Make changes

- You can relaunch the extension from the debug toolbar after changing code in `extension.js`.
- You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.

## Explore the API

- You can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`.

## Run tests

- Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.
- Press `F5` to run the tests in a new window with your extension loaded.
- See the output of the test result in the debug console.
- Make changes to `test/extension.test.js` or create new test files inside the `test` folder.
  - By convention, the test runner will only consider files matching the name pattern `**.test.js`.
  - You can create folders inside the `test` folder to structure your tests any way you want.
Download .txt
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
Download .txt
SYMBOL INDEX (106 symbols across 32 files)

FILE: .storybook/config.js
  function loadStories (line 5) | function loadStories() {

FILE: cli/git.js
  function getCommits (line 4) | async function getCommits(path, last, before) {
  function getContent (line 45) | async function getContent(commit, path) {

FILE: src/airframe/airframe.js
  constant MULTIPLY (line 2) | const MULTIPLY = "multiply";
  function mergeResults (line 5) | function mergeResults(results, composite) {
  function createAnimation (line 101) | function createAnimation(type, props, ...children) {

FILE: src/animation.js
  function ShrinkHeight (line 20) | function ShrinkHeight() {
  function GrowHeight (line 37) | function GrowHeight() {
  function SwitchLines (line 47) | function SwitchLines({ filterExit, filterEnter, filterFadeOut }) {

FILE: src/app-helpers.js
  function Center (line 3) | function Center({ children }) {
  function Loading (line 20) | function Loading({ repo, path }) {
  function Error (line 30) | function Error({ error, gitProvider }) {
  function useDocumentTitle (line 70) | function useDocumentTitle(title) {

FILE: src/app.js
  function App (line 7) | function App() {
  function InnerApp (line 27) | function InnerApp({ gitProvider }) {
  function useVersionsLoader (line 53) | function useVersionsLoader(gitProvider) {

FILE: src/git-providers/bitbucket-commit-fetcher.js
  function getCommits (line 3) | async function getCommits({ repo, sha, path, last, token }) {
  function getContent (line 50) | async function getContent(repo, sha, path, token) {

FILE: src/git-providers/bitbucket-provider.js
  constant TOKEN_KEY (line 7) | const TOKEN_KEY = "bitbucket-token";
  function isLoggedIn (line 9) | function isLoggedIn() {
  function getUrlParams (line 13) | function getUrlParams() {
  function getPath (line 25) | function getPath() {
  function showLanding (line 30) | function showLanding() {
  function logIn (line 35) | function logIn() {
  function LogInButton (line 51) | function LogInButton() {
  function getParams (line 62) | function getParams() {
  function getVersions (line 68) | async function getVersions(last) {

FILE: src/git-providers/cli-commit-fetcher.js
  function getCommits (line 1) | async function getCommits({ path, last }) {

FILE: src/git-providers/cli-provider.js
  function getPath (line 4) | function getPath() {
  function showLanding (line 8) | function showLanding() {
  function getVersions (line 12) | async function getVersions(last) {

FILE: src/git-providers/differ.js
  function myDiff (line 5) | function myDiff(oldCode, newCode) {
  function insert (line 30) | function insert(array, index, elements) {
  function slideDiff (line 34) | function slideDiff(lines, codes, slideIndex, language) {
  function parseLines (line 62) | function parseLines(codes, language) {
  function getSlides (line 70) | function getSlides(codes, language) {
  function getChanges (line 88) | function getChanges(lines) {

FILE: src/git-providers/github-commit-fetcher.js
  function getCommits (line 5) | async function getCommits({ repo, sha, path, token, last }) {
  function getContent (line 50) | async function getContent(repo, sha, path, token) {

FILE: src/git-providers/github-provider.js
  constant TOKEN_KEY (line 6) | const TOKEN_KEY = "github-token";
  function isLoggedIn (line 8) | function isLoggedIn() {
  function getUrlParams (line 12) | function getUrlParams() {
  function getPath (line 29) | function getPath() {
  function showLanding (line 34) | function showLanding() {
  function logIn (line 39) | function logIn() {
  function LogInButton (line 57) | function LogInButton() {
  function getParams (line 82) | function getParams() {
  function getVersions (line 88) | async function getVersions(last) {

FILE: src/git-providers/gitlab-commit-fetcher.js
  function getCommits (line 5) | async function getCommits({ repo, sha, path, token, last }) {
  function getContent (line 48) | async function getContent(repo, sha, path, token) {

FILE: src/git-providers/gitlab-provider.js
  constant TOKEN_KEY (line 7) | const TOKEN_KEY = "gitlab-token";
  function isLoggedIn (line 9) | function isLoggedIn() {
  function getUrlParams (line 13) | function getUrlParams() {
  function getPath (line 30) | function getPath() {
  function showLanding (line 35) | function showLanding() {
  function logIn (line 40) | function logIn() {
  function LogInButton (line 59) | function LogInButton() {
  function getParams (line 70) | function getParams() {
  function getVersions (line 76) | async function getVersions(last) {

FILE: src/git-providers/language-detector.js
  function getLanguage (line 62) | function getLanguage(filename) {
  function getLanguageDependencies (line 72) | function getLanguageDependencies(lang) {
  function loadLanguage (line 76) | function loadLanguage(lang) {

FILE: src/git-providers/providers.js
  function getGitProvider (line 18) | function getGitProvider(source) {

FILE: src/git-providers/sources.js
  constant SOURCE (line 1) | const SOURCE = {
  function getSource (line 9) | function getSource() {

FILE: src/git-providers/tokenizer.js
  function flattenTokens (line 11) | function flattenTokens(tokens) {
  function tokenizeStrings (line 24) | function tokenizeStrings(prismTokens, parentType = "plain") {
  function tokenize (line 37) | function tokenize(code, language = "javascript") {

FILE: src/git-providers/versioner.worker.js
  function getVersions (line 17) | async function getVersions(source, params) {

FILE: src/git-providers/vscode-provider.js
  function getPath (line 6) | function getPath() {
  function showLanding (line 10) | function showLanding() {
  function getCommits (line 14) | function getCommits(path, last) {
  function getVersions (line 36) | async function getVersions(last) {

FILE: src/history.js
  function CommitInfo (line 7) | function CommitInfo({ commit, move, onClick }) {
  function CommitList (line 72) | function CommitList({ commits, currentIndex, selectCommit }) {
  function History (line 102) | function History({ versions, loadMore }) {
  function Slides (line 106) | function Slides({ versions, loadMore }) {
  function useSliderSpring (line 149) | function useSliderSpring(initial) {

FILE: src/landing.js
  function Landing (line 14) | function Landing() {
  function Testimonies (line 119) | function Testimonies() {
  function Testimony (line 164) | function Testimony({ link, avatar, name, children }) {
  function ResponsivePicture (line 180) | function ResponsivePicture({ link, src, alt, append = "" }) {
  function Backers (line 198) | function Backers() {

FILE: src/scroller.js
  function Scroller (line 14) | function Scroller({
  function getAreaIndex (line 179) | function getAreaIndex(scrollTop, areas, heights, containerHeight) {
  function useHeight (line 207) | function useHeight(ref) {

FILE: src/scroller.story.js
  function getRow (line 37) | function getRow(item) {
  function getRowHeight (line 45) | function getRowHeight(item) {
  function BasicScroller (line 49) | function BasicScroller({ areas }) {
  function DoubleScroller (line 78) | function DoubleScroller() {

FILE: src/slide.js
  function getLineHeight (line 16) | function getLineHeight(line, i, { styles }) {
  function getLine (line 20) | function getLine(line, i, { styles }) {
  function Slide (line 40) | function Slide({ lines, styles, changes }) {
  function SlideWrapper (line 64) | function SlideWrapper({ time, version }) {

FILE: src/use-spring.js
  function useSpring (line 5) | function useSpring({

FILE: src/use-spring.story.js
  function Test (line 5) | function Test() {

FILE: src/use-virtual-children.js
  function useChildren (line 3) | function useChildren({

FILE: src/utils.js
  function nextIndex (line 1) | function nextIndex(list, currentIndex) {
  function prevIndex (line 5) | function prevIndex(list, currentIndex) {
  function closestIndex (line 9) | function closestIndex(list, currentIndex) {
  function getScrollTop (line 13) | function getScrollTop(area, contentHeight, containerHeight, heights) {

FILE: vscode-ext/extension.js
  function activate (line 12) | function activate(context) {
  function getCurrentPath (line 89) | function getCurrentPath() {
  function deactivate (line 100) | function deactivate() {}

FILE: vscode-ext/git.js
  function getCommits (line 4) | async function getCommits(path, last, before) {
  function getContent (line 45) | async function getContent(commit, path) {
Condensed preview — 69 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (113K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 83,
    "preview": "open_collective: git-history\ngithub: [pomber]\ncustom: https://www.paypal.me/pomber\n"
  },
  {
    "path": ".github/opencollective.yml",
    "chars": 24,
    "preview": "collective: git-history\n"
  },
  {
    "path": ".gitignore",
    "chars": 318,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".storybook/addons.js",
    "chars": 86,
    "preview": "import \"@storybook/addon-actions/register\";\nimport \"@storybook/addon-links/register\";\n"
  },
  {
    "path": ".storybook/config.js",
    "chars": 218,
    "preview": "import { configure } from \"@storybook/react\";\n\nconst req = require.context(\"../src\", true, /\\.story\\.js$/);\n\nfunction lo"
  },
  {
    "path": ".travis.yml",
    "chars": 60,
    "preview": "language: node_js\nnode_js:\n  - \"stable\"\ncache:\n  yarn: true\n"
  },
  {
    "path": "cli/.gitignore",
    "chars": 17,
    "preview": "node_modules\nsite"
  },
  {
    "path": "cli/cli.js",
    "chars": 335,
    "preview": "#!/usr/bin/env node\n\nconst runServer = require(\"./server\");\nconst fs = require(\"fs\");\n\nlet path = process.argv[2] || \"./"
  },
  {
    "path": "cli/git.js",
    "chars": 1474,
    "preview": "const execa = require(\"execa\");\nconst pather = require(\"path\");\n\nasync function getCommits(path, last, before) {\n  const"
  },
  {
    "path": "cli/package.json",
    "chars": 1211,
    "preview": "{\n  \"name\": \"git-file-history\",\n  \"description\": \"Quickly browse the history of a file from any git repository\",\n  \"vers"
  },
  {
    "path": "cli/readme.md",
    "chars": 1039,
    "preview": "<div align=\"center\">\n<img alt=\"demo\" src=\"https://user-images.githubusercontent.com/1911623/52544866-51102b00-2d92-11e9-"
  },
  {
    "path": "cli/server.js",
    "chars": 1152,
    "preview": "const serve = require(\"koa-static\");\nconst Koa = require(\"koa\");\nconst pather = require(\"path\");\nconst getCommits = requ"
  },
  {
    "path": "craco.config.js",
    "chars": 161,
    "preview": "module.exports = {\n  webpack: {\n    configure: {\n      output: {\n        // I need \"this\" for workerize-loader\n        g"
  },
  {
    "path": "license",
    "chars": 1069,
    "preview": "MIT License\n\nCopyright (c) 2019 Rodrigo Pombo\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "netlify.toml",
    "chars": 63,
    "preview": "[[redirects]]\n  from = \"/*\"\n  to = \"/index.html\"\n  status = 200"
  },
  {
    "path": "package.json",
    "chars": 1657,
    "preview": "{\n  \"name\": \"githistory-web\",\n  \"version\": \"1.0.1\",\n  \"repository\": \"pomber/git-history\",\n  \"private\": true,\n  \"dependen"
  },
  {
    "path": "public/index.html",
    "chars": 2074,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/"
  },
  {
    "path": "public/manifest.json",
    "chars": 296,
    "preview": "{\n  \"short_name\": \"Git History\",\n  \"name\": \"Git History\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": "
  },
  {
    "path": "readme.md",
    "chars": 3305,
    "preview": "<div align=\"center\">\n<a href=\"https://github.githistory.xyz/torvalds/linux/blob/master/kernel/up.c\">\n<img alt=\"demo\" src"
  },
  {
    "path": "src/airframe/airframe.js",
    "chars": 3688,
    "preview": "import easing from \"./easing\";\nconst MULTIPLY = \"multiply\";\n\n/* eslint-disable */\nfunction mergeResults(results, composi"
  },
  {
    "path": "src/airframe/easing.js",
    "chars": 1498,
    "preview": "export default {\n  // no easing, no acceleration\n  linear: function(t) {\n    return t;\n  },\n  // accelerating from zero "
  },
  {
    "path": "src/animation.js",
    "chars": 2242,
    "preview": "/* eslint-disable */\nimport { createAnimation, Stagger } from \"./airframe/airframe\";\nimport easing from \"./airframe/easi"
  },
  {
    "path": "src/app-helpers.js",
    "chars": 1581,
    "preview": "import React, { useEffect } from \"react\";\n\nexport function Center({ children }) {\n  return (\n    <div\n      style={{\n   "
  },
  {
    "path": "src/app.js",
    "chars": 2198,
    "preview": "import React, { useState, useEffect } from \"react\";\nimport History from \"./history\";\nimport Landing from \"./landing\";\nim"
  },
  {
    "path": "src/comment-box.css",
    "chars": 683,
    "preview": ".comment-box {\n  position: relative;\n  border: 2px solid rgb(214, 222, 235, 0.5);\n  color: rgb(214, 222, 235, 0.8);\n  pa"
  },
  {
    "path": "src/duotoneLight.js",
    "chars": 1998,
    "preview": "// Duotone Light\n// Author: Simurai, adapted from DuoTone themes for Atom (http://simurai.com/projects/2016/01/01/duoton"
  },
  {
    "path": "src/git-providers/bitbucket-commit-fetcher.js",
    "chars": 2077,
    "preview": "const cache = {};\n\nasync function getCommits({ repo, sha, path, last, token }) {\n  if (!cache[path]) {\n    let fields =\n"
  },
  {
    "path": "src/git-providers/bitbucket-provider.js",
    "chars": 1662,
    "preview": "import netlify from \"netlify-auth-providers\";\nimport React from \"react\";\n\nimport versioner from \"./versioner\";\nimport { "
  },
  {
    "path": "src/git-providers/cli-commit-fetcher.js",
    "chars": 311,
    "preview": "async function getCommits({ path, last }) {\n  // TODO cache\n  const response = await fetch(\n    `/api/commits?path=${enc"
  },
  {
    "path": "src/git-providers/cli-provider.js",
    "chars": 408,
    "preview": "import versioner from \"./versioner\";\nimport { SOURCE } from \"./sources\";\n\nfunction getPath() {\n  return new URLSearchPar"
  },
  {
    "path": "src/git-providers/differ.js",
    "chars": 3053,
    "preview": "import { diffLines } from \"diff\";\nimport tokenize from \"./tokenizer\";\nconst newlineRe = /\\r\\n|\\r|\\n/;\n\nfunction myDiff(o"
  },
  {
    "path": "src/git-providers/github-commit-fetcher.js",
    "chars": 1914,
    "preview": "import { Base64 } from \"js-base64\";\n\nconst cache = {};\n\nasync function getCommits({ repo, sha, path, token, last }) {\n  "
  },
  {
    "path": "src/git-providers/github-provider.js",
    "chars": 2632,
    "preview": "import netlify from \"netlify-auth-providers\";\nimport React from \"react\";\nimport versioner from \"./versioner\";\nimport { S"
  },
  {
    "path": "src/git-providers/gitlab-commit-fetcher.js",
    "chars": 1784,
    "preview": "import { Base64 } from \"js-base64\";\n\nconst cache = {};\n\nasync function getCommits({ repo, sha, path, token, last }) {\n  "
  },
  {
    "path": "src/git-providers/gitlab-provider.js",
    "chars": 1739,
    "preview": "import netlify from \"netlify-auth-providers\";\nimport React from \"react\";\n\nimport versioner from \"./versioner\";\nimport { "
  },
  {
    "path": "src/git-providers/language-detector.js",
    "chars": 3066,
    "preview": "const filenameRegex = [\n  { lang: \"js\", regex: /\\.js$/i },\n  { lang: \"jsx\", regex: /\\.jsx$/i },\n  { lang: \"typescript\", "
  },
  {
    "path": "src/git-providers/language-detector.test.js",
    "chars": 6409,
    "preview": "import { getLanguage, getLanguageDependencies } from \"./language-detector\";\n\ndescribe(\"Can detect language\", () => {\n  t"
  },
  {
    "path": "src/git-providers/providers.js",
    "chars": 665,
    "preview": "const { SOURCE, getSource } = require(\"./sources\");\n\nlet providers;\nif (process.env.REACT_APP_GIT_PROVIDER === SOURCE.VS"
  },
  {
    "path": "src/git-providers/sources.js",
    "chars": 506,
    "preview": "export const SOURCE = {\n  GITHUB: \"github\",\n  GITLAB: \"gitlab\",\n  BITBUCKET: \"bitbucket\",\n  CLI: \"cli\",\n  VSCODE: \"vscod"
  },
  {
    "path": "src/git-providers/tokenizer.js",
    "chars": 1723,
    "preview": "// https://github.com/PrismJS/prism/issues/1303#issuecomment-375353987\nglobal.Prism = { disableWorkerMessageHandler: tru"
  },
  {
    "path": "src/git-providers/versioner.js",
    "chars": 164,
    "preview": "/* eslint-disable import/no-webpack-loader-syntax */\nimport worker from \"workerize-loader!./versioner.worker\";\nlet versi"
  },
  {
    "path": "src/git-providers/versioner.worker.js",
    "chars": 985,
    "preview": "import { getLanguage, loadLanguage } from \"./language-detector\";\nimport { getSlides, getChanges } from \"./differ\";\n\nimpo"
  },
  {
    "path": "src/git-providers/vscode-provider.js",
    "chars": 1139,
    "preview": "import { getLanguage, loadLanguage } from \"./language-detector\";\nimport { getSlides, getChanges } from \"./differ\";\n\ncons"
  },
  {
    "path": "src/history.js",
    "chars": 4265,
    "preview": "import React, { useEffect, useState } from \"react\";\nimport useSpring from \"react-use/lib/useSpring\";\nimport Swipeable fr"
  },
  {
    "path": "src/index.js",
    "chars": 242,
    "preview": "import App from \"./app\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\n\nconst root = document.getElementB"
  },
  {
    "path": "src/landing.css",
    "chars": 2285,
    "preview": ".extensions {\n  display: flex;\n  justify-content: center;\n  padding-bottom: 10px;\n}\n\n.extensions > * {\n  padding: 4px 10"
  },
  {
    "path": "src/landing.js",
    "chars": 8694,
    "preview": "import React from \"react\";\nimport demoMp4 from \"./demo.mp4\";\nimport demoWebm from \"./demo.webm\";\nimport smashing from \"."
  },
  {
    "path": "src/nightOwl.js",
    "chars": 1947,
    "preview": "const theme = {\n  plain: {\n    color: \"#d6deeb\",\n    backgroundColor: \"#011627\"\n  },\n  styles: [\n    {\n      types: [\"ch"
  },
  {
    "path": "src/scroller.css",
    "chars": 165,
    "preview": ".scroller::-webkit-scrollbar {\n  background-color: rgba(255, 255, 255, 0.01);\n}\n\n.scroller::-webkit-scrollbar-thumb {\n  "
  },
  {
    "path": "src/scroller.js",
    "chars": 5028,
    "preview": "import React from \"react\";\nimport useChildren from \"./use-virtual-children\";\nimport \"./scroller.css\";\nimport useSpring f"
  },
  {
    "path": "src/scroller.story.js",
    "chars": 2200,
    "preview": "import React from \"react\";\nimport { storiesOf } from \"@storybook/react\";\nimport Scroller from \"./scroller\";\n\nconst snapA"
  },
  {
    "path": "src/slide.js",
    "chars": 1646,
    "preview": "import React from \"react\";\nimport animation from \"./animation\";\nimport theme from \"./nightOwl\";\nimport Scroller from \"./"
  },
  {
    "path": "src/use-spring.js",
    "chars": 1094,
    "preview": "// based on https://github.com/streamich/react-use/blob/master/src/useSpring.ts\nimport { SpringSystem } from \"rebound\";\n"
  },
  {
    "path": "src/use-spring.story.js",
    "chars": 1646,
    "preview": "import React from \"react\";\nimport { storiesOf } from \"@storybook/react\";\nimport useSpring from \"./use-spring\";\n\nfunction"
  },
  {
    "path": "src/use-virtual-children.js",
    "chars": 932,
    "preview": "import React from \"react\";\n\nexport default function useChildren({\n  items,\n  getRow,\n  getRowHeight,\n  height,\n  top,\n  "
  },
  {
    "path": "src/utils.js",
    "chars": 908,
    "preview": "export function nextIndex(list, currentIndex) {\n  return Math.min(list.length - 1, Math.floor(currentIndex + 1));\n}\n\nexp"
  },
  {
    "path": "src/utils.test.js",
    "chars": 839,
    "preview": "import { nextIndex, prevIndex } from \"./utils\";\n\ndescribe(\"nextIndex\", () => {\n  const fiveItems = [1, 2, 3, 4, 5];\n  te"
  },
  {
    "path": "vscode-ext/.gitignore",
    "chars": 17,
    "preview": "node_modules\nsite"
  },
  {
    "path": "vscode-ext/.vscode/launch.json",
    "chars": 800,
    "preview": "// A launch configuration that launches the extension inside a new window\n// Use IntelliSense to learn about possible at"
  },
  {
    "path": "vscode-ext/extension.js",
    "chars": 2999,
    "preview": "const vscode = require(\"vscode\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst getCommits = require(\"./"
  },
  {
    "path": "vscode-ext/git.js",
    "chars": 1474,
    "preview": "const execa = require(\"execa\");\nconst pather = require(\"path\");\n\nasync function getCommits(path, last, before) {\n  const"
  },
  {
    "path": "vscode-ext/jsconfig.json",
    "chars": 177,
    "preview": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es6\",\n    \"checkJs\": false /* Typecheck .js files. */,"
  },
  {
    "path": "vscode-ext/package.json",
    "chars": 1405,
    "preview": "{\n  \"name\": \"git-file-history\",\n  \"displayName\": \"Git File History\",\n  \"description\": \"Modern, fast and intuitive tool f"
  },
  {
    "path": "vscode-ext/readme.md",
    "chars": 967,
    "preview": "# Git File History\n\nQuickly browse the history of a file from any git repository\n\n![feature X](https://user-images.githu"
  },
  {
    "path": "vscode-ext/test/extension.test.js",
    "chars": 732,
    "preview": "/* global suite, test */\n\n//\n// Note: This example test is leveraging the Mocha test framework.\n// Please refer to their"
  },
  {
    "path": "vscode-ext/test/index.js",
    "chars": 1018,
    "preview": "//\n// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING\n//\n// This file is providing the test runner to u"
  },
  {
    "path": "vscode-ext/test-git.js",
    "chars": 333,
    "preview": "#!/usr/bin/env node\n\n// node test-git.js extension.js 2 94c91d9\n\nconst getCommits = require(\"./git\");\n\nconst [, , path, "
  },
  {
    "path": "vscode-ext/vsc-extension-quickstart.md",
    "chars": 2085,
    "preview": "## What's in the folder\n\n- This folder contains all of the files necessary for your extension.\n- `package.json` - this i"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the pomber/git-history GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 69 files (100.3 KB), approximately 29.6k tokens, and a symbol index with 106 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!