[
  {
    "path": ".github/FUNDING.yml",
    "content": "open_collective: git-history\ngithub: [pomber]\ncustom: https://www.paypal.me/pomber\n"
  },
  {
    "path": ".github/opencollective.yml",
    "content": "collective: git-history\n"
  },
  {
    "path": ".gitignore",
    "content": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n\n/*/site"
  },
  {
    "path": ".storybook/addons.js",
    "content": "import \"@storybook/addon-actions/register\";\nimport \"@storybook/addon-links/register\";\n"
  },
  {
    "path": ".storybook/config.js",
    "content": "import { configure } from \"@storybook/react\";\n\nconst req = require.context(\"../src\", true, /\\.story\\.js$/);\n\nfunction loadStories() {\n  req.keys().forEach(filename => req(filename));\n}\n\nconfigure(loadStories, module);\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - \"stable\"\ncache:\n  yarn: true\n"
  },
  {
    "path": "cli/.gitignore",
    "content": "node_modules\nsite"
  },
  {
    "path": "cli/cli.js",
    "content": "#!/usr/bin/env node\n\nconst runServer = require(\"./server\");\nconst fs = require(\"fs\");\n\nlet path = process.argv[2] || \"./.\";\n\nif (path === \"--help\") {\n  console.log(`Usage:\n\n  githistory some/file.ext\n  `);\n  process.exit();\n}\n\nif (!fs.existsSync(path)) {\n  console.log(`File not found: ${path}`);\n  process.exit();\n}\n\nrunServer(path);\n"
  },
  {
    "path": "cli/git.js",
    "content": "const execa = require(\"execa\");\nconst pather = require(\"path\");\n\nasync function getCommits(path, last, before) {\n  const format = `{\"hash\":\"%h\",\"author\":{\"login\":\"%aN\"},\"date\":\"%ad\"},`;\n  const { stdout } = await execa(\n    \"git\",\n    [\n      \"log\",\n      `--max-count=${before ? last + 1 : last}`,\n      `--pretty=format:${format}`,\n      \"--date=iso\",\n      `${before || \"HEAD\"}`,\n      \"--\",\n      pather.basename(path)\n    ],\n    { cwd: pather.dirname(path) }\n  );\n  const json = `[${stdout.slice(0, -1)}]`;\n\n  const messagesOutput = await execa(\n    \"git\",\n    [\n      \"log\",\n      `--max-count=${last}`,\n      `--pretty=format:%s`,\n      `${before || \"HEAD\"}`,\n      \"--\",\n      pather.basename(path)\n    ],\n    { cwd: pather.dirname(path) }\n  );\n\n  const messages = messagesOutput.stdout.replace('\"', '\\\\\"').split(/\\r?\\n/);\n\n  const result = JSON.parse(json).map((commit, i) => ({\n    ...commit,\n    date: new Date(commit.date),\n    message: messages[i]\n  }));\n\n  return before ? result.slice(1) : result;\n}\n\nasync function getContent(commit, path) {\n  const { stdout } = await execa(\n    \"git\",\n    [\"show\", `${commit.hash}:./${pather.basename(path)}`],\n    { cwd: pather.dirname(path) }\n  );\n  return stdout;\n}\n\nmodule.exports = async function(path, last, before) {\n  const commits = await getCommits(path, last, before);\n  await Promise.all(\n    commits.map(async commit => {\n      commit.content = await getContent(commit, path);\n    })\n  );\n  return commits;\n};\n"
  },
  {
    "path": "cli/package.json",
    "content": "{\n  \"name\": \"git-file-history\",\n  \"description\": \"Quickly browse the history of a file from any git repository\",\n  \"version\": \"1.0.1\",\n  \"repository\": \"pomber/git-history\",\n  \"keywords\": [\n    \"cli\",\n    \"git\",\n    \"file\",\n    \"history\",\n    \"log\",\n    \"commits\",\n    \"change\",\n    \"animation\",\n    \"gui\"\n  ],\n  \"license\": \"MIT\",\n  \"bin\": {\n    \"git-file-history\": \"./cli.js\",\n    \"githistory\": \"./cli.js\",\n    \"git-history\": \"./cli.js\"\n  },\n  \"files\": [\n    \"site\",\n    \"*.js\"\n  ],\n  \"dependencies\": {\n    \"execa\": \"^1.0.0\",\n    \"get-port\": \"^4.1.0\",\n    \"koa\": \"^2.7.0\",\n    \"koa-router\": \"^7.4.0\",\n    \"koa-static\": \"^5.0.0\",\n    \"open\": \"^0.0.5\",\n    \"opencollective-postinstall\": \"^2.0.2\",\n    \"serve-handler\": \"^5.0.8\",\n    \"yargs\": \"^13.2.2\"\n  },\n  \"scripts\": {\n    \"build-site\": \"cd .. && cross-env REACT_APP_GIT_PROVIDER=cli yarn build && rm -fr cli/site/ && cp -r build/ cli/site/\",\n    \"build\": \"yarn build-site\",\n    \"ls-package\": \"npm pack && tar -xvzf *.tgz && rm -rf package *.tgz\",\n    \"postinstall\": \"opencollective-postinstall\"\n  },\n  \"devDependencies\": {\n    \"cross-env\": \"^5.2.0\"\n  },\n  \"collective\": {\n    \"type\": \"opencollective\",\n    \"url\": \"https://opencollective.com/git-history\"\n  }\n}\n"
  },
  {
    "path": "cli/readme.md",
    "content": "<div align=\"center\">\n<img alt=\"demo\" src=\"https://user-images.githubusercontent.com/1911623/52544866-51102b00-2d92-11e9-9d9a-323470c1b0af.gif\" />\n</div>\n\n# Git History CLI\n\nQuickly browse the history of a file from any git repository.\n\n> You need [node](https://nodejs.org/en/) to run this\n\n```bash\n$ npx git-file-history path/to/file.ext\n```\n\nor\n\n```bash\n$ npm install -g git-file-history\n$ git-file-history path/to/file.ext\n```\n\n### Sponsors\n\nSupport 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)]\n\n<a href=\"https://opencollective.com/git-history/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/git-history/sponsor/0/avatar.svg\"></a>\n\n### Backers\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)]\n\n<a href=\"https://opencollective.com/git-history#backers\" target=\"_blank\"><img src=\"https://opencollective.com/git-history/backers.svg?width=890\"></a>\n"
  },
  {
    "path": "cli/server.js",
    "content": "const serve = require(\"koa-static\");\nconst Koa = require(\"koa\");\nconst pather = require(\"path\");\nconst getCommits = require(\"./git\");\nconst getPort = require(\"get-port\");\nconst open = require(\"open\");\nconst router = require(\"koa-router\")();\nconst argv = require(\"yargs\")\n  .usage(\"Usage: $0 <some/file.ext> [options]\")\n  .describe(\"port\", \"Port number (default = 5000)\")\n  .default(\"port\", 5000).argv;\n\nconst sitePath = pather.join(__dirname, \"site/\");\n\nrouter.get(\"/api/commits\", async ctx => {\n  const query = ctx.query;\n  const { path, last = 10, before = null } = query;\n\n  const commits = await getCommits(path, last, before);\n\n  ctx.body = commits;\n});\n\nconst app = new Koa();\napp.use(router.routes());\napp.use(serve(sitePath));\napp.on(\"error\", err => {\n  console.error(\"Server error\", err);\n  console.error(\n    \"Let us know of the error at https://github.com/pomber/git-history/issues\"\n  );\n});\n\nmodule.exports = async function runServer(path) {\n  const port = await getPort({ port: argv.port });\n  app.listen(port);\n  console.log(\"Running at http://localhost:\" + port);\n  open(`http://localhost:${port}/?path=${encodeURIComponent(path)}`);\n};\n"
  },
  {
    "path": "craco.config.js",
    "content": "module.exports = {\n  webpack: {\n    configure: {\n      output: {\n        // I need \"this\" for workerize-loader\n        globalObject: \"this\"\n      }\n    }\n  }\n};\n"
  },
  {
    "path": "license",
    "content": "MIT License\n\nCopyright (c) 2019 Rodrigo Pombo\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "netlify.toml",
    "content": "[[redirects]]\n  from = \"/*\"\n  to = \"/index.html\"\n  status = 200"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"githistory-web\",\n  \"version\": \"1.0.1\",\n  \"repository\": \"pomber/git-history\",\n  \"private\": true,\n  \"dependencies\": {\n    \"@craco/craco\": \"^3.5.0\",\n    \"diff\": \"^4.0.1\",\n    \"js-base64\": \"^2.5.1\",\n    \"netlify-auth-providers\": \"^1.0.0-alpha5\",\n    \"opencollective-postinstall\": \"^2.0.2\",\n    \"prismjs\": \"^1.15.0\",\n    \"react\": \"^16.8.4\",\n    \"react-dom\": \"^16.8.4\",\n    \"react-scripts\": \"2.1.3\",\n    \"react-swipeable\": \"^4.3.2\",\n    \"react-use\": \"^5.2.2\",\n    \"rebound\": \"^0.1.0\",\n    \"workerize-loader\": \"^1.0.4\"\n  },\n  \"scripts\": {\n    \"start\": \"craco --openssl-legacy-provider start\",\n    \"build\": \"craco build\",\n    \"format\": \"prettier --write \\\"**/*.{js,jsx,md,json,html,css,yml}\\\" --ignore-path .gitignore\",\n    \"test-prettier\": \"prettier --check \\\"**/*.{js,jsx,md,json,html,css,yml}\\\" --ignore-path .gitignore\",\n    \"test-cra\": \"react-scripts test\",\n    \"test\": \"run-s test-prettier test-cra\",\n    \"eject\": \"react-scripts eject\",\n    \"postinstall\": \"opencollective-postinstall\",\n    \"storybook\": \"start-storybook -p 9009 -s public\",\n    \"build-storybook\": \"build-storybook -s public\"\n  },\n  \"eslintConfig\": {\n    \"extends\": \"react-app\"\n  },\n  \"browserslist\": [\n    \">0.2%\",\n    \"not dead\",\n    \"not ie <= 11\",\n    \"not op_mini all\"\n  ],\n  \"devDependencies\": {\n    \"@babel/core\": \"^7.3.4\",\n    \"@storybook/addon-actions\": \"^4.1.13\",\n    \"@storybook/addon-links\": \"^4.1.13\",\n    \"@storybook/addons\": \"^4.1.13\",\n    \"@storybook/react\": \"^4.1.13\",\n    \"babel-loader\": \"8.0.4\",\n    \"npm-run-all\": \"^4.1.5\",\n    \"prettier\": \"^1.16.4\"\n  },\n  \"collective\": {\n    \"type\": \"opencollective\",\n    \"url\": \"https://opencollective.com/git-history\"\n  }\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\" />\n    <link rel=\"preconnect\" href=\"https://api.github.com/\" crossorigin />\n    <meta\n      name=\"viewport\"\n      content=\"width=device-width, initial-scale=1, shrink-to-fit=no\"\n    />\n    <meta name=\"theme-color\" content=\"#000000\" />\n    <!--\n      manifest.json provides metadata used when your web app is added to the\n      homescreen on Android. See https://developers.google.com/web/fundamentals/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\" />\n    <meta\n      name=\"description\"\n      content=\"Quickly browse the history of a file from GitHub, GitLab, Bitbucket or any git repository\"\n    />\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>Git History</title>\n    <style>\n      html {\n        height: 100%;\n      }\n      body {\n        margin: 0;\n        padding: 0;\n        height: 100%;\n        font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\",\n          \"Oxygen\", \"Ubuntu\", \"Cantarell\", \"Fira Sans\", \"Droid Sans\",\n          \"Helvetica Neue\", sans-serif;\n        -webkit-font-smoothing: antialiased;\n        -moz-osx-font-smoothing: grayscale;\n        background-color: rgb(1, 22, 39);\n        color: rgb(214, 222, 235);\n      }\n\n      #root {\n        height: 100%;\n      }\n\n      footer {\n        position: fixed;\n        right: 10px;\n        bottom: 8px;\n        opacity: 0.4;\n        text-align: center;\n      }\n\n      a {\n        color: rgb(173, 219, 103);\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"root\"></div>\n  </body>\n</html>\n"
  },
  {
    "path": "public/manifest.json",
    "content": "{\n  \"short_name\": \"Git History\",\n  \"name\": \"Git History\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \".\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#d6deeb\",\n  \"background_color\": \"#011627\"\n}\n"
  },
  {
    "path": "readme.md",
    "content": "<div align=\"center\">\n<a href=\"https://github.githistory.xyz/torvalds/linux/blob/master/kernel/up.c\">\n<img alt=\"demo\" src=\"https://user-images.githubusercontent.com/1911623/54575634-9b10b000-49d3-11e9-8a19-56e40636e45d.gif\" width=\"600\" />\n</a>\n</div>\n\n# [Git History](https://githistory.xyz)\n\nQuickly browse the history of files in any git repo:\n\n1. Go to a file in **GitHub** (or **GitLab**, or **Bitbucket**)\n1. Replace `github.com` with `github.githistory.xyz`\n1. There's no step three\n\n[Try it](https://github.githistory.xyz/babel/babel/blob/master/packages/babel-core/test/browserify.js)\n\n> If you like this project consider [backing my open source work on Patreon!](https://patreon.com/pomber)  \n> And follow [@pomber](https://twitter.com/pomber) on twitter for updates.\n\n## Extensions\n\n### Browsers\n\nYou 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.\n\n<details><summary>Or you can use a bookmarklet.</summary>\n\n```javascript\njavascript: (function() {\n  var url = window.location.href;\n  var regEx = /^(https?\\:\\/\\/)(www\\.)?(github|gitlab|bitbucket)\\.(com|org)\\/(.*)$/i;\n  if (regEx.test(url)) {\n    url = url.replace(regEx, \"$1$3.githistory.xyz/$5\");\n    window.open(url, \"_blank\");\n  } else {\n    alert(\"Not a Git File URL\");\n  }\n})();\n```\n\n</details>\n\n### Local Repos\n\nYou 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).\n\n## Support Git History\n\n### Sponsors\n\nSupport 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)]\n\n<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>\n\n<a href=\"https://opencollective.com/git-history/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/git-history/sponsor/0/avatar.svg\"></a>\n\n### Backers\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)]\n\n<a href=\"https://opencollective.com/git-history#backers\" target=\"_blank\"><img src=\"https://opencollective.com/git-history/backers.svg?width=890\"></a>\n\n### Thanks\n\n<p>Browser testing via <a href=\"https://www.lambdatest.com/\" target=\"_blank\"><picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://www.lambdatest.com/resources/images/logo-white.svg\">\n  <img alt=\"LambdaTest\" src=\"https://www.lambdatest.com/resources/images/logos/logo.svg\" style=\"vertical-align: middle;margin-left:5px\" width=\"147\" height=\"26\" >\n</picture></a></p>\n\n### Credits\n\nBased on these amazing projects:\n\n- [Prism](https://github.com/PrismJS/prism) by [Lea Verou](https://twitter.com/leaverou)\n- [jsdiff](https://github.com/kpdecker/jsdiff) by [Kevin Decker](https://twitter.com/kpdecker)\n- [Night Owl](https://github.com/sdras/night-owl-vscode-theme) by [Sarah Drasner](https://twitter.com/sarah_edo)\n\n## License\n\nMIT\n"
  },
  {
    "path": "src/airframe/airframe.js",
    "content": "import easing from \"./easing\";\nconst MULTIPLY = \"multiply\";\n\n/* eslint-disable */\nfunction mergeResults(results, composite) {\n  const firstResult = results[0];\n  if (results.length < 2) {\n    return firstResult;\n  }\n  if (Array.isArray(firstResult)) {\n    return firstResult.map((_, i) => {\n      return mergeResults(results.map(result => result[i]), composite);\n    });\n  } else {\n    const merged = Object.assign({}, ...results);\n\n    if (composite === MULTIPLY) {\n      const opacities = results.map(x => x.opacity).filter(x => x != null);\n      if (opacities.length !== 0) {\n        merged.opacity = opacities.reduce((a, b) => a * b);\n      }\n    }\n    return merged;\n  }\n}\n\nconst airframe = {\n  parallel: ({ children: fns }) => {\n    return (t, ...args) => {\n      const styles = fns.map(fn => fn(t, ...args));\n      const result = mergeResults(styles, MULTIPLY);\n      return result;\n    };\n  },\n  chain: ({ children: fns, durations }) => {\n    return (t, ...args) => {\n      let style = fns[0](0, ...args);\n      let lowerDuration = 0;\n      for (let i = 0; i < fns.length; i++) {\n        const fn = fns[i];\n        const thisDuration = durations[i];\n        const upperDuration = lowerDuration + thisDuration;\n        if (lowerDuration <= t && t <= upperDuration) {\n          const innerT = (t - lowerDuration) / thisDuration;\n          style = mergeResults([style, fn(innerT, ...args)]);\n        } else if (upperDuration < t) {\n          // merge the end of previous animation\n          style = mergeResults([style, fn(1, ...args)]);\n        } else if (t < lowerDuration) {\n          // merge the start of future animation\n          style = mergeResults([fn(0, ...args), style]);\n        }\n        lowerDuration = upperDuration;\n      }\n      return style;\n    };\n  },\n  delay: () => () => ({}),\n  tween: ({ from, to, ease = easing.linear }) => (t, targets) => {\n    const style = {};\n    Object.keys(from).forEach(key => {\n      const value = from[key] + (to[key] - from[key]) * ease(t);\n      if (key === \"x\") {\n        style[\"transform\"] = `translateX(${value}px)`;\n      } else {\n        style[key] = value;\n      }\n    });\n    return style;\n  }\n};\n\n/* @jsx createAnimation */\nexport const Stagger = props => (t, targets) => {\n  const filter = target => !props.filter || props.filter(target);\n  const interval =\n    targets.filter(filter).length < 2\n      ? 0\n      : props.interval / (targets.filter(filter).length - 1);\n  let i = 0;\n  return targets.map(target => {\n    // console.log(target, props.filter(target));\n    if (!filter(target)) {\n      return {};\n    }\n    const animation = (\n      <parallel>\n        <chain durations={[i * interval, 1 - props.interval]}>\n          <delay />\n          {props.children[0]}\n        </chain>\n      </parallel>\n    );\n    i++;\n    const result = animation(t, target);\n    // console.log(\"Stagger Result\", t, result);\n    return result;\n  });\n};\n\nexport function createAnimation(type, props, ...children) {\n  const allProps = Object.assign({ children }, props);\n  if (typeof type === \"string\") {\n    if (window.LOG === \"verbose\") {\n      return (t, ...args) => {\n        console.groupCollapsed(type, t);\n        const result = airframe[type](allProps)(t, ...args);\n        console.log(result);\n        console.groupEnd();\n        return result;\n      };\n    } else {\n      return airframe[type](allProps);\n    }\n  } else {\n    if (window.LOG === \"verbose\") {\n      return (t, ...args) => {\n        console.groupCollapsed(type.name, t);\n        const result = type(allProps)(t, ...args);\n        console.log(result);\n        console.groupEnd();\n        return result;\n      };\n    } else {\n      return type(allProps);\n    }\n  }\n}\n"
  },
  {
    "path": "src/airframe/easing.js",
    "content": "export default {\n  // no easing, no acceleration\n  linear: function(t) {\n    return t;\n  },\n  // accelerating from zero velocity\n  easeInQuad: function(t) {\n    return t * t;\n  },\n  // decelerating to zero velocity\n  easeOutQuad: function(t) {\n    return t * (2 - t);\n  },\n  // acceleration until halfway, then deceleration\n  easeInOutQuad: function(t) {\n    return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;\n  },\n  // accelerating from zero velocity\n  easeInCubic: function(t) {\n    return t * t * t;\n  },\n  // decelerating to zero velocity\n  easeOutCubic: function(t) {\n    return --t * t * t + 1;\n  },\n  // acceleration until halfway, then deceleration\n  easeInOutCubic: function(t) {\n    return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;\n  },\n  // accelerating from zero velocity\n  easeInQuart: function(t) {\n    return t * t * t * t;\n  },\n  // decelerating to zero velocity\n  easeOutQuart: function(t) {\n    return 1 - --t * t * t * t;\n  },\n  // acceleration until halfway, then deceleration\n  easeInOutQuart: function(t) {\n    return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * --t * t * t * t;\n  },\n  // accelerating from zero velocity\n  easeInQuint: function(t) {\n    return t * t * t * t * t;\n  },\n  // decelerating to zero velocity\n  easeOutQuint: function(t) {\n    return 1 + --t * t * t * t * t;\n  },\n  // acceleration until halfway, then deceleration\n  easeInOutQuint: function(t) {\n    return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * --t * t * t * t * t;\n  }\n};\n"
  },
  {
    "path": "src/animation.js",
    "content": "/* eslint-disable */\nimport { createAnimation, Stagger } from \"./airframe/airframe\";\nimport easing from \"./airframe/easing\";\n\nconst dx = 250;\nconst offOpacity = 0.6;\n\n/* @jsx createAnimation */\n\n// window.LOG = \"verbose\";\n\nconst SlideToLeft = () => (\n  <tween\n    from={{ x: 0, opacity: 1 }}\n    to={{ x: -dx, opacity: 0 }}\n    ease={easing.easeInQuad}\n  />\n);\n\nfunction ShrinkHeight() {\n  return (\n    <tween\n      from={{ height: 15 }}\n      to={{ height: 0 }}\n      ease={easing.easeInOutQuad}\n    />\n  );\n}\n\nconst SlideFromRight = () => (\n  <tween\n    from={{ x: dx, opacity: 0 }}\n    to={{ x: 0, opacity: 1 }}\n    ease={easing.easeOutQuad}\n  />\n);\nfunction GrowHeight() {\n  return (\n    <tween\n      from={{ height: 0 }}\n      to={{ height: 15 }}\n      ease={easing.easeInOutQuad}\n    />\n  );\n}\n\nfunction SwitchLines({ filterExit, filterEnter, filterFadeOut }) {\n  return (\n    <parallel>\n      <Stagger interval={0.2} filter={filterExit}>\n        <chain durations={[0.35, 0.3, 0.35]}>\n          <SlideToLeft />\n          <ShrinkHeight />\n        </chain>\n      </Stagger>\n      <Stagger interval={0.2} filter={filterEnter}>\n        <chain durations={[0.35, 0.3, 0.35]}>\n          <delay />\n          <GrowHeight />\n          <SlideFromRight />\n        </chain>\n      </Stagger>\n      <Stagger interval={0} filter={filterEnter}>\n        <tween from={{ opacity: offOpacity }} to={{ opacity: 1 }} />\n      </Stagger>\n      <Stagger interval={0} filter={filterFadeOut}>\n        <tween\n          from={{ opacity: 1 }}\n          to={{ opacity: offOpacity }}\n          ease={easing.easeOutCubic}\n        />\n      </Stagger>\n      <Stagger interval={0} filter={l => !filterEnter(l) && !filterFadeOut(l)}>\n        <tween from={{ opacity: offOpacity }} to={{ opacity: offOpacity }} />\n      </Stagger>\n    </parallel>\n  );\n}\n\nexport default (\n  <chain durations={[0.5, 0.5]}>\n    <SwitchLines\n      filterExit={line => line.left && !line.middle}\n      filterEnter={line => !line.left && line.middle}\n      filterFadeOut={line => false}\n    />\n    <SwitchLines\n      filterExit={line => line.middle && !line.right}\n      filterEnter={line => !line.middle && line.right}\n      filterFadeOut={line => !line.left && line.middle}\n    />\n  </chain>\n);\n"
  },
  {
    "path": "src/app-helpers.js",
    "content": "import React, { useEffect } from \"react\";\n\nexport function Center({ children }) {\n  return (\n    <div\n      style={{\n        display: \"flex\",\n        flexDirection: \"column\",\n        alignItems: \"center\",\n        justifyContent: \"center\",\n        height: \"100%\",\n        padding: \"0 40px\"\n      }}\n    >\n      {children}\n    </div>\n  );\n}\n\nexport function Loading({ repo, path }) {\n  return (\n    <Center>\n      <p>\n        Loading <strong>{path}</strong> history {repo ? \"from \" + repo : \"\"}...\n      </p>\n    </Center>\n  );\n}\n\nexport function Error({ error, gitProvider }) {\n  const { LogInButton } = gitProvider;\n  if (error.status === 403) {\n    // FIX bitbucket uses 403 for private repos\n    return (\n      <Center>\n        <p>\n          GitHub API rate limit exceeded for your IP (60 requests per hour).\n        </p>\n        <p>Sign in with GitHub for more:</p>\n        <LogInButton />\n      </Center>\n    );\n  }\n\n  if (error.status === 404) {\n    return (\n      <Center>\n        <p>File not found.</p>\n        {gitProvider.isLoggedIn && !gitProvider.isLoggedIn() && (\n          <React.Fragment>\n            <p>Is it from a private repo? Sign in:</p>\n            <LogInButton />\n          </React.Fragment>\n        )}\n      </Center>\n    );\n  }\n\n  console.error(error);\n  console.error(\n    \"Let us know of the error at https://github.com/pomber/git-history/issues\"\n  );\n  return (\n    <Center>\n      <p>Unexpected error. Check the console.</p>\n    </Center>\n  );\n}\n\nexport function useDocumentTitle(title) {\n  useEffect(() => {\n    document.title = title;\n  }, [title]);\n}\n"
  },
  {
    "path": "src/app.js",
    "content": "import React, { useState, useEffect } from \"react\";\nimport History from \"./history\";\nimport Landing from \"./landing\";\nimport { useDocumentTitle, Loading, Error } from \"./app-helpers\";\nimport getGitProvider from \"./git-providers/providers\";\n\nexport default function App() {\n  const gitProvider = getGitProvider();\n\n  if (gitProvider.showLanding()) {\n    return <Landing />;\n  } else {\n    return (\n      <React.Fragment>\n        <InnerApp gitProvider={gitProvider} />\n        <footer>\n          <a href=\"https://github.com/pomber/git-history\">Git History</a>\n          <br />\n          by\n          <a href=\"https://twitter.com/pomber\">@pomber</a>\n        </footer>\n      </React.Fragment>\n    );\n  }\n}\n\nfunction InnerApp({ gitProvider }) {\n  const path = gitProvider.getPath();\n  const fileName = path.split(\"/\").pop();\n\n  useDocumentTitle(`Git History - ${fileName}`);\n\n  const [versions, loading, error, loadMore] = useVersionsLoader(\n    gitProvider,\n    path\n  );\n\n  if (error) {\n    return <Error error={error} gitProvider={gitProvider} />;\n  }\n\n  if (!versions && loading) {\n    return <Loading path={path} />;\n  }\n\n  if (!versions.length) {\n    return <Error error={{ status: 404 }} gitProvider={gitProvider} />;\n  }\n\n  return <History versions={versions} loadMore={loadMore} />;\n}\n\nfunction useVersionsLoader(gitProvider) {\n  const [state, setState] = useState({\n    data: null,\n    loading: true,\n    error: null,\n    last: 10,\n    noMore: false\n  });\n\n  const loadMore = () => {\n    setState(old => {\n      const shouldFetchMore = !old.loading && !old.noMore;\n      return shouldFetchMore\n        ? { ...old, last: old.last + 10, loading: true }\n        : old;\n    });\n  };\n\n  useEffect(() => {\n    gitProvider\n      .getVersions(state.last)\n      .then(data => {\n        setState(old => ({\n          data,\n          loading: false,\n          error: false,\n          last: old.last,\n          noMore: data.length < old.last\n        }));\n      })\n      .catch(error => {\n        setState(old => ({\n          ...old,\n          loading: false,\n          error: error.message || error\n        }));\n      });\n  }, [state.last]);\n\n  return [state.data, state.loading, state.error, loadMore];\n}\n"
  },
  {
    "path": "src/comment-box.css",
    "content": ".comment-box {\n  position: relative;\n  border: 2px solid rgb(214, 222, 235, 0.5);\n  color: rgb(214, 222, 235, 0.8);\n  padding: 6px;\n  min-width: 500px;\n  white-space: nowrap;\n}\n.comment-box:after,\n.comment-box:before {\n  bottom: 100%;\n  left: 50%;\n  border: solid transparent;\n  content: \" \";\n  height: 0;\n  width: 0;\n  position: absolute;\n  pointer-events: none;\n}\n\n.comment-box:after {\n  border-color: rgba(1, 22, 39, 0);\n  border-bottom-color: rgb(1, 22, 39);\n  border-width: 13px;\n  margin-left: -13px;\n}\n.comment-box:before {\n  border-color: rgba(1, 22, 39, 0);\n  border-bottom-color: rgb(214, 222, 235, 0.5);\n  border-width: 14px;\n  margin-left: -14px;\n  margin-bottom: 2px;\n}\n"
  },
  {
    "path": "src/duotoneLight.js",
    "content": "// Duotone Light\n// Author: Simurai, adapted from DuoTone themes for Atom (http://simurai.com/projects/2016/01/01/duotone-themes)\n// Conversion: Bram de Haan (http://atelierbram.github.io/Base2Tone-prism/output/prism/prism-base2tone-evening-dark.css)\n// Generated with Base16 Builder (https://github.com/base16-builder/base16-builder)\n\nvar theme /*: PrismTheme */ = {\n  plain: {\n    backgroundColor: \"#faf8f5\",\n    color: \"#728fcb\"\n  },\n  styles: [\n    {\n      types: [\"comment\", \"prolog\", \"doctype\", \"cdata\", \"punctuation\"],\n      style: {\n        color: \"#b6ad9a\"\n      }\n    },\n    {\n      types: [\"namespace\"],\n      style: {\n        opacity: 0.7\n      }\n    },\n    {\n      types: [\"tag\", \"operator\", \"number\"],\n      style: {\n        color: \"#063289\"\n      }\n    },\n    {\n      types: [\"property\", \"function\"],\n      style: {\n        color: \"#b29762\"\n      }\n    },\n    {\n      types: [\"tag-id\", \"selector\", \"atrule-id\"],\n      style: {\n        color: \"#2d2006\"\n      }\n    },\n    {\n      types: [\"attr-name\"],\n      style: {\n        color: \"#896724\"\n      }\n    },\n    {\n      types: [\n        \"boolean\",\n        \"string\",\n        \"entity\",\n        \"url\",\n        \"attr-value\",\n        \"keyword\",\n        \"control\",\n        \"directive\",\n        \"unit\",\n        \"statement\",\n        \"regex\",\n        \"at-rule\"\n      ],\n      style: {\n        color: \"#728fcb\"\n      }\n    },\n    {\n      types: [\"placeholder\", \"variable\"],\n      style: {\n        color: \"#93abdc\"\n      }\n    },\n    {\n      types: [\"deleted\"],\n      style: {\n        textDecorationLine: \"line-through\"\n      }\n    },\n    {\n      types: [\"inserted\"],\n      style: {\n        textDecorationLine: \"underline\"\n      }\n    },\n    {\n      types: [\"italic\"],\n      style: {\n        fontStyle: \"italic\"\n      }\n    },\n    {\n      types: [\"important\", \"bold\"],\n      style: {\n        fontWeight: \"bold\"\n      }\n    },\n    {\n      types: [\"important\"],\n      style: {\n        color: \"#896724\"\n      }\n    }\n  ]\n};\n\nmodule.exports = theme;\n"
  },
  {
    "path": "src/git-providers/bitbucket-commit-fetcher.js",
    "content": "const cache = {};\n\nasync function getCommits({ repo, sha, path, last, token }) {\n  if (!cache[path]) {\n    let fields =\n      \"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\";\n    // fields = \"*.*.*.*.*\";\n    const commitsResponse = await fetch(\n      `https://api.bitbucket.org/2.0/repositories/${repo}/filehistory/${sha}/${path}?fields=${fields}`,\n      { headers: token ? { Authorization: `bearer ${token}` } : {} }\n    );\n\n    if (!commitsResponse.ok) {\n      throw {\n        status: commitsResponse.status === 403 ? 404 : commitsResponse.status,\n        body: commitsJson\n      };\n    }\n\n    const commitsJson = await commitsResponse.json();\n\n    cache[path] = commitsJson.values.map(({ commit }) => ({\n      sha: commit.hash,\n      date: new Date(commit.date),\n      author: {\n        login: commit.author.user\n          ? commit.author.user.nickname\n          : commit.author.raw,\n        avatar: commit.author.user && commit.author.user.links.avatar.href\n      },\n      commitUrl: commit.links.html.href,\n      message: commit.message\n    }));\n  }\n\n  const commits = cache[path].slice(0, last);\n\n  await Promise.all(\n    commits.map(async commit => {\n      if (!commit.content) {\n        const info = await getContent(repo, commit.sha, path, token);\n        commit.content = info.content;\n      }\n    })\n  );\n\n  return commits;\n}\n\nasync function getContent(repo, sha, path, token) {\n  const contentResponse = await fetch(\n    `https://api.bitbucket.org/2.0/repositories/${repo}/src/${sha}/${path}`,\n    { headers: token ? { Authorization: `bearer ${token}` } : {} }\n  );\n\n  if (contentResponse.status === 404) {\n    return { content: \"\" };\n  }\n\n  if (!contentResponse.ok) {\n    throw {\n      status: contentResponse.status,\n      body: await contentResponse.json()\n    };\n  }\n\n  const content = await contentResponse.text();\n\n  return { content };\n}\n\nexport default {\n  getCommits\n};\n"
  },
  {
    "path": "src/git-providers/bitbucket-provider.js",
    "content": "import netlify from \"netlify-auth-providers\";\nimport React from \"react\";\n\nimport versioner from \"./versioner\";\nimport { SOURCE } from \"./sources\";\n\nconst TOKEN_KEY = \"bitbucket-token\";\n\nfunction isLoggedIn() {\n  return !!window.localStorage.getItem(TOKEN_KEY);\n}\n\nfunction getUrlParams() {\n  const [, owner, reponame, , sha, ...paths] = window.location.pathname.split(\n    \"/\"\n  );\n\n  if (!sha) {\n    return [];\n  }\n\n  return [owner + \"/\" + reponame, sha, paths.join(\"/\")];\n}\n\nfunction getPath() {\n  const [, , path] = getUrlParams();\n  return path;\n}\n\nfunction showLanding() {\n  const [repo, ,] = getUrlParams();\n  return !repo;\n}\n\nfunction logIn() {\n  // return new Promise((resolve, reject) => {\n  var authenticator = new netlify({\n    site_id: \"ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6\"\n  });\n  authenticator.authenticate({ provider: \"bitbucket\" }, function(err, data) {\n    if (err) {\n      console.error(err);\n      return;\n    }\n    window.localStorage.setItem(TOKEN_KEY, data.token);\n    window.location.reload(false);\n  });\n  // });\n}\n\nfunction LogInButton() {\n  return (\n    <button\n      onClick={logIn}\n      style={{ fontWeight: 600, padding: \"0.5em 0.7em\", cursor: \"pointer\" }}\n    >\n      <div>Sign in with Bitbucket</div>\n    </button>\n  );\n}\n\nfunction getParams() {\n  const [repo, sha, path] = getUrlParams();\n  const token = window.localStorage.getItem(TOKEN_KEY);\n  return { repo, sha, path, token };\n}\n\nasync function getVersions(last) {\n  const params = { ...getParams(), last };\n  return await versioner.getVersions(SOURCE.BITBUCKET, params);\n}\n\nexport default {\n  showLanding,\n  getPath,\n  getVersions,\n  logIn,\n  isLoggedIn,\n  LogInButton\n};\n"
  },
  {
    "path": "src/git-providers/cli-commit-fetcher.js",
    "content": "async function getCommits({ path, last }) {\n  // TODO cache\n  const response = await fetch(\n    `/api/commits?path=${encodeURIComponent(path)}&last=${last}`\n  );\n  const commits = await response.json();\n  commits.forEach(c => (c.date = new Date(c.date)));\n\n  return commits;\n}\n\nexport default {\n  getCommits\n};\n"
  },
  {
    "path": "src/git-providers/cli-provider.js",
    "content": "import versioner from \"./versioner\";\nimport { SOURCE } from \"./sources\";\n\nfunction getPath() {\n  return new URLSearchParams(window.location.search).get(\"path\");\n}\n\nfunction showLanding() {\n  return false;\n}\n\nasync function getVersions(last) {\n  const params = { path: getPath(), last };\n  return await versioner.getVersions(SOURCE.CLI, params);\n}\n\nexport default {\n  showLanding,\n  getVersions,\n  getPath\n};\n"
  },
  {
    "path": "src/git-providers/differ.js",
    "content": "import { diffLines } from \"diff\";\nimport tokenize from \"./tokenizer\";\nconst newlineRe = /\\r\\n|\\r|\\n/;\n\nfunction myDiff(oldCode, newCode) {\n  const changes = diffLines(oldCode || \"\", newCode);\n\n  let oldIndex = -1;\n  return changes.map(({ value, count, removed, added }) => {\n    const lines = value.split(newlineRe);\n    // check if last line is empty, if it is, remove it\n    const lastLine = lines.pop();\n    if (lastLine) {\n      lines.push(lastLine);\n    }\n    const result = {\n      oldIndex,\n      lines,\n      count,\n      removed,\n      added\n    };\n    if (!added) {\n      oldIndex += count;\n    }\n    return result;\n  });\n}\n\nfunction insert(array, index, elements) {\n  return array.splice(index, 0, ...elements);\n}\n\nfunction slideDiff(lines, codes, slideIndex, language) {\n  const prevLines = lines.filter(l => l.slides.includes(slideIndex - 1));\n  const prevCode = codes[slideIndex - 1] || \"\";\n  const currCode = codes[slideIndex];\n\n  const changes = myDiff(prevCode, currCode);\n\n  changes.forEach(change => {\n    if (change.added) {\n      const prevLine = prevLines[change.oldIndex];\n      const addAtIndex = lines.indexOf(prevLine) + 1;\n      const addLines = change.lines.map(content => ({\n        content,\n        slides: [slideIndex]\n      }));\n      insert(lines, addAtIndex, addLines);\n    } else if (!change.removed) {\n      for (let j = 1; j <= change.count; j++) {\n        prevLines[change.oldIndex + j].slides.push(slideIndex);\n      }\n    }\n  });\n\n  const tokenLines = tokenize(currCode, language);\n  const currLines = lines.filter(l => l.slides.includes(slideIndex));\n  currLines.forEach((line, index) => (line.tokens = tokenLines[index]));\n}\n\nexport function parseLines(codes, language) {\n  const lines = [];\n  for (let slideIndex = 0; slideIndex < codes.length; slideIndex++) {\n    slideDiff(lines, codes, slideIndex, language);\n  }\n  return lines;\n}\n\nexport function getSlides(codes, language) {\n  // codes are in reverse cronological order\n  const lines = parseLines(codes, language);\n  // console.log(\"lines\", lines);\n  return codes.map((_, slideIndex) => {\n    return lines\n      .map((line, lineIndex) => ({\n        content: line.content,\n        tokens: line.tokens,\n        left: line.slides.includes(slideIndex + 1),\n        middle: line.slides.includes(slideIndex),\n        right: line.slides.includes(slideIndex - 1),\n        key: lineIndex\n      }))\n      .filter(line => line.middle || line.left || line.right);\n  });\n}\n\nexport function getChanges(lines) {\n  const changes = [];\n  let currentChange = null;\n  let i = 0;\n  const isNewLine = i => !lines[i].left && lines[i].middle;\n  while (i < lines.length) {\n    if (isNewLine(i)) {\n      if (!currentChange) {\n        currentChange = { start: i };\n      }\n    } else {\n      if (currentChange) {\n        currentChange.end = i - 1;\n        changes.push(currentChange);\n        currentChange = null;\n      }\n    }\n    i++;\n  }\n\n  if (currentChange) {\n    currentChange.end = i - 1;\n    changes.push(currentChange);\n    currentChange = null;\n  }\n\n  return changes;\n}\n"
  },
  {
    "path": "src/git-providers/github-commit-fetcher.js",
    "content": "import { Base64 } from \"js-base64\";\n\nconst cache = {};\n\nasync function getCommits({ repo, sha, path, token, last }) {\n  if (!cache[path]) {\n    const commitsResponse = await fetch(\n      `https://api.github.com/repos/${repo}/commits?sha=${sha}&path=${path}`,\n      { headers: token ? { Authorization: `bearer ${token}` } : {} }\n    );\n\n    if (!commitsResponse.ok) {\n      throw {\n        status: commitsResponse.status,\n        body: commitsJson\n      };\n    }\n\n    const commitsJson = await commitsResponse.json();\n\n    cache[path] = commitsJson.map(commit => ({\n      sha: commit.sha,\n      date: new Date(commit.commit.author.date),\n      author: {\n        login: commit.author ? commit.author.login : commit.commit.author.name,\n        avatar: commit.author\n          ? commit.author.avatar_url\n          : \"https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png\"\n      },\n      commitUrl: commit.html_url,\n      message: commit.commit.message\n    }));\n  }\n\n  const commits = cache[path].slice(0, last);\n\n  await Promise.all(\n    commits.map(async commit => {\n      if (!commit.content) {\n        const info = await getContent(repo, commit.sha, path, token);\n        commit.content = info.content;\n        commit.fileUrl = info.url;\n      }\n    })\n  );\n\n  return commits;\n}\n\nasync function getContent(repo, sha, path, token) {\n  const contentResponse = await fetch(\n    `https://api.github.com/repos/${repo}/contents${path}?ref=${sha}`,\n    { headers: token ? { Authorization: `bearer ${token}` } : {} }\n  );\n\n  if (contentResponse.status === 404) {\n    return { content: \"\" };\n  }\n\n  const contentJson = await contentResponse.json();\n\n  if (!contentResponse.ok) {\n    throw {\n      status: contentResponse.status,\n      body: contentJson\n    };\n  }\n\n  const content = Base64.decode(contentJson.content);\n  return { content, url: contentJson.html_url };\n}\n\nexport default {\n  getCommits\n};\n"
  },
  {
    "path": "src/git-providers/github-provider.js",
    "content": "import netlify from \"netlify-auth-providers\";\nimport React from \"react\";\nimport versioner from \"./versioner\";\nimport { SOURCE } from \"./sources\";\n\nconst TOKEN_KEY = \"github-token\";\n\nfunction isLoggedIn() {\n  return !!window.localStorage.getItem(TOKEN_KEY);\n}\n\nfunction getUrlParams() {\n  const [\n    ,\n    owner,\n    reponame,\n    action,\n    sha,\n    ...paths\n  ] = window.location.pathname.split(\"/\");\n\n  if (action !== \"commits\" && action !== \"blob\") {\n    return [];\n  }\n\n  return [owner + \"/\" + reponame, sha, \"/\" + paths.join(\"/\")];\n}\n\nfunction getPath() {\n  const [, , path] = getUrlParams();\n  return path;\n}\n\nfunction showLanding() {\n  const [repo, ,] = getUrlParams();\n  return !repo;\n}\n\nfunction logIn() {\n  // return new Promise((resolve, reject) => {\n  var authenticator = new netlify({\n    site_id: \"ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6\"\n  });\n  authenticator.authenticate({ provider: \"github\", scope: \"repo\" }, function(\n    err,\n    data\n  ) {\n    if (err) {\n      console.error(err);\n      return;\n    }\n    window.localStorage.setItem(TOKEN_KEY, data.token);\n    window.location.reload(false);\n  });\n  // });\n}\nfunction LogInButton() {\n  return (\n    <button\n      onClick={logIn}\n      style={{ fontWeight: 600, padding: \"0.5em 0.7em\", cursor: \"pointer\" }}\n    >\n      <div>\n        <svg\n          fill=\"currentColor\"\n          preserveAspectRatio=\"xMidYMid meet\"\n          height=\"1em\"\n          width=\"1em\"\n          viewBox=\"0 0 40 40\"\n          style={{ verticalAlign: \"middle\", marginRight: \"0.5rem\" }}\n        >\n          <g>\n            <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\" />\n          </g>\n        </svg>\n        Sign in with GitHub\n      </div>\n    </button>\n  );\n}\n\nfunction getParams() {\n  const [repo, sha, path] = getUrlParams();\n  const token = window.localStorage.getItem(TOKEN_KEY);\n  return { repo, sha, path, token };\n}\n\nasync function getVersions(last) {\n  const params = { ...getParams(), last };\n  return await versioner.getVersions(SOURCE.GITHUB, params);\n}\n\nexport default {\n  showLanding,\n  getPath,\n  getParams,\n  getVersions,\n  logIn,\n  isLoggedIn,\n  LogInButton\n};\n"
  },
  {
    "path": "src/git-providers/gitlab-commit-fetcher.js",
    "content": "import { Base64 } from \"js-base64\";\n\nconst cache = {};\n\nasync function getCommits({ repo, sha, path, token, last }) {\n  if (!cache[path]) {\n    const commitsResponse = await fetch(\n      `https://gitlab.com/api/v4/projects/${encodeURIComponent(\n        repo\n      )}/repository/commits?path=${encodeURIComponent(path)}&ref_name=${sha}`,\n      { headers: token ? { Authorization: `bearer ${token}` } : {} }\n    );\n\n    const commitsJson = await commitsResponse.json();\n\n    if (!commitsResponse.ok) {\n      throw {\n        status: commitsResponse.status,\n        body: commitsJson\n      };\n    }\n\n    cache[path] = commitsJson.map(commit => ({\n      sha: commit.id,\n      date: new Date(commit.authored_date),\n      author: {\n        login: commit.author_name\n      },\n      // commitUrl: commit.html_url,\n      message: commit.title\n    }));\n  }\n\n  const commits = cache[path].slice(0, last);\n\n  await Promise.all(\n    commits.map(async commit => {\n      if (!commit.content) {\n        const info = await getContent(repo, commit.sha, path, token);\n        commit.content = info.content;\n      }\n    })\n  );\n\n  return commits;\n}\n\nasync function getContent(repo, sha, path, token) {\n  const contentResponse = await fetch(\n    `https://gitlab.com/api/v4/projects/${encodeURIComponent(\n      repo\n    )}/repository/files/${encodeURIComponent(path)}?ref=${sha}`,\n    { headers: token ? { Authorization: `bearer ${token}` } : {} }\n  );\n\n  if (contentResponse.status === 404) {\n    return { content: \"\" };\n  }\n\n  const contentJson = await contentResponse.json();\n\n  if (!contentResponse.ok) {\n    throw {\n      status: contentResponse.status,\n      body: contentJson\n    };\n  }\n\n  const content = Base64.decode(contentJson.content);\n  return { content };\n}\n\nexport default {\n  getCommits\n};\n"
  },
  {
    "path": "src/git-providers/gitlab-provider.js",
    "content": "import netlify from \"netlify-auth-providers\";\nimport React from \"react\";\n\nimport versioner from \"./versioner\";\nimport { SOURCE } from \"./sources\";\n\nconst TOKEN_KEY = \"gitlab-token\";\n\nfunction isLoggedIn() {\n  return !!window.localStorage.getItem(TOKEN_KEY);\n}\n\nfunction getUrlParams() {\n  const [\n    ,\n    owner,\n    reponame,\n    action,\n    sha,\n    ...paths\n  ] = window.location.pathname.split(\"/\");\n\n  if (action !== \"commits\" && action !== \"blob\") {\n    return [];\n  }\n\n  return [owner + \"/\" + reponame, sha, paths.join(\"/\")];\n}\n\nfunction getPath() {\n  const [, , path] = getUrlParams();\n  return path;\n}\n\nfunction showLanding() {\n  const [repo, ,] = getUrlParams();\n  return !repo;\n}\n\nfunction logIn() {\n  // return new Promise((resolve, reject) => {\n  var authenticator = new netlify({\n    site_id: \"ccf3a0e2-ac06-4f37-9b17-df1dd41fb1a6\"\n  });\n  authenticator.authenticate({ provider: \"gitlab\", scope: \"api\" }, function(\n    err,\n    data\n  ) {\n    if (err) {\n      console.error(err);\n      return;\n    }\n    window.localStorage.setItem(TOKEN_KEY, data.token);\n    window.location.reload(false);\n  });\n  // });\n}\n\nfunction LogInButton() {\n  return (\n    <button\n      onClick={logIn}\n      style={{ fontWeight: 600, padding: \"0.5em 0.7em\", cursor: \"pointer\" }}\n    >\n      <div>Sign in with GitLab</div>\n    </button>\n  );\n}\n\nfunction getParams() {\n  const [repo, sha, path] = getUrlParams();\n  const token = window.localStorage.getItem(TOKEN_KEY);\n  return { repo, sha, path, token };\n}\n\nasync function getVersions(last) {\n  const params = { ...getParams(), last };\n  return await versioner.getVersions(SOURCE.GITLAB, params);\n}\n\nexport default {\n  showLanding,\n  getPath,\n  getVersions,\n  logIn,\n  isLoggedIn,\n  LogInButton\n};\n"
  },
  {
    "path": "src/git-providers/language-detector.js",
    "content": "const filenameRegex = [\n  { lang: \"js\", regex: /\\.js$/i },\n  { lang: \"jsx\", regex: /\\.jsx$/i },\n  { lang: \"typescript\", regex: /\\.ts$/i },\n  { lang: \"tsx\", regex: /\\.tsx$/i },\n  { lang: \"json\", regex: /\\.json$|.babelrc$/i },\n  { lang: \"yaml\", regex: /\\.yaml$|.yml$/i },\n  { lang: \"bash\", regex: /\\.sh$/i },\n  { lang: \"python\", regex: /\\.py$/i },\n  { lang: \"dart\", regex: /\\.dart$/i },\n  { lang: \"perl\", regex: /\\.pl$|.pm$/i },\n  { lang: \"assembly\", regex: /\\.asm$/i },\n  { lang: \"groovy\", regex: /\\.groovy$/i },\n  { lang: \"sql\", regex: /\\.sql$/i },\n  { lang: \"css\", regex: /\\.css$/i },\n  { lang: \"less\", regex: /\\.less$/i },\n  { lang: \"scss\", regex: /\\.scss$/i },\n  { lang: \"ini\", regex: /\\.ini$|.editorconfig$/i },\n  { lang: \"markup\", regex: /\\.xml$|\\.html$|\\.htm$|\\.svg$|\\.mathml$/i },\n  { lang: \"batch\", regex: /\\.bat$/i },\n  { lang: \"clojure\", regex: /\\.clj$/i },\n  { lang: \"coffeescript\", regex: /\\.coffee$/i },\n  { lang: \"cpp\", regex: /\\.cpp$|\\.cc$/i },\n  { lang: \"csharp\", regex: /\\.cs$/i },\n  { lang: \"csp\", regex: /\\.csp$/i },\n  { lang: \"diff\", regex: /\\.diff$/i },\n  { lang: \"docker\", regex: /dockerfile$/i },\n  { lang: \"fsharp\", regex: /\\.fsharp$/i },\n  { lang: \"go\", regex: /\\.go$/i },\n  { lang: \"handlebars\", regex: /\\.hbs$/i },\n  { lang: \"haskell\", regex: /\\.hs$/i },\n  { lang: \"java\", regex: /\\.java$/i },\n  { lang: \"kotlin\", regex: /\\.kt$/i },\n  { lang: \"lua\", regex: /\\.lua$/i },\n  { lang: \"markdown\", regex: /\\.md$/i },\n  { lang: \"msdax\", regex: /\\.msdax$/i },\n  { lang: \"sql\", regex: /\\.mysql$/i },\n  { lang: \"objective-c\", regex: /\\.objc$/i },\n  { lang: \"pgsql\", regex: /\\.pgsql$/i },\n  { lang: \"php\", regex: /\\.php$/i },\n  { lang: \"postiats\", regex: /\\.postiats$/i },\n  { lang: \"powershell\", regex: /\\.ps$/i },\n  { lang: \"pug\", regex: /\\.pug$/i },\n  { lang: \"r\", regex: /\\.r$/i },\n  { lang: \"razor\", regex: /\\.razor$/i },\n  { lang: \"reason\", regex: /\\.re$/i },\n  { lang: \"ruby\", regex: /\\.rb$/i },\n  { lang: \"rust\", regex: /\\.rs$/i },\n  { lang: \"small basic\", regex: /\\.smallbasic$/i },\n  { lang: \"scala\", regex: /\\.scala$/i },\n  { lang: \"scheme\", regex: /\\.scheme$/i },\n  { lang: \"solidity\", regex: /\\.solidity$/i },\n  { lang: \"st\", regex: /\\.st$/i },\n  { lang: \"swift\", regex: /\\.swift$/i },\n  // { lang: \"toml\", regex: /\\.toml$/i },\n  { lang: \"vb\", regex: /\\.vb$/i },\n  { lang: \"wasm\", regex: /\\.wasm$/i },\n  // fallback\n  { lang: \"js\", regex: /.*/i }\n];\n\nexport function getLanguage(filename) {\n  return filenameRegex.find(x => x.regex.test(filename)).lang;\n}\n\nconst dependencies = {\n  cpp: [\"c\"],\n  tsx: [\"jsx\"],\n  scala: [\"java\"]\n};\n\nexport function getLanguageDependencies(lang) {\n  return dependencies[lang];\n}\n\nexport function loadLanguage(lang) {\n  if ([\"js\", \"css\", \"html\"].includes(lang)) {\n    return Promise.resolve();\n  }\n\n  const deps = getLanguageDependencies(lang);\n\n  let depPromise = import(\"prismjs\");\n\n  if (deps) {\n    depPromise = depPromise.then(() =>\n      Promise.all(deps.map(dep => import(`prismjs/components/prism-${dep}`)))\n    );\n  }\n\n  return depPromise.then(() => import(`prismjs/components/prism-${lang}`));\n}\n"
  },
  {
    "path": "src/git-providers/language-detector.test.js",
    "content": "import { getLanguage, getLanguageDependencies } from \"./language-detector\";\n\ndescribe(\"Can detect language\", () => {\n  test(\"javascript\", () => {\n    expect(getLanguage(\"my-file.js\")).toBe(\"js\");\n  });\n\n  test(\"jsx\", () => {\n    expect(getLanguage(\"my-file.jsx\")).toBe(\"jsx\");\n  });\n\n  test(\"typescript\", () => {\n    expect(getLanguage(\"my-file.ts\")).toBe(\"typescript\");\n  });\n\n  test(\"tsx\", () => {\n    expect(getLanguage(\"my-file.tsx\")).toBe(\"tsx\");\n  });\n\n  describe(\"json:\", () => {\n    test(\"json\", () => {\n      expect(getLanguage(\"my-file.json\")).toBe(\"json\");\n    });\n    test(\"babelrc\", () => {\n      expect(getLanguage(\"my-file.babelrc\")).toBe(\"json\");\n    });\n  });\n\n  describe(\"markup\", () => {\n    test(\"html\", () => {\n      expect(getLanguage(\"my-file.html\")).toBe(\"markup\");\n    });\n\n    test(\"htm\", () => {\n      expect(getLanguage(\"my-file.htm\")).toBe(\"markup\");\n    });\n\n    test(\"svg\", () => {\n      expect(getLanguage(\"my-file.svg\")).toBe(\"markup\");\n    });\n    test(\"xml\", () => {\n      expect(getLanguage(\"my-file.xml\")).toBe(\"markup\");\n    });\n  });\n\n  describe(\"yaml\", () => {\n    test(\"yaml\", () => {\n      expect(getLanguage(\"my-file.yaml\")).toBe(\"yaml\");\n    });\n\n    test(\"yml\", () => {\n      expect(getLanguage(\"my-file.yml\")).toBe(\"yaml\");\n    });\n  });\n\n  test(\"bash\", () => {\n    expect(getLanguage(\"my-file.sh\")).toBe(\"bash\");\n  });\n\n  test(\"pyhton\", () => {\n    expect(getLanguage(\"my-file.py\")).toBe(\"python\");\n  });\n\n  test(\"dart\", () => {\n    expect(getLanguage(\"my-file.dart\")).toBe(\"dart\");\n  });\n\n  describe(\"perl\", () => {\n    test(\"pl\", () => {\n      expect(getLanguage(\"my-file.pl\")).toBe(\"perl\");\n    });\n\n    test(\"pm\", () => {\n      expect(getLanguage(\"my-file.pm\")).toBe(\"perl\");\n    });\n  });\n\n  test(\"assembly\", () => {\n    expect(getLanguage(\"my-file.asm\")).toBe(\"assembly\");\n  });\n\n  test(\"groovy\", () => {\n    expect(getLanguage(\"my-file.groovy\")).toBe(\"groovy\");\n  });\n\n  test(\"sql\", () => {\n    expect(getLanguage(\"my-file.sql\")).toBe(\"sql\");\n  });\n\n  test(\"css\", () => {\n    expect(getLanguage(\"my-file.css\")).toBe(\"css\");\n  });\n\n  test(\"less\", () => {\n    expect(getLanguage(\"my-file.less\")).toBe(\"less\");\n  });\n\n  test(\"scss\", () => {\n    expect(getLanguage(\"my-file.scss\")).toBe(\"scss\");\n  });\n\n  describe(\"ini\", () => {\n    test(\"ini\", () => {\n      expect(getLanguage(\"my-file.ini\")).toBe(\"ini\");\n    });\n\n    test(\"editorconfig\", () => {\n      expect(getLanguage(\"my-file.editorconfig\")).toBe(\"ini\");\n    });\n  });\n\n  test(\"bat\", () => {\n    expect(getLanguage(\"my-file.bat\")).toBe(\"batch\");\n  });\n\n  test(\"clojure\", () => {\n    expect(getLanguage(\"my-file.clj\")).toBe(\"clojure\");\n  });\n\n  test(\"coffeescript\", () => {\n    expect(getLanguage(\"my-file.coffee\")).toBe(\"coffeescript\");\n  });\n\n  test(\"clojure\", () => {\n    expect(getLanguage(\"my-file.clj\")).toBe(\"clojure\");\n  });\n\n  describe(\"cpp\", () => {\n    test(\"cpp\", () => {\n      expect(getLanguage(\"my-file.cpp\")).toBe(\"cpp\");\n    });\n\n    test(\"cc\", () => {\n      expect(getLanguage(\"my-file.cc\")).toBe(\"cpp\");\n    });\n  });\n\n  test(\"csharp\", () => {\n    expect(getLanguage(\"my-file.cs\")).toBe(\"csharp\");\n  });\n\n  test(\"csp\", () => {\n    expect(getLanguage(\"my-file.csp\")).toBe(\"csp\");\n  });\n\n  test(\"diff\", () => {\n    expect(getLanguage(\"my-file.diff\")).toBe(\"diff\");\n  });\n\n  describe(\"docker\", () => {\n    test(\"long dockerfile\", () => {\n      expect(getLanguage(\"my-file.dockerfile\")).toBe(\"docker\");\n    });\n\n    test(\"dockerfile\", () => {\n      expect(getLanguage(\"Dockerfile\")).toBe(\"docker\");\n    });\n  });\n\n  test(\"fsharp\", () => {\n    expect(getLanguage(\"my-file.fsharp\")).toBe(\"fsharp\");\n  });\n\n  test(\"go\", () => {\n    expect(getLanguage(\"my-file.go\")).toBe(\"go\");\n  });\n\n  test(\"haskell\", () => {\n    expect(getLanguage(\"my-file.hs\")).toBe(\"haskell\");\n  });\n\n  test(\"java\", () => {\n    expect(getLanguage(\"my-file.java\")).toBe(\"java\");\n  });\n\n  test(\"kotlin\", () => {\n    expect(getLanguage(\"my-file.kt\")).toBe(\"kotlin\");\n  });\n\n  test(\"lua\", () => {\n    expect(getLanguage(\"my-file.lua\")).toBe(\"lua\");\n  });\n\n  test(\"markdown\", () => {\n    expect(getLanguage(\"my-file.md\")).toBe(\"markdown\");\n  });\n\n  test(\"msdax\", () => {\n    expect(getLanguage(\"my-file.msdax\")).toBe(\"msdax\");\n  });\n\n  test(\"sql\", () => {\n    expect(getLanguage(\"my-file.mysql\")).toBe(\"sql\");\n  });\n\n  test(\"objective-c\", () => {\n    expect(getLanguage(\"my-file.objc\")).toBe(\"objective-c\");\n  });\n\n  test(\"pgsql\", () => {\n    expect(getLanguage(\"my-file.pgsql\")).toBe(\"pgsql\");\n  });\n\n  test(\"php\", () => {\n    expect(getLanguage(\"my-file.php\")).toBe(\"php\");\n  });\n\n  test(\"postiats\", () => {\n    expect(getLanguage(\"my-file.postiats\")).toBe(\"postiats\");\n  });\n\n  test(\"powershell\", () => {\n    expect(getLanguage(\"my-file.ps\")).toBe(\"powershell\");\n  });\n\n  test(\"pug\", () => {\n    expect(getLanguage(\"my-file.pug\")).toBe(\"pug\");\n  });\n\n  test(\"r\", () => {\n    expect(getLanguage(\"my-file.r\")).toBe(\"r\");\n  });\n\n  test(\"razor\", () => {\n    expect(getLanguage(\"my-file.razor\")).toBe(\"razor\");\n  });\n\n  test(\"reason\", () => {\n    expect(getLanguage(\"my-file.re\")).toBe(\"reason\");\n  });\n\n  test(\"ruby\", () => {\n    expect(getLanguage(\"my-file.rb\")).toBe(\"ruby\");\n  });\n\n  test(\"rust\", () => {\n    expect(getLanguage(\"my-file.rs\")).toBe(\"rust\");\n  });\n\n  test(\"small basic\", () => {\n    expect(getLanguage(\"my-file.smallbasic\")).toBe(\"small basic\");\n  });\n\n  test(\"scala\", () => {\n    expect(getLanguage(\"my-file.scala\")).toBe(\"scala\");\n  });\n\n  test(\"scheme\", () => {\n    expect(getLanguage(\"my-file.scheme\")).toBe(\"scheme\");\n  });\n\n  test(\"solidity\", () => {\n    expect(getLanguage(\"my-file.solidity\")).toBe(\"solidity\");\n  });\n\n  test(\"swift\", () => {\n    expect(getLanguage(\"my-file.swift\")).toBe(\"swift\");\n  });\n\n  test(\"vb\", () => {\n    expect(getLanguage(\"my-file.vb\")).toBe(\"vb\");\n  });\n\n  test(\"wasm\", () => {\n    expect(getLanguage(\"my-file.wasm\")).toBe(\"wasm\");\n  });\n});\n\ndescribe(\"Fallback scenarios\", () => {\n  test(\"Random file extension\", () => {\n    expect(getLanguage(\"my-file.nonsense\")).toBe(\"js\");\n  });\n\n  test(\"No file extension\", () => {\n    expect(getLanguage(\"my-file\")).toBe(\"js\");\n  });\n\n  test(\"Empty string\", () => {\n    expect(getLanguage(\"\")).toBe(\"js\");\n  });\n});\n\ndescribe(\"Dependencies\", () => {\n  test(\"tsx\", () => {\n    expect(getLanguageDependencies(\"tsx\")).toEqual([\"jsx\"]);\n  });\n\n  test(\"cpp\", () => {\n    expect(getLanguageDependencies(\"cpp\")).toEqual([\"c\"]);\n  });\n});\n"
  },
  {
    "path": "src/git-providers/providers.js",
    "content": "const { SOURCE, getSource } = require(\"./sources\");\n\nlet providers;\nif (process.env.REACT_APP_GIT_PROVIDER === SOURCE.VSCODE) {\n  // We can't use web workers on vscode webview\n  providers = {\n    [SOURCE.VSCODE]: require(\"./vscode-provider\").default\n  };\n} else {\n  providers = {\n    [SOURCE.CLI]: require(\"./cli-provider\").default,\n    [SOURCE.GITLAB]: require(\"./gitlab-provider\").default,\n    [SOURCE.GITHUB]: require(\"./github-provider\").default,\n    [SOURCE.BITBUCKET]: require(\"./bitbucket-provider\").default\n  };\n}\n\nexport default function getGitProvider(source) {\n  source = source || getSource();\n  const provider = providers[source];\n  return provider;\n}\n"
  },
  {
    "path": "src/git-providers/sources.js",
    "content": "export const SOURCE = {\n  GITHUB: \"github\",\n  GITLAB: \"gitlab\",\n  BITBUCKET: \"bitbucket\",\n  CLI: \"cli\",\n  VSCODE: \"vscode\"\n};\n\nexport function getSource() {\n  if (process.env.REACT_APP_GIT_PROVIDER)\n    return process.env.REACT_APP_GIT_PROVIDER;\n\n  const [cloud] = window.location.host.split(\".\");\n  if ([SOURCE.GITLAB, SOURCE.GITHUB, SOURCE.BITBUCKET].includes(cloud)) {\n    return cloud;\n  }\n  const source = new URLSearchParams(window.location.search).get(\"source\");\n  return source || SOURCE.GITHUB;\n}\n"
  },
  {
    "path": "src/git-providers/tokenizer.js",
    "content": "// https://github.com/PrismJS/prism/issues/1303#issuecomment-375353987\nglobal.Prism = { disableWorkerMessageHandler: true };\nconst Prism = require(\"prismjs\");\n\nconst newlineRe = /\\r\\n|\\r|\\n/;\n\n// Take a list of nested tokens\n// (token.content may contain an array of tokens)\n// and flatten it so content is always a string\n// and type the type of the leaf\nfunction flattenTokens(tokens) {\n  const flatList = [];\n  tokens.forEach(token => {\n    if (Array.isArray(token.content)) {\n      flatList.push(...flattenTokens(token.content));\n    } else {\n      flatList.push(token);\n    }\n  });\n  return flatList;\n}\n\n// Convert strings to tokens\nfunction tokenizeStrings(prismTokens, parentType = \"plain\") {\n  return prismTokens.map(pt =>\n    typeof pt === \"string\"\n      ? { type: parentType, content: pt }\n      : {\n          type: pt.type,\n          content: Array.isArray(pt.content)\n            ? tokenizeStrings(pt.content, pt.type)\n            : pt.content\n        }\n  );\n}\n\nexport default function tokenize(code, language = \"javascript\") {\n  const prismTokens = Prism.tokenize(code, Prism.languages[language]);\n  const nestedTokens = tokenizeStrings(prismTokens);\n  const tokens = flattenTokens(nestedTokens);\n\n  let currentLine = [];\n  const lines = [currentLine];\n  tokens.forEach(token => {\n    const contentLines = token.content.split(newlineRe);\n\n    const firstContent = contentLines.shift();\n    if (firstContent !== \"\") {\n      currentLine.push({ type: token.type, content: firstContent });\n    }\n    contentLines.forEach(content => {\n      currentLine = [];\n      lines.push(currentLine);\n      if (content !== \"\") {\n        currentLine.push({ type: token.type, content });\n      }\n    });\n  });\n  return lines;\n}\n"
  },
  {
    "path": "src/git-providers/versioner.js",
    "content": "/* eslint-disable import/no-webpack-loader-syntax */\nimport worker from \"workerize-loader!./versioner.worker\";\nlet versioner = worker();\n\nexport default versioner;\n"
  },
  {
    "path": "src/git-providers/versioner.worker.js",
    "content": "import { getLanguage, loadLanguage } from \"./language-detector\";\nimport { getSlides, getChanges } from \"./differ\";\n\nimport github from \"./github-commit-fetcher\";\nimport gitlab from \"./gitlab-commit-fetcher\";\nimport bitbucket from \"./bitbucket-commit-fetcher\";\nimport cli from \"./cli-commit-fetcher\";\nimport { SOURCE } from \"./sources\";\n\nconst fetchers = {\n  [SOURCE.GITHUB]: github.getCommits,\n  [SOURCE.GITLAB]: gitlab.getCommits,\n  [SOURCE.BITBUCKET]: bitbucket.getCommits,\n  [SOURCE.CLI]: cli.getCommits\n};\n\nexport async function getVersions(source, params) {\n  const { path } = params;\n  const lang = getLanguage(path);\n  const langPromise = loadLanguage(lang);\n\n  const getCommits = fetchers[source];\n  const commits = await getCommits(params);\n  await langPromise;\n\n  const codes = commits.map(commit => commit.content);\n  const slides = getSlides(codes, lang);\n  return commits.map((commit, i) => ({\n    commit,\n    lines: slides[i],\n    changes: getChanges(slides[i])\n  }));\n}\n"
  },
  {
    "path": "src/git-providers/vscode-provider.js",
    "content": "import { getLanguage, loadLanguage } from \"./language-detector\";\nimport { getSlides, getChanges } from \"./differ\";\n\nconst vscode = window.vscode;\n\nfunction getPath() {\n  return window._PATH;\n}\n\nfunction showLanding() {\n  return false;\n}\n\nfunction getCommits(path, last) {\n  return new Promise((resolve, reject) => {\n    window.addEventListener(\n      \"message\",\n      event => {\n        const commits = event.data;\n        commits.forEach(c => (c.date = new Date(c.date)));\n        resolve(commits);\n      },\n      { once: true }\n    );\n\n    vscode.postMessage({\n      command: \"commits\",\n      params: {\n        path,\n        last\n      }\n    });\n  });\n}\n\nasync function getVersions(last) {\n  const path = getPath();\n  const lang = getLanguage(path);\n  const langPromise = loadLanguage(lang);\n\n  const commits = await getCommits(path, last);\n  await langPromise;\n\n  const codes = commits.map(commit => commit.content);\n  const slides = getSlides(codes, lang);\n  return commits.map((commit, i) => ({\n    commit,\n    lines: slides[i],\n    changes: getChanges(slides[i])\n  }));\n}\n\nexport default {\n  showLanding,\n  getPath,\n  getVersions\n};\n"
  },
  {
    "path": "src/history.js",
    "content": "import React, { useEffect, useState } from \"react\";\nimport useSpring from \"react-use/lib/useSpring\";\nimport Swipeable from \"react-swipeable\";\nimport Slide from \"./slide\";\nimport \"./comment-box.css\";\n\nfunction CommitInfo({ commit, move, onClick }) {\n  const message = commit.message.split(\"\\n\")[0].slice(0, 80);\n  const isActive = Math.abs(move) < 0.5;\n  return (\n    <div\n      style={{\n        position: \"absolute\",\n        left: \"50%\",\n        transform: `translateX(-50%) translateX(${250 * move}px)`,\n        opacity: 1 / (1 + Math.min(0.8, Math.abs(move))),\n        zIndex: !isActive && 2\n      }}\n    >\n      <div\n        style={{\n          display: \"flex\",\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          cursor: \"pointer\",\n          padding: \"5px 0 15px\"\n        }}\n        onClick={onClick}\n      >\n        {commit.author.avatar && (\n          <img\n            src={commit.author.avatar}\n            alt={commit.author.login}\n            height={40}\n            width={40}\n            style={{ borderRadius: \"4px\" }}\n          />\n        )}\n        <div style={{ paddingLeft: \"6px\" }}>\n          <div style={{ fontSize: \"1.1rem\", fontWeight: \"500\" }}>\n            {commit.author.login}\n          </div>\n          <div style={{ fontSize: \"0.85rem\", opacity: \"0.9\" }}>\n            {isActive && commit.commitUrl ? (\n              <a\n                href={commit.commitUrl}\n                target=\"_blank\"\n                rel=\"noopener noreferrer\"\n              >\n                on {commit.date.toDateString()}\n              </a>\n            ) : (\n              `on ${commit.date.toDateString()}`\n            )}\n          </div>\n        </div>\n      </div>\n      {isActive && (\n        <div\n          className=\"comment-box\"\n          title={commit.message}\n          style={{ opacity: 1 - 2 * Math.abs(move) }}\n        >\n          {message}\n          {message !== commit.message ? \" ...\" : \"\"}\n        </div>\n      )}\n    </div>\n  );\n}\n\nfunction CommitList({ commits, currentIndex, selectCommit }) {\n  const mouseWheelEvent = e => {\n    e.preventDefault();\n    selectCommit(currentIndex - (e.deltaX + e.deltaY) / 100);\n  };\n  return (\n    <div\n      onWheel={mouseWheelEvent}\n      style={{\n        overflow: \"hidden\",\n        width: \"100%\",\n        height: \"100px\",\n        position: \"fixed\",\n        top: 0,\n        background: \"rgb(1, 22, 39)\",\n        zIndex: 1\n      }}\n    >\n      {commits.map((commit, commitIndex) => (\n        <CommitInfo\n          commit={commit}\n          move={currentIndex - commitIndex}\n          key={commitIndex}\n          onClick={() => selectCommit(commitIndex)}\n        />\n      ))}\n    </div>\n  );\n}\n\nexport default function History({ versions, loadMore }) {\n  return <Slides versions={versions} loadMore={loadMore} />;\n}\n\nfunction Slides({ versions, loadMore }) {\n  const [current, target, setTarget] = useSliderSpring(0);\n  const commits = versions.map(v => v.commit);\n  const setClampedTarget = newTarget => {\n    setTarget(Math.min(commits.length - 0.75, Math.max(-0.25, newTarget)));\n    if (newTarget >= commits.length - 5) {\n      loadMore();\n    }\n  };\n  const index = Math.round(current);\n  const nextSlide = () => setClampedTarget(Math.round(target - 0.51));\n  const prevSlide = () => setClampedTarget(Math.round(target + 0.51));\n  useEffect(() => {\n    document.body.onkeydown = function(e) {\n      if (e.keyCode === 39) {\n        nextSlide();\n      } else if (e.keyCode === 37) {\n        prevSlide();\n      } else if (e.keyCode === 32) {\n        setClampedTarget(current);\n      }\n    };\n  });\n\n  return (\n    <React.Fragment>\n      <CommitList\n        commits={commits}\n        currentIndex={current}\n        selectCommit={index => setClampedTarget(index)}\n      />\n      <Swipeable\n        onSwipedLeft={nextSlide}\n        onSwipedRight={prevSlide}\n        style={{ height: \"100%\" }}\n      >\n        <Slide time={index - current} version={versions[index]} />\n      </Swipeable>\n    </React.Fragment>\n  );\n}\n\n// TODO use ./useSpring\nfunction useSliderSpring(initial) {\n  const [target, setTarget] = useState(initial);\n  const tension = 0;\n  const friction = 10;\n  const value = useSpring(target, tension, friction);\n  return [Math.round(value * 100) / 100, target, setTarget];\n}\n"
  },
  {
    "path": "src/index.js",
    "content": "import App from \"./app\";\nimport React from \"react\";\nimport ReactDOM from \"react-dom\";\n\nconst root = document.getElementById(\"root\");\nReactDOM.render(\n  <React.unstable_ConcurrentMode>\n    <App />\n  </React.unstable_ConcurrentMode>,\n  root\n);\n"
  },
  {
    "path": "src/landing.css",
    "content": ".extensions {\n  display: flex;\n  justify-content: center;\n  padding-bottom: 10px;\n}\n\n.extensions > * {\n  padding: 4px 10px;\n}\n\n.landing a {\n  color: inherit;\n}\n\n.landing {\n  color: #222;\n  background: #fafafa;\n}\n\n.landing > * {\n  background: linear-gradient(rgba(255, 255, 255), rgba(220, 220, 220));\n}\n\n.landing header {\n  display: flex;\n  padding: 100px 0px;\n}\n.landing h1 {\n  margin-top: 10px;\n}\n\n.landing header a {\n  color: rgb(1, 22, 39);\n}\n\n.landing header a.button {\n  background: rgb(1, 22, 39);\n  color: #fafafa;\n  padding: 9px 16px;\n  margin: 10px auto 15px;\n  width: 80px;\n  text-align: center;\n  border-radius: 4px;\n  text-decoration: none;\n  display: block;\n}\n\n.landing header video {\n  margin-right: 115px;\n}\n\n@media (max-width: 1130px) {\n  .landing header video {\n    margin-right: 0px;\n    margin-bottom: 20px;\n    max-width: 80%;\n    height: auto !important;\n    min-width: 350px;\n  }\n  .landing header {\n    flex-direction: column;\n    padding: 40px 0px 20px;\n  }\n  .landing header .summary {\n    width: 560px;\n    max-width: 80%;\n  }\n}\n\n.landing .testimonies {\n  font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n  display: grid;\n  width: 800px;\n  margin: 10px auto;\n  grid-template-columns: 400px 400px;\n  grid-template-rows: 180px 150px;\n  grid-column-gap: 14px;\n  grid-row-gap: 14px;\n}\n\n@media (max-width: 900px) {\n  .landing .testimonies {\n    grid-template-columns: 400px;\n    grid-template-rows: 180px 180px 150px 150px;\n    width: 400px;\n  }\n}\n@media (max-width: 420px) {\n  .landing .testimonies {\n    grid-template-columns: auto;\n    grid-template-rows: auto;\n    width: 90%;\n  }\n}\n\n.landing .testimonies > * {\n  border: 1px solid #e1e8ed;\n  display: inline-block;\n  text-decoration: none;\n  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\n  border-radius: 5px;\n}\n\n.landing blockquote {\n  display: flex;\n  flex-direction: column;\n  height: 100%;\n  margin: 0;\n  padding: 19px;\n  box-sizing: border-box;\n}\n\n.landing blockquote p {\n  flex: 1;\n  margin: 0 0 12px 0;\n}\n\n.landing blockquote img {\n  height: 36px;\n  width: 36px;\n  border-radius: 50%;\n}\n\n.landing .support > div {\n  width: 800px;\n}\n\n@media (max-width: 900px) {\n  .landing .support > div {\n    width: 600px;\n  }\n}\n@media (max-width: 600px) {\n  .landing .support > div {\n    width: 350px;\n  }\n}\n"
  },
  {
    "path": "src/landing.js",
    "content": "import React from \"react\";\nimport demoMp4 from \"./demo.mp4\";\nimport demoWebm from \"./demo.webm\";\nimport smashing from \"./avatar.smashing.jpg\";\nimport github from \"./avatar.github.jpg\";\nimport addy from \"./avatar.addy.jpg\";\nimport cssTricks from \"./avatar.css-tricks.jpg\";\nimport { ReactComponent as ChromeLogo } from \"./icons/chrome.svg\";\nimport { ReactComponent as FirefoxLogo } from \"./icons/firefox.svg\";\nimport { ReactComponent as CliLogo } from \"./icons/cli.svg\";\nimport { ReactComponent as VsCodeLogo } from \"./icons/vscode.svg\";\nimport \"./landing.css\";\n\nexport default function Landing() {\n  const url = `${window.location.protocol}//${\n    window.location.host\n  }/babel/babel/blob/master/packages/babel-core/test/browserify.js`;\n  return (\n    <div className=\"landing\">\n      <header\n        style={{\n          alignItems: \"center\",\n          justifyContent: \"center\",\n          color: \"#222\",\n        }}\n      >\n        <video\n          autoPlay\n          loop\n          muted\n          playsInline\n          width=\"560\"\n          height=\"350\"\n          style={{\n            borderRadius: \"3px\",\n            height: \"350\",\n            boxShadow: \"0 20px 50px 0 rgba(0,0,0,0.2)\",\n          }}\n        >\n          <source src={demoWebm} type=\"video/webm\" />\n          <source src={demoMp4} type=\"video/mp4\" />\n        </video>\n        <div className=\"summary\">\n          <h1>Git History</h1>\n          Quickly browse the history of files in any git repo:\n          <ol>\n            <li>\n              Go to a file in <strong>GitHub</strong> (or{\" \"}\n              <strong>GitLab</strong>, or <strong>Bitbucket</strong>)\n            </li>\n            <li>\n              Replace <i>github.com</i> with <i>github.githistory.xyz</i>\n            </li>\n            <li>There's no step three</li>\n          </ol>\n          <a className=\"button\" href={url}>\n            Try it\n          </a>\n          <p style={{ marginBottom: \"7px\" }}>Also available as extensions:</p>\n          <div className=\"extensions\">\n            <a\n              href=\"https://chrome.google.com/webstore/detail/github-history-browser-ex/laghnmifffncfonaoffcndocllegejnf\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <ChromeLogo height={44} width={44} />\n            </a>\n            <a\n              href=\"https://addons.mozilla.org/firefox/addon/github-history/\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <FirefoxLogo height={44} width={44} />\n            </a>\n\n            <a\n              href=\"https://github.com/pomber/git-history/tree/master/cli\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <CliLogo height={44} width={44} />\n            </a>\n\n            <a\n              href=\"https://marketplace.visualstudio.com/items?itemName=pomber.git-file-history\"\n              target=\"_blank\"\n              rel=\"noopener noreferrer\"\n            >\n              <VsCodeLogo height={44} width={44} />\n            </a>\n          </div>\n          <div\n            style={{\n              display: \"flex\",\n              alignItems: \"center\",\n              justifyContent: \"space-between\",\n            }}\n          >\n            <iframe\n              src=\"https://ghbtns.com/github-btn.html?user=pomber&repo=git-history&type=star&count=true&size=large\"\n              title=\"GitHub Stars\"\n              frameBorder=\"0\"\n              scrolling=\"0\"\n              width=\"160px\"\n              height=\"30px\"\n            />\n            <i>\n              by <a href=\"https://twitter.com/pomber\">@pomber</a>\n            </i>\n          </div>\n        </div>\n      </header>\n      <Testimonies />\n      <Backers />\n    </div>\n  );\n}\n\nfunction Testimonies() {\n  return (\n    <section style={{ margin: \"45px 0px 60px\", background: \"#fafafa\" }}>\n      <h2 style={{ textAlign: \"center\" }}>What people are saying...</h2>\n      <div className=\"testimonies\">\n        <Testimony\n          name=\"GitHub\"\n          link=\"https://github.blog/2019-03-01-release-radar-february-2019/#git-history\"\n          avatar={github}\n        >\n          Git History caught our eye with a beautiful way to tour the history of\n          a file in a GitHub repo. ... there’s nothing to download and install:\n          point Git History to a repository file URL to start traveling through\n          time. Great Scott!\n        </Testimony>\n        <Testimony\n          name=\"Smashing Magazine\"\n          link=\"https://twitter.com/smashingmag/status/1094865325974261761\"\n          avatar={smashing}\n        >\n          Ahh you know when you need to browse your Git history but it takes a\n          while to find what you are looking for? Git History lets you browse\n          the history in no-time. Useful.\n        </Testimony>\n        <Testimony\n          name=\"CSS-Tricks\"\n          link=\"https://twitter.com/css/status/1105999990814662656\"\n          avatar={cssTricks}\n        >\n          I love little apps like this that copy the URL structure of another\n          app, so you can replace just the TLD and it does something useful.\n        </Testimony>\n        <Testimony\n          name=\"Addy Osmani\"\n          link=\"https://twitter.com/addyosmani/status/1093970927413387264\"\n          avatar={addy}\n        >\n          There's something really satisfying about browsing file history with\n          this timeline UI. It's super nice.\n        </Testimony>\n      </div>\n    </section>\n  );\n}\n\nfunction Testimony({ link, avatar, name, children }) {\n  return (\n    <a href={link} target=\"_blank\" rel=\"noopener noreferrer\">\n      <blockquote>\n        <p>{children}</p>\n        <cite style={{ display: \"flex\", alignItems: \"center\" }}>\n          <img src={avatar} alt=\"avatar\" />\n          <strong style={{ paddingLeft: \"10px\", fontStyle: \"normal\" }}>\n            {name}\n          </strong>\n        </cite>\n      </blockquote>\n    </a>\n  );\n}\n\nfunction ResponsivePicture({ link, src, alt, append = \"\" }) {\n  return (\n    <a href={link} target=\"_blank\" rel=\"noopener noreferrer\">\n      <picture>\n        <source\n          srcSet={src + \"?width=800\" + append}\n          media=\"(min-width: 900px)\"\n        />\n        <source\n          srcSet={src + \"?width=600\" + append}\n          media=\"(min-width: 600px)\"\n        />\n        <img src={src + \"?width=350\" + append} alt={alt} />\n      </picture>\n    </a>\n  );\n}\n\nfunction Backers() {\n  return (\n    <section\n      style={{\n        padding: \"28px 0px\",\n        background: \"linear-gradient(rgba(220, 220, 220),rgba(255, 255, 255))\",\n      }}\n      className=\"support\"\n    >\n      <h2 style={{ textAlign: \"center\" }}>Support Git History</h2>\n      <div style={{ margin: \"20px auto\" }}>\n        <h3>Sponsors</h3>\n        <p>\n          Support this project by becoming a sponsor. Your logo will show up\n          here with a link to your website.{\" \"}\n          <a href=\"https://opencollective.com/git-history#sponsor\">\n            Become a sponsor\n          </a>\n        </p>\n        <a\n          href=\"https://github.com/selefra/selefra\"\n          target=\"_blank\"\n          rel=\"noopener noreferrer\"\n        >\n          <img\n            src=\"https://github.com/selefra.png\"\n            style={{ borderRadius: \"50%\" }}\n            alt=\"selefra\"\n            title=\"Selefra\"\n            width=\"100\"\n          />\n        </a>\n        <br />\n        <ResponsivePicture\n          link=\"https://opencollective.com/git-history/sponsor/0/website\"\n          alt=\"sponsors\"\n          src=\"https://opencollective.com/git-history/sponsor/0/avatar.svg\"\n        />\n        <h3>Backers</h3>\n        <p>\n          Thank you to all our backers!{\" \"}\n          <span role=\"img\" aria-label=\"thanks\">\n            🙏\n          </span>\n          .{\" \"}\n          <a href=\"https://opencollective.com/git-history#sponsor\">\n            Become a backer to help us ship more features!\n          </a>\n        </p>\n        <ResponsivePicture\n          link=\"https://opencollective.com/git-history#backers\"\n          alt=\"Backers\"\n          src=\"https://opencollective.com/git-history/backers.svg\"\n        />\n        <h3>Thanks</h3>\n        <p>\n          Browser testing via{\" \"}\n          <a\n            href=\"https://www.lambdatest.com/\"\n            target=\"_blank\"\n            rel=\"noopener noreferrer\"\n          >\n            <img\n              src=\"https://www.lambdatest.com/resources/images/logos/logo.svg\"\n              style={{\n                verticalAlign: \"middle\",\n                marginLeft: \"5px\",\n              }}\n              width=\"147\"\n              height=\"26\"\n              alt=\"LambdaTest\"\n            />\n          </a>\n        </p>\n      </div>\n    </section>\n  );\n}\n"
  },
  {
    "path": "src/nightOwl.js",
    "content": "const theme = {\n  plain: {\n    color: \"#d6deeb\",\n    backgroundColor: \"#011627\"\n  },\n  styles: [\n    {\n      types: [\"changed\"],\n      style: {\n        color: \"rgb(162, 191, 252)\",\n        fontStyle: \"italic\"\n      }\n    },\n    {\n      types: [\"deleted\"],\n      style: {\n        color: \"rgba(239, 83, 80, 0.56)\",\n        fontStyle: \"italic\"\n      }\n    },\n    {\n      types: [\"inserted\", \"attr-name\"],\n      style: {\n        color: \"rgb(173, 219, 103)\",\n        fontStyle: \"italic\"\n      }\n    },\n    {\n      types: [\"comment\"],\n      style: {\n        color: \"rgb(99, 119, 119)\",\n        fontStyle: \"italic\"\n      }\n    },\n    {\n      types: [\"string\", \"url\"],\n      style: {\n        color: \"rgb(173, 219, 103)\"\n      }\n    },\n    {\n      types: [\"variable\"],\n      style: {\n        color: \"rgb(214, 222, 235)\"\n      }\n    },\n    {\n      types: [\"number\"],\n      style: {\n        color: \"rgb(247, 140, 108)\"\n      }\n    },\n    {\n      types: [\"builtin\", \"char\", \"constant\", \"function\"],\n      style: {\n        color: \"rgb(130, 170, 255)\"\n      }\n    },\n    {\n      // This was manually added after the auto-generation\n      // so that punctuations are not italicised\n      types: [\"punctuation\"],\n      style: {\n        color: \"rgb(199, 146, 234)\"\n      }\n    },\n    {\n      types: [\"selector\", \"doctype\"],\n      style: {\n        color: \"rgb(199, 146, 234)\",\n        fontStyle: \"italic\"\n      }\n    },\n    {\n      types: [\"class-name\"],\n      style: {\n        color: \"rgb(255, 203, 139)\"\n      }\n    },\n    {\n      types: [\"tag\", \"operator\", \"keyword\"],\n      style: {\n        color: \"rgb(127, 219, 202)\"\n      }\n    },\n    {\n      types: [\"boolean\"],\n      style: {\n        color: \"rgb(255, 88, 116)\"\n      }\n    },\n    {\n      types: [\"property\"],\n      style: {\n        color: \"rgb(128, 203, 196)\"\n      }\n    },\n    {\n      types: [\"namespace\"],\n      style: {\n        color: \"rgb(178, 204, 214)\"\n      }\n    }\n  ]\n};\n\nmodule.exports = theme;\n"
  },
  {
    "path": "src/scroller.css",
    "content": ".scroller::-webkit-scrollbar {\n  background-color: rgba(255, 255, 255, 0.01);\n}\n\n.scroller::-webkit-scrollbar-thumb {\n  background-color: rgb(173, 219, 103, 0.3);\n}\n"
  },
  {
    "path": "src/scroller.js",
    "content": "import React from \"react\";\nimport useChildren from \"./use-virtual-children\";\nimport \"./scroller.css\";\nimport useSpring from \"./use-spring\";\nimport { nextIndex, prevIndex, getScrollTop } from \"./utils\";\n\nconst initialState = {\n  snap: false,\n  targetTop: 0,\n  currentTop: 0,\n  areaIndex: 0\n};\n\nexport default function Scroller({\n  items,\n  getRow,\n  getRowHeight,\n  data,\n  snapAreas\n}) {\n  const ref = React.useRef();\n  const height = useHeight(ref);\n\n  const reducer = (prevState, action) => {\n    switch (action.type) {\n      case \"unsnap\":\n        return !prevState.snap ? prevState : { ...prevState, snap: false };\n      case \"change-area\":\n        if (snapAreas.length === 0) {\n          return prevState;\n        }\n\n        const { changeIndex, recalculate } = action;\n        const movingFromUnknownIndex = !prevState.snap || recalculate;\n\n        // TODO memo\n        const heights = items.map((item, i) => getRowHeight(item, i, data));\n\n        let newIndex;\n        if (movingFromUnknownIndex) {\n          //todo memo\n          const oldIndex = getAreaIndex(\n            prevState.targetTop,\n            snapAreas,\n            heights,\n            height\n          );\n\n          newIndex = changeIndex(snapAreas, oldIndex);\n        } else {\n          newIndex = changeIndex(snapAreas, prevState.areaIndex);\n        }\n\n        if (newIndex === prevState.areaIndex && !movingFromUnknownIndex) {\n          return prevState;\n        }\n\n        // TODO  memo\n        let contentHeight = heights.reduce((a, b) => a + b, 0);\n\n        const targetTop = getScrollTop(\n          snapAreas[newIndex],\n          contentHeight,\n          height,\n          heights\n        );\n\n        return {\n          ...prevState,\n          areaIndex: newIndex,\n          snap: true,\n          currentTop: null,\n          targetTop\n        };\n      case \"manual-scroll\":\n        const { newTop } = action;\n        if (newTop === prevState.currentTop && !prevState.snap) {\n          return prevState;\n        }\n        // console.log(\"manual scroll\", newTop);\n        return {\n          ...prevState,\n          snap: false,\n          currentTop: newTop,\n          targetTop: newTop\n        };\n      default:\n        throw Error();\n    }\n  };\n\n  const [{ snap, targetTop, currentTop }, dispatch] = React.useReducer(\n    reducer,\n    initialState\n  );\n\n  const top = useSpring({\n    target: targetTop,\n    current: currentTop,\n    round: Math.round\n  });\n  // console.log(\"render\", targetTop, top);\n\n  const children = useChildren({\n    height,\n    top,\n    items,\n    getRow,\n    getRowHeight,\n    data\n  });\n\n  React.useEffect(() => {\n    document.body.addEventListener(\"keydown\", e => {\n      if (e.keyCode === 38) {\n        dispatch({ type: \"change-area\", changeIndex: prevIndex });\n        e.preventDefault();\n      } else if (e.keyCode === 40) {\n        dispatch({ type: \"change-area\", changeIndex: nextIndex });\n        e.preventDefault();\n      }\n    });\n  }, []);\n\n  // Auto-scroll to closest change when changing versions:\n  // React.useLayoutEffect(() => {\n  //   dispatch({\n  //     type: \"change-area\",\n  //     recalculate: true,\n  //     changeIndex: closestIndex\n  //   });\n  // }, [snapAreas]);\n\n  React.useEffect(() => {\n    dispatch({\n      type: \"unsnap\"\n    });\n  }, [snapAreas]);\n\n  React.useLayoutEffect(() => {\n    if (snap) {\n      ref.current.scrollTop = top;\n    }\n  }, [snap, top]);\n\n  return (\n    <div\n      style={{\n        height: \"100%\",\n        overflowY: \"auto\",\n        overflowX: \"hidden\"\n      }}\n      className=\"scroller\"\n      ref={ref}\n      onScroll={e => {\n        const newTop = e.target.scrollTop;\n        if (newTop === top) {\n          return;\n        }\n        dispatch({ type: \"manual-scroll\", newTop });\n      }}\n    >\n      <code\n        style={{\n          display: \"block\",\n          width: \"calc(100% - 20px)\",\n          maxWidth: \"900px\",\n          margin: \"auto\",\n          padding: \"10px\",\n          boxSizing: \"border-box\",\n          height: \"100%\"\n        }}\n        children={children}\n      />\n    </div>\n  );\n}\n\nfunction getAreaIndex(scrollTop, areas, heights, containerHeight) {\n  if (areas.length === 0) {\n    return 0;\n  }\n\n  const scrollMiddle = scrollTop + containerHeight / 2;\n\n  let h = 0;\n  let i = 0;\n  while (scrollMiddle > h) {\n    h += heights[i++];\n  }\n  const middleRow = i;\n\n  const areaCenters = areas.map(a => (a.start + a.end) / 2);\n  areaCenters.unshift(0);\n  for (let a = 0; a < areas.length; a++) {\n    if (middleRow < areaCenters[a + 1]) {\n      return (\n        a -\n        (areaCenters[a + 1] - middleRow) / (areaCenters[a + 1] - areaCenters[a])\n      );\n    }\n  }\n\n  return areas.length - 0.9;\n}\n\nfunction useHeight(ref) {\n  let [height, setHeight] = React.useState(null);\n\n  function handleResize() {\n    setHeight(ref.current.clientHeight);\n  }\n\n  React.useEffect(() => {\n    handleResize();\n    window.addEventListener(\"resize\", handleResize);\n    return () => {\n      window.removeEventListener(\"resize\", handleResize);\n    };\n  }, []);\n\n  return height;\n}\n"
  },
  {
    "path": "src/scroller.story.js",
    "content": "import React from \"react\";\nimport { storiesOf } from \"@storybook/react\";\nimport Scroller from \"./scroller\";\n\nconst snapAreas1 = [\n  { start: 1, end: 5 },\n  { start: 15, end: 26 },\n  { start: 50, end: 100 },\n  { start: 300, end: 302 }\n];\n\nconst snapAreas2 = [\n  { start: 8, end: 12 },\n  { start: 30, end: 32 },\n  { start: 550, end: 552 },\n  { start: 595, end: 599 }\n];\n\nconst items = Array(600)\n  .fill(0)\n  .map((_, i) => {\n    const a1 = snapAreas1.find(a => a.start <= i && i <= a.end);\n    const a2 = snapAreas2.find(a => a.start <= i && i <= a.end);\n    return {\n      content: `Row ${i}${\n        a1\n          ? ` - Area1 [${a1.start}, ${a1.end}]`\n          : a2\n          ? ` - Area2 [${a2.start}, ${a2.end}]`\n          : \"\"\n      }`,\n      key: i,\n      height: 22\n    };\n  });\n\nfunction getRow(item) {\n  return (\n    <div key={item.key} style={{ height: item.height }}>\n      {item.content}\n    </div>\n  );\n}\n\nfunction getRowHeight(item) {\n  return item.height;\n}\n\nfunction BasicScroller({ areas }) {\n  const [top, setTop] = React.useState(40);\n  return (\n    <Scroller\n      items={items}\n      snapAreas={areas}\n      getRow={getRow}\n      getRowHeight={getRowHeight}\n      top={top}\n      setTop={setTop}\n    />\n  );\n}\n\nstoriesOf(\"Scroller\", module).add(\"single\", () => (\n  <div\n    style={{\n      display: \"flex\",\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      height: \"90vh\"\n    }}\n  >\n    <div style={{ width: \"60%\", height: \"80vh\", border: \"1px solid black\" }}>\n      <BasicScroller areas={snapAreas1} />\n    </div>\n  </div>\n));\n\nfunction DoubleScroller() {\n  const [flag, setFlag] = React.useState(false);\n  return (\n    <div>\n      <div style={{ height: \"80vh\", border: \"1px solid black\", width: \"60vw\" }}>\n        <BasicScroller areas={flag ? snapAreas1 : snapAreas2} />\n      </div>\n      <div>\n        {flag ? \"Areas 1\" : \"Areas 2\"}\n        <button onClick={() => setFlag(flag => !flag)}>Toggle</button>\n      </div>\n    </div>\n  );\n}\n\nstoriesOf(\"Scroller\", module).add(\"multiple\", () => (\n  <div\n    style={{\n      display: \"flex\",\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      height: \"90vh\"\n    }}\n  >\n    <DoubleScroller />\n  </div>\n));\n"
  },
  {
    "path": "src/slide.js",
    "content": "import React from \"react\";\nimport animation from \"./animation\";\nimport theme from \"./nightOwl\";\nimport Scroller from \"./scroller\";\n\nconst themeStylesByType = Object.create(null);\ntheme.styles.forEach(({ types, style }) => {\n  types.forEach(type => {\n    themeStylesByType[type] = Object.assign(\n      themeStylesByType[type] || {},\n      style\n    );\n  });\n});\n\nfunction getLineHeight(line, i, { styles }) {\n  return styles[i].height != null ? styles[i].height : 15;\n}\n\nfunction getLine(line, i, { styles }) {\n  const style = styles[i];\n  return (\n    <div\n      style={Object.assign({ overflow: \"hidden\", height: \"15px\" }, style)}\n      key={line.key}\n    >\n      {!line.tokens.length && <br />}\n      {line.tokens.map((token, i) => {\n        const style = themeStylesByType[token.type] || {};\n        return (\n          <span style={style} key={i}>\n            {token.content}\n          </span>\n        );\n      })}\n    </div>\n  );\n}\n\nfunction Slide({ lines, styles, changes }) {\n  return (\n    <pre\n      style={{\n        backgroundColor: theme.plain.backgroundColor,\n        color: theme.plain.color,\n        paddingTop: \"100px\",\n        margin: 0,\n        height: \"100%\",\n        width: \"100%\",\n        boxSizing: \"border-box\"\n      }}\n    >\n      <Scroller\n        items={lines}\n        getRow={getLine}\n        getRowHeight={getLineHeight}\n        data={{ styles }}\n        snapAreas={changes}\n      />\n    </pre>\n  );\n}\n\nexport default function SlideWrapper({ time, version }) {\n  const { lines, changes } = version;\n  const styles = animation((time + 1) / 2, lines);\n  return <Slide lines={lines} styles={styles} changes={changes} />;\n}\n"
  },
  {
    "path": "src/use-spring.js",
    "content": "// based on https://github.com/streamich/react-use/blob/master/src/useSpring.ts\nimport { SpringSystem } from \"rebound\";\nimport { useState, useEffect } from \"react\";\n\nexport default function useSpring({\n  target = 0,\n  current = null,\n  tension = 0,\n  friction = 10,\n  round = x => x\n}) {\n  const [spring, setSpring] = useState(null);\n  const [value, setValue] = useState(target);\n\n  useEffect(() => {\n    const listener = {\n      onSpringUpdate: spring => {\n        const value = spring.getCurrentValue();\n        setValue(round(value));\n      }\n    };\n\n    if (!spring) {\n      const newSpring = new SpringSystem().createSpring(tension, friction);\n      newSpring.setCurrentValue(target);\n      setSpring(newSpring);\n      newSpring.addListener(listener);\n      return;\n    }\n\n    return () => {\n      spring.removeListener(listener);\n      setSpring(null);\n    };\n  }, [tension, friction]);\n\n  useEffect(() => {\n    if (spring) {\n      spring.setEndValue(target);\n      if (current != null) {\n        spring.setCurrentValue(current);\n      }\n    }\n  }, [target, current]);\n\n  return value;\n}\n"
  },
  {
    "path": "src/use-spring.story.js",
    "content": "import React from \"react\";\nimport { storiesOf } from \"@storybook/react\";\nimport useSpring from \"./use-spring\";\n\nfunction Test() {\n  const [{ target, current }, setState] = React.useState({\n    target: 0,\n    current: null\n  });\n  const value = useSpring({ target, current });\n\n  return (\n    <div>\n      <div style={{ display: \"flex\" }}>\n        <span style={{ flex: 0.3 }}>Target</span>\n        <input\n          value={target}\n          onChange={e =>\n            setState({\n              target: +e.target.value,\n              current: null\n            })\n          }\n          style={{ flex: 1 }}\n          type=\"range\"\n        />\n        <span style={{ flex: 0.3 }}>{target}</span>\n      </div>\n      <div style={{ display: \"flex\" }}>\n        <span style={{ flex: 0.3 }}>Current</span>\n        <input\n          value={current}\n          onChange={e =>\n            setState({\n              target: +e.target.value,\n              current: +e.target.value\n            })\n          }\n          style={{ flex: 1 }}\n          type=\"range\"\n        />\n        <span style={{ flex: 0.3 }}>{current}</span>\n      </div>\n      <div style={{ display: \"flex\" }}>\n        <span style={{ flex: 0.3 }}>Value</span>\n        <input value={value} type=\"range\" readOnly style={{ flex: 1 }} />\n        <span style={{ flex: 0.3 }}>{Math.round(value * 1000) / 1000}</span>\n      </div>\n    </div>\n  );\n}\n\nstoriesOf(\"useSpring\", module).add(\"test\", () => (\n  <div\n    style={{\n      display: \"flex\",\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      height: \"90vh\"\n    }}\n  >\n    <div style={{ width: \"60%\" }}>\n      <Test />\n    </div>\n  </div>\n));\n"
  },
  {
    "path": "src/use-virtual-children.js",
    "content": "import React from \"react\";\n\nexport default function useChildren({\n  items,\n  getRow,\n  getRowHeight,\n  height,\n  top,\n  data\n}) {\n  const children = [];\n\n  const extraRender = 1000;\n\n  const topT = top - extraRender;\n  const bottomT = top + height + extraRender;\n  let h = 0;\n\n  let topPlaceHolderH = 0;\n  let bottomPlaceholderH = 0;\n\n  // This is the bottleneck\n  items.forEach((item, i) => {\n    const itemH = getRowHeight(item, i, data);\n    const nextH = h + itemH;\n    const isOverTop = nextH < topT;\n    const isUnderBottom = h > bottomT;\n\n    if (isOverTop) {\n      topPlaceHolderH += itemH;\n    } else if (isUnderBottom) {\n      bottomPlaceholderH += itemH;\n    } else {\n      children.push(getRow(item, i, data));\n    }\n\n    h = nextH;\n  });\n\n  children.unshift(<div style={{ height: topPlaceHolderH }} key=\"top-ph\" />);\n  children.push(<div style={{ height: bottomPlaceholderH }} key=\"bottom-ph\" />);\n  return children;\n}\n"
  },
  {
    "path": "src/utils.js",
    "content": "export function nextIndex(list, currentIndex) {\n  return Math.min(list.length - 1, Math.floor(currentIndex + 1));\n}\n\nexport function prevIndex(list, currentIndex) {\n  return Math.max(0, Math.ceil(currentIndex - 1));\n}\n\nexport function closestIndex(list, currentIndex) {\n  return Math.min(Math.max(0, Math.round(currentIndex)), list.length - 1);\n}\n\nexport function getScrollTop(area, contentHeight, containerHeight, heights) {\n  const start = heights.slice(0, area.start).reduce((a, b) => a + b, 0);\n  const end =\n    start + heights.slice(area.start, area.end + 1).reduce((a, b) => a + b, 0);\n  const middle = (end + start) / 2;\n  const halfContainer = containerHeight / 2;\n  const bestTop =\n    end - start > containerHeight ? start : middle - halfContainer;\n  if (bestTop < 0) return 0;\n  if (bestTop + containerHeight > contentHeight) {\n    return contentHeight - containerHeight;\n  }\n  return bestTop;\n}\n"
  },
  {
    "path": "src/utils.test.js",
    "content": "import { nextIndex, prevIndex } from \"./utils\";\n\ndescribe(\"nextIndex\", () => {\n  const fiveItems = [1, 2, 3, 4, 5];\n  test(\"works with middle index\", () => {\n    expect(nextIndex(fiveItems, 2)).toBe(3);\n  });\n  test(\"works with last index\", () => {\n    expect(nextIndex(fiveItems, 4)).toBe(4);\n  });\n  test(\"works with fractions\", () => {\n    expect(nextIndex(fiveItems, 1.1)).toBe(2);\n    expect(nextIndex(fiveItems, 1.9)).toBe(2);\n  });\n});\n\ndescribe(\"prevIndex\", () => {\n  const fiveItems = [1, 2, 3, 4, 5];\n  test(\"works with middle index\", () => {\n    expect(prevIndex(fiveItems, 2)).toBe(1);\n  });\n  test(\"works with start index\", () => {\n    expect(prevIndex(fiveItems, 0)).toBe(0);\n  });\n  test(\"works with fractions\", () => {\n    expect(prevIndex(fiveItems, 1.1)).toBe(1);\n    expect(prevIndex(fiveItems, 1.9)).toBe(1);\n  });\n});\n"
  },
  {
    "path": "vscode-ext/.gitignore",
    "content": "node_modules\nsite"
  },
  {
    "path": "vscode-ext/.vscode/launch.json",
    "content": "// A launch configuration that launches the extension inside a new window\n// Use IntelliSense to learn about possible attributes.\n// Hover to view descriptions of existing attributes.\n// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387\n{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"name\": \"Extension\",\n      \"type\": \"extensionHost\",\n      \"request\": \"launch\",\n      \"runtimeExecutable\": \"${execPath}\",\n      \"args\": [\"--extensionDevelopmentPath=${workspaceFolder}\"]\n    },\n    {\n      \"name\": \"Extension Tests\",\n      \"type\": \"extensionHost\",\n      \"request\": \"launch\",\n      \"runtimeExecutable\": \"${execPath}\",\n      \"args\": [\n        \"--extensionDevelopmentPath=${workspaceFolder}\",\n        \"--extensionTestsPath=${workspaceFolder}/test\"\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "vscode-ext/extension.js",
    "content": "const vscode = require(\"vscode\");\nconst path = require(\"path\");\nconst fs = require(\"fs\");\nconst getCommits = require(\"./git\");\n\n// this method is called when your extension is activated\n// your extension is activated the very first time the command is executed\n\n/**\n * @param {vscode.ExtensionContext} context\n */\nfunction activate(context) {\n  // The command has been defined in the package.json file\n  // Now provide the implementation of the command with  registerCommand\n  // The commandId parameter must match the command field in package.json\n  let disposable = vscode.commands.registerCommand(\n    \"extension.git-file-history\",\n    function() {\n      // The code you place here will be executed every time your command is executed\n      try {\n        const currentPath = getCurrentPath();\n        if (!currentPath) {\n          vscode.window.showInformationMessage(\"No active file\");\n          return;\n        }\n\n        const panel = vscode.window.createWebviewPanel(\n          \"gfh\",\n          `${path.basename(currentPath)} (Git History)`,\n          vscode.ViewColumn.One,\n          {\n            enableScripts: true,\n            retainContextWhenHidden: true,\n            localResourceRoots: [\n              vscode.Uri.file(path.join(context.extensionPath, \"site\"))\n            ]\n          }\n        );\n        const indexPath = path.join(\n          context.extensionPath,\n          \"site\",\n          \"index.html\"\n        );\n\n        const index = fs.readFileSync(indexPath, \"utf-8\");\n        const newIndex = index\n          .replace(\n            \"<body>\",\n            `<body><script>/*<!--*/window.vscode=acquireVsCodeApi();window._PATH=${JSON.stringify(\n              currentPath\n            )}/*-->*/</script>`\n          )\n          .replace(\n            \"<head>\",\n            `<head><base href=\"${vscode.Uri.file(\n              path.join(context.extensionPath, \"site\")\n            ).with({\n              scheme: \"vscode-resource\"\n            })}/\"/>`\n          );\n\n        panel.webview.html = newIndex;\n\n        panel.webview.onDidReceiveMessage(\n          message => {\n            switch (message.command) {\n              case \"commits\":\n                const { path, last = 15, before = null } = message.params;\n                getCommits(path, last, before)\n                  .then(commits => {\n                    panel.webview.postMessage(commits);\n                  })\n                  .catch(console.error);\n            }\n          },\n          undefined,\n          context.subscriptions\n        );\n      } catch (e) {\n        console.error(e);\n        throw e;\n      }\n    }\n  );\n\n  context.subscriptions.push(disposable);\n}\n\nfunction getCurrentPath() {\n  return (\n    vscode.window.activeTextEditor &&\n    vscode.window.activeTextEditor.document &&\n    vscode.window.activeTextEditor.document.fileName\n  );\n}\n\nexports.activate = activate;\n\n// this method is called when your extension is deactivated\nfunction deactivate() {}\n\nmodule.exports = {\n  activate,\n  deactivate\n};\n"
  },
  {
    "path": "vscode-ext/git.js",
    "content": "const execa = require(\"execa\");\nconst pather = require(\"path\");\n\nasync function getCommits(path, last, before) {\n  const format = `{\"hash\":\"%h\",\"author\":{\"login\":\"%aN\"},\"date\":\"%ad\"},`;\n  const { stdout } = await execa(\n    \"git\",\n    [\n      \"log\",\n      `--max-count=${before ? last + 1 : last}`,\n      `--pretty=format:${format}`,\n      \"--date=iso\",\n      `${before || \"HEAD\"}`,\n      \"--\",\n      pather.basename(path)\n    ],\n    { cwd: pather.dirname(path) }\n  );\n  const json = `[${stdout.slice(0, -1)}]`;\n\n  const messagesOutput = await execa(\n    \"git\",\n    [\n      \"log\",\n      `--max-count=${last}`,\n      `--pretty=format:%s`,\n      `${before || \"HEAD\"}`,\n      \"--\",\n      pather.basename(path)\n    ],\n    { cwd: pather.dirname(path) }\n  );\n\n  const messages = messagesOutput.stdout.replace('\"', '\\\\\"').split(/\\r?\\n/);\n\n  const result = JSON.parse(json).map((commit, i) => ({\n    ...commit,\n    date: new Date(commit.date),\n    message: messages[i]\n  }));\n\n  return before ? result.slice(1) : result;\n}\n\nasync function getContent(commit, path) {\n  const { stdout } = await execa(\n    \"git\",\n    [\"show\", `${commit.hash}:./${pather.basename(path)}`],\n    { cwd: pather.dirname(path) }\n  );\n  return stdout;\n}\n\nmodule.exports = async function(path, last, before) {\n  const commits = await getCommits(path, last, before);\n  await Promise.all(\n    commits.map(async commit => {\n      commit.content = await getContent(commit, path);\n    })\n  );\n  return commits;\n};\n"
  },
  {
    "path": "vscode-ext/jsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"module\": \"commonjs\",\n    \"target\": \"es6\",\n    \"checkJs\": false /* Typecheck .js files. */,\n    \"lib\": [\"es6\"]\n  },\n  \"exclude\": [\"node_modules\"]\n}\n"
  },
  {
    "path": "vscode-ext/package.json",
    "content": "{\n  \"name\": \"git-file-history\",\n  \"displayName\": \"Git File History\",\n  \"description\": \"Modern, fast and intuitive tool for browsing the history and files in any git repository\",\n  \"version\": \"1.0.1\",\n  \"repository\": \"pomber/git-history\",\n  \"publisher\": \"pomber\",\n  \"license\": \"MIT\",\n  \"keywords\": [\n    \"git\",\n    \"history\",\n    \"log\",\n    \"file\",\n    \"commit\",\n    \"show\"\n  ],\n  \"engines\": {\n    \"vscode\": \"^1.30.2\"\n  },\n  \"categories\": [\n    \"Other\"\n  ],\n  \"icon\": \"images/icon.png\",\n  \"galleryBanner\": {\n    \"color\": \"#011627\",\n    \"theme\": \"dark\"\n  },\n  \"activationEvents\": [\n    \"onCommand:extension.git-file-history\"\n  ],\n  \"main\": \"./extension.js\",\n  \"contributes\": {\n    \"commands\": [\n      {\n        \"command\": \"extension.git-file-history\",\n        \"title\": \"Git File History\"\n      }\n    ]\n  },\n  \"scripts\": {\n    \"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/\",\n    \"build\": \" yarn build-site\",\n    \"postinstall\": \"node ./node_modules/vscode/bin/install\",\n    \"test\": \"node ./node_modules/vscode/bin/test\",\n    \"vscode:prepublish\": \"yarn build\"\n  },\n  \"devDependencies\": {\n    \"@types/mocha\": \"^2.2.42\",\n    \"@types/node\": \"^10.12.21\",\n    \"cross-env\": \"^5.2.0\",\n    \"eslint\": \"^5.13.0\",\n    \"typescript\": \"^3.3.1\",\n    \"vscode\": \"^1.1.28\"\n  },\n  \"dependencies\": {\n    \"execa\": \"^1.0.0\"\n  }\n}\n"
  },
  {
    "path": "vscode-ext/readme.md",
    "content": "# Git File History\n\nQuickly browse the history of a file from any git repository\n\n![feature X](https://user-images.githubusercontent.com/1911623/52807635-d5043480-306a-11e9-9b03-351b7cda4936.gif)\n\n### Sponsors\n\nSupport 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)]\n\n<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>\n\n<a href=\"https://opencollective.com/git-history/sponsor/0/website\" target=\"_blank\"><img src=\"https://opencollective.com/git-history/sponsor/0/avatar.svg\"></a>\n\n### Backers\n\nThank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/git-history#backer)]\n\n<a href=\"https://opencollective.com/git-history#backers\" target=\"_blank\"><img src=\"https://opencollective.com/git-history/backers.svg?width=890\"></a>\n"
  },
  {
    "path": "vscode-ext/test/extension.test.js",
    "content": "/* global suite, test */\n\n//\n// Note: This example test is leveraging the Mocha test framework.\n// Please refer to their documentation on https://mochajs.org/ for help.\n//\n\n// The module 'assert' provides assertion methods from node\nconst assert = require(\"assert\");\n\n// You can import and use all API from the 'vscode' module\n// as well as import your extension to test it\n// const vscode = require('vscode');\n// const myExtension = require('../extension');\n\n// Defines a Mocha test suite to group tests of similar kind together\nsuite(\"Extension Tests\", function() {\n  // Defines a Mocha unit test\n  test(\"Something 1\", function() {\n    assert.equal(-1, [1, 2, 3].indexOf(5));\n    assert.equal(-1, [1, 2, 3].indexOf(0));\n  });\n});\n"
  },
  {
    "path": "vscode-ext/test/index.js",
    "content": "//\n// PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING\n//\n// This file is providing the test runner to use when running extension tests.\n// By default the test runner in use is Mocha based.\n//\n// You can provide your own test runner if you want to override it by exporting\n// a function run(testRoot: string, clb: (error:Error) => void) that the extension\n// host can call to run the tests. The test runner is expected to use console.log\n// to report the results back to the caller. When the tests are finished, return\n// a possible error to the callback or null if none.\n\nconst testRunner = require(\"vscode/lib/testrunner\");\n\n// You can directly control Mocha options by configuring the test runner below\n// See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options\n// for more info\ntestRunner.configure({\n  ui: \"tdd\", // the TDD UI is being used in extension.test.js (suite, test, etc.)\n  useColors: true // colored output from test results\n});\n\nmodule.exports = testRunner;\n"
  },
  {
    "path": "vscode-ext/test-git.js",
    "content": "#!/usr/bin/env node\n\n// node test-git.js extension.js 2 94c91d9\n\nconst getCommits = require(\"./git\");\n\nconst [, , path, last, before] = process.argv;\n\ngetCommits(path, last, before).then(cs =>\n  console.log(\n    cs\n      .map(c => {\n        return `${c.hash} ${c.date.toDateString()} ${c.message}`;\n      })\n      .join(\"\\n\")\n  )\n);\n"
  },
  {
    "path": "vscode-ext/vsc-extension-quickstart.md",
    "content": "## What's in the folder\n\n- This folder contains all of the files necessary for your extension.\n- `package.json` - this is the manifest file in which you declare your extension and command.\n  - 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.\n- `extension.js` - this is the main file where you will provide the implementation of your command.\n  - 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`.\n  - We pass the function containing the implementation of the command as the second parameter to `registerCommand`.\n\n## Get up and running straight away\n\n- Press `F5` to open a new window with your extension loaded.\n- Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Git File History`.\n- Set breakpoints in your code inside `extension.js` to debug your extension.\n- Find output from your extension in the debug console.\n\n## Make changes\n\n- You can relaunch the extension from the debug toolbar after changing code in `extension.js`.\n- You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes.\n\n## Explore the API\n\n- You can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`.\n\n## Run tests\n\n- Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`.\n- Press `F5` to run the tests in a new window with your extension loaded.\n- See the output of the test result in the debug console.\n- Make changes to `test/extension.test.js` or create new test files inside the `test` folder.\n  - By convention, the test runner will only consider files matching the name pattern `**.test.js`.\n  - You can create folders inside the `test` folder to structure your tests any way you want.\n"
  }
]