[
  {
    "path": ".gitattributes",
    "content": "* text=auto eol=lf\n/bin/credentials-helper eol=lf\n/bin/ungit eol=lf\n"
  },
  {
    "path": ".github/workflows/bump.yml",
    "content": "name: Bump Dependencies\n\non:\n  push:\n    branches:\n      - master\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatch:\n\njobs:\n  bump:\n    if: github.event.repository.fork == false\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Use Node.js\n        uses: actions/setup-node@v6\n        with:\n          node-version: '18'\n\n      - run: npm ci\n      - run: |\n          body=\"$(npm run bumpdependencies)\"\n          body=\"${body#\"${body%%[![:space:]]*}\"}\"\n          body=\"${body%\"${body##*[![:space:]]}\"}\"\n          echo \"$body\"\n          echo \"body<<EOF\" >> $GITHUB_OUTPUT\n          echo \"$body\" >> $GITHUB_OUTPUT\n          echo \"EOF\" >> $GITHUB_OUTPUT\n        id: bumpdependencies\n      - run: npm install\n\n      - name: Create Pull Request\n        uses: peter-evans/create-pull-request@v8\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>\n          commit-message: |\n            Bump Dependencies\n\n            ${{ steps.bumpdependencies.outputs.body }}\n          title: Bump Dependencies\n          body: |\n            ```\n            ${{ steps.bumpdependencies.outputs.body }}\n            ```\n          labels: dependencies\n          branch: bumpdependencies\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non: [push, pull_request]\n\npermissions:\n  id-token: write   # Required for npm OIDC provenance publishing\n  contents: write\n\njobs:\n  test:\n    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name\n    strategy:\n      fail-fast: false\n      matrix:\n        node-version: ['20', '22', '*']\n        os: [ubuntu-latest, macos-latest, windows-latest]\n    runs-on: ${{ matrix.os }}\n#    env:\n      # electron-packager (win32 ia32 on macos) https://github.com/electron/electron-packager/pull/449#issuecomment-240508298\n#      WINEDLLOVERRIDES: 'mscoree,mshtml='\n\n    steps:\n      - uses: actions/checkout@v6\n\n      - name: Use Node.js ${{ matrix.node-version }}\n        uses: actions/setup-node@v6\n        with:\n          node-version: ${{ matrix.node-version }}\n          cache: 'npm'\n          registry-url: 'https://registry.npmjs.org'\n\n      # linux dependencies\n      # https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces\n      - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0\n        if: matrix.os == 'ubuntu-latest'\n      - run: sudo apt update && sudo apt install -y wine64 && sudo ln -sf /usr/bin/wine /usr/bin/wine64\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n      - run: wine64 --version\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n      - run: sudo add-apt-repository ppa:git-core/ppa -y && sudo apt-get update -q && sudo apt-get install -y git\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '*'\n      # macos dependencies\n# required for electron-packager\n#      - run: brew update && brew cask install xquartz wine-stable\n#        if: matrix.os == 'macos-latest' && matrix.node-version == '20'\n#      - run: wine64 --version\n#        if: matrix.os == 'macos-latest' && matrix.node-version == '20'\n      - run: brew reinstall git\n        if: matrix.os == 'macos-latest' && matrix.node-version == '*'\n      # windows dependencies\n      # https://github.community/t5/GitHub-Actions/TEMP-is-broken-on-Windows/td-p/30432\n      - run: echo \"TEMP=$env:USERPROFILE\\AppData\\Local\\Temp\" >> $env:GITHUB_ENV\n        if: matrix.os == 'windows-latest'\n      - run: choco upgrade git\n        if: matrix.os == 'windows-latest' && matrix.node-version == '*'\n\n      - run: git --version\n      - run: git config --global user.email \"test@testy.com\"\n      - run: git config --global user.name \"Test testy\"\n      - run: git config --global protocol.file.allow always # tests use file based submodules see #1539\n\n      - run: npm ci\n      - run: npm run lint\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n      - run: npm run build\n      - run: npm test\n\n      # publish artifacts\n      - run: npm pack\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n      - run: npm run electronpackage -- --all\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n      - run: npm run electronzip\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n\n      - name: Upload npm pack\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit\n          path: ungit-*.tgz\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-darwin-arm64\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-darwin-arm64\n          path: dist/ungit-darwin-arm64.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-darwin-x64\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-darwin-x64\n          path: dist/ungit-darwin-x64.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-linux-arm64\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-linux-arm64\n          path: dist/ungit-linux-arm64.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-linux-armv7l\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-linux-armv7l\n          path: dist/ungit-linux-armv7l.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-linux-x64\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-linux-x64\n          path: dist/ungit-linux-x64.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-win32-arm64\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-win32-arm64\n          path: dist/ungit-win32-arm64.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-win32-ia32\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-win32-ia32\n          path: dist/ungit-win32-ia32.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upload ungit-win32-x64\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'\n        uses: actions/upload-artifact@v7\n        with:\n          name: ungit-win32-x64\n          path: dist/ungit-win32-x64.zip\n          archive: false\n          retention-days: 7\n\n      - name: Upgrade npm for trusted publishing\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20' && github.repository == 'FredrikNoren/ungit' && github.ref == 'refs/heads/master'\n        run: npm install -g npm@latest\n\n      - name: npm publish\n        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20' && github.repository == 'FredrikNoren/ungit' && github.ref == 'refs/heads/master'\n        uses: actions/github-script@v8\n        with:\n          script: |\n            const script = require('./scripts/npmpublish.js')\n            await script({github, context, core, exec})\n"
  },
  {
    "path": ".gitignore",
    "content": "# Ignore generated folders\n.nyc_output/\nbuild/\ncoverage/\ndist/\nnode_modules/\npublic/css/\npublic/fonts/glyphicons-*\npublic/js/\n\n# Ignore compiled files\ncomponents/**/*.bundle.js\ncomponents/**/*.bundle.js.map\ncomponents/**/*.css\ncomponents/**/*.css.map\n\n# Ignore potential files\n.vscode/\n.ungitrc\nungit-*.tgz\n"
  },
  {
    "path": ".mochaclicktest.json",
    "content": "{\n  \"spec\": \"clicktests/spec.*.js\",\n  \"file\": \"./source/utils/logger.js\",\n  \"timeout\": 20000,\n  \"bail\": true,\n  \"exit\": true\n}"
  },
  {
    "path": ".mochatest.json",
    "content": "{\n  \"spec\": \"test/spec.*.js\",\n  \"file\": \"./source/utils/logger.js\",\n  \"timeout\": 12000,\n  \"exit\": true\n}"
  },
  {
    "path": ".npmignore",
    "content": "# Ignore whole folders\n.github/\n.nyc_output/\nassets/\nbuild/\nclicktests/\ncoverage/\ndist/\nscripts/\ntest/\n\n# Ignore non-compiled sources\ncomponents/**/*.less\ncomponents/**/*.js\n!components/**/*.bundle.js\npublic/images/icon.icns\npublic/images/icon.ico\npublic/js/raven.min.js.map\npublic/less/\npublic/source/\npublic/vendor/\n\n# Ignore dot-files\n.gitattributes\n.mochaclicktest.json\n.mochatest.json\n.prettierignore\n.prettierrc\nappveyor.yml\neslint.config.mjs\ntsconfig.json\n\n# Ignore docs\nCONTRIBUTING.md\ngpg_save_screenshot.png\nMERGETOOL.md\nPLUGINS.md\nscreenshot.png\nxkcd.png\n\n# Ignore potential files\n.vscode/\n.ungitrc\nungit-*.tgz\n"
  },
  {
    "path": ".prettierignore",
    "content": "# Generated folders\n.nyc_output/\nbuild/\ncoverage/\n\n# Third-party files\npublic/css/\npublic/js\npublic/vendor/\n\n# Browserify bundles\n**/*.bundle.js\n# All css files are autogenerated\n**/*.css\n# We don't like the prettier Markdown formatting\n**/*.md\n\n# Don't interfere with npm\npackage-lock.json\npackage.json\n"
  },
  {
    "path": ".prettierrc",
    "content": "{\n  \"endOfLine\": \"lf\",\n  \"trailingComma\": \"es5\",\n  \"jsdocUseInlineCommentForASingleTagBlock\": true,\n  \"plugins\": [\"@homer0/prettier-plugin-jsdoc\"],\n  \"printWidth\": 100,\n  \"singleQuote\": true\n}\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Change Log\nAll notable changes to this project will be documented in this file.\nThis project adheres to [Semantic Versioning](https://semver.org/).\nWe are following the [Keep a Changelog](https://keepachangelog.com/) format.\n\n## [Unreleased](https://github.com/FredrikNoren/ungit/compare/v1.5.29...master)\n\n## [1.5.29](https://github.com/FredrikNoren/ungit/compare/v1.5.28...v1.5.29)\n\n### Changed\n- Update README.md [#1615](https://github.com/FredrikNoren/ungit/pull/1615)\n- Bump Dependencies [#1614](https://github.com/FredrikNoren/ungit/pull/1614), [#1645](https://github.com/FredrikNoren/ungit/pull/1645), [#1643](https://github.com/FredrikNoren/ungit/pull/1643), [#1641](https://github.com/FredrikNoren/ungit/pull/1641), [#1639](https://github.com/FredrikNoren/ungit/pull/1639), [#1636](https://github.com/FredrikNoren/ungit/pull/1636), [#1634](https://github.com/FredrikNoren/ungit/pull/1634), [#1632](https://github.com/FredrikNoren/ungit/pull/1632), [#1630](https://github.com/FredrikNoren/ungit/pull/1630), [#1629](https://github.com/FredrikNoren/ungit/pull/1629), [#1626](https://github.com/FredrikNoren/ungit/pull/1626), [#1624](https://github.com/FredrikNoren/ungit/pull/1624), [#1620](https://github.com/FredrikNoren/ungit/pull/1620), [#1619](https://github.com/FredrikNoren/ungit/pull/1619), [#1616](https://github.com/FredrikNoren/ungit/pull/1616), [#1592](https://github.com/FredrikNoren/ungit/pull/1592)\n- Migrate to eslint flat config [#1607](https://github.com/FredrikNoren/ungit/pull/1607)\n- Bump formidable from 3.5.2 to 3.5.4 [#1617](https://github.com/FredrikNoren/ungit/pull/1617)\n- show remote fetch / push url in title [#1618](https://github.com/FredrikNoren/ungit/pull/1618)\n- Bump qs from 6.14.1 to 6.14.2 [#1644](https://github.com/FredrikNoren/ungit/pull/1644)\n- Bump actions/upload-artifact to 7 [#1649](https://github.com/FredrikNoren/ungit/pull/1649)\n\n## [1.5.28](https://github.com/FredrikNoren/ungit/compare/v1.5.27...v1.5.28)\n\n### Fixes\n- Enable git fetch again [#1604](https://github.com/FredrikNoren/ungit/pull/1604)\n\n### Changed\n- Bump Dependencies https://github.com/FredrikNoren/ungit/commit/dd9b7a35c92fafc3883a2ad7d04814049e04406a, https://github.com/FredrikNoren/ungit/commit/7e4e830cb71bdcbdd92d903419e1ad5e23a5ddc2, https://github.com/FredrikNoren/ungit/commit/944835dc8627c767c7145dad94b9fb4a2028e685, https://github.com/FredrikNoren/ungit/commit/ea063f47acf1f733b2a0af80d9c94293c3d880b7\n- CI Fixes https://github.com/FredrikNoren/ungit/commit/c02db509c2c334e7541e8f3dea4f615c7a614ccb, https://github.com/FredrikNoren/ungit/commit/a32668649836d7fd382574615333807ef3eed370, https://github.com/FredrikNoren/ungit/commit/7349b941a58f95117801fa36029fd44769dee8f8, \n- Replace node 21 with 22 https://github.com/FredrikNoren/ungit/commit/571d03f5fd9f4dca073152b123f117b5fb9bde8b\n\n## [1.5.27](https://github.com/FredrikNoren/ungit/compare/v1.5.26...v1.5.27)\n\n### Added\n- Show parent commit ids and try to select them on click [#1581](https://github.com/FredrikNoren/ungit/issues/1581)\n\n### Changed\n- Use monospace font-family in commit body [#1598](https://github.com/FredrikNoren/ungit/pull/1598)\n- Change watcher to properly filter ignored directories [#1597](https://github.com/FredrikNoren/ungit/pull/1597)\n\n## [1.5.26](https://github.com/FredrikNoren/ungit/compare/v1.5.25...v1.5.26)\n\n### Changed\n- Bump Dependencies [#1584](https://github.com/FredrikNoren/ungit/pull/1584), [#1585](https://github.com/FredrikNoren/ungit/pull/1585), [#1587](https://github.com/FredrikNoren/ungit/pull/1587), [#1586](https://github.com/FredrikNoren/ungit/pull/1586)\n\n## [1.5.25](https://github.com/FredrikNoren/ungit/compare/v1.5.24...v1.5.25)\n\n### Changed\n- Bump Dependencies [#1566](https://github.com/FredrikNoren/ungit/pull/1566), [#1571](https://github.com/FredrikNoren/ungit/pull/1571), [#1574](https://github.com/FredrikNoren/ungit/pull/1574), [#1578](https://github.com/FredrikNoren/ungit/pull/1578)\n\n## [1.5.24](https://github.com/FredrikNoren/ungit/compare/v1.5.23...v1.5.24)\n\n### Fixes\n- Fallback for font if bundled font dont support glyphs [#1564](https://github.com/FredrikNoren/ungit/pull/1564)\n\n### Changed\n- Bump Dependencies [#1555](https://github.com/FredrikNoren/ungit/pull/1555), [#1559](https://github.com/FredrikNoren/ungit/pull/1559), [#1560](https://github.com/FredrikNoren/ungit/pull/1560), [#1561](https://github.com/FredrikNoren/ungit/pull/1561), [#1563](https://github.com/FredrikNoren/ungit/pull/1563)\n\n## [1.5.23](https://github.com/FredrikNoren/ungit/compare/v1.5.22...v1.5.23)\n\n### Fixes\n- Ungit returns 0 when wrong arguments are used [#1548](https://github.com/FredrikNoren/ungit/issues/1548)\n- Server process keeps running when parent gets killed [#1552](https://github.com/FredrikNoren/ungit/issues/1552)\n\n### Changed\n- Bump Dependencies [#1542](https://github.com/FredrikNoren/ungit/pull/1542), [#1545](https://github.com/FredrikNoren/ungit/pull/1545), [#1551](https://github.com/FredrikNoren/ungit/pull/1551)\n\n## [1.5.22](https://github.com/FredrikNoren/ungit/compare/v1.5.21...v1.5.22)\n\n### Fixes\n- build: fix memory use by building serially [#1529](https://github.com/FredrikNoren/ungit/pull/1529)\n- Small fixes and cleanups [#1530](https://github.com/FredrikNoren/ungit/pull/1530)\n\n### Changed\n- Update README.md [#1526](https://github.com/FredrikNoren/ungit/pull/1526)\n- Update git version dependency to 2.34.x [#1536](https://github.com/FredrikNoren/ungit/pull/1536)\n- Use fork to spawn a new node process [#1537](https://github.com/FredrikNoren/ungit/pull/1537)\n- CI: git allow file protocol which is used in submodule tests [#1540](https://github.com/FredrikNoren/ungit/pull/1540)\n- Bump Dependencies [#1525](https://github.com/FredrikNoren/ungit/pull/1525), [#1531](https://github.com/FredrikNoren/ungit/pull/1531), [#1533](https://github.com/FredrikNoren/ungit/pull/1533)\n\n## [1.5.21](https://github.com/FredrikNoren/ungit/compare/v1.5.20...v1.5.21)\n\n### Fixes\n- fix patch checkbox html [#1517](https://github.com/FredrikNoren/ungit/pull/1517)\n\n### Changed\n- Bump Dependencies [#1512](https://github.com/FredrikNoren/ungit/pull/1512), [#1518](https://github.com/FredrikNoren/ungit/pull/1518)\n\n### Removed\n- Remove node 12 from build matrix [#1516](https://github.com/FredrikNoren/ungit/pull/1516)\n\n## [1.5.20](https://github.com/FredrikNoren/ungit/compare/v1.5.19...v1.5.20)\n\n### Fixes\n- Fix potential remote code exec [#1510](https://github.com/FredrikNoren/ungit/pull/1510)\n- Fix intermittent test failures [#1495](https://github.com/FredrikNoren/ungit/issues/1495)\n- lint: small bugs + jsdoc [#1504](https://github.com/FredrikNoren/ungit/pull/1504)\n\n### Changed\n- Bump Dependencies [#1503](https://github.com/FredrikNoren/ungit/pull/1503)\n\n## [1.5.19](https://github.com/FredrikNoren/ungit/compare/v1.5.18...v1.5.19)\n\n### Added\n- Add Tab Size Configuration [#1499](https://github.com/FredrikNoren/ungit/pull/1499)\n- Types: add support for VSCode IntelliSense [#1466](https://github.com/FredrikNoren/ungit/pull/1466)\n\n### Changed\n- Directory view [#1491](https://github.com/FredrikNoren/ungit/pull/1491)\n- Node watch [#1465](https://github.com/FredrikNoren/ungit/pull/1465)\n- Bump cached-path-relative from 1.0.2 to 1.1.0 [#1501](https://github.com/FredrikNoren/ungit/pull/1501)\n- Bump Dependencies [#1483](https://github.com/FredrikNoren/ungit/pull/1483), [#1487](https://github.com/FredrikNoren/ungit/pull/1487), [#1492](https://github.com/FredrikNoren/ungit/pull/1492), [#1494](https://github.com/FredrikNoren/ungit/pull/1494)\n\n## [1.5.18](https://github.com/FredrikNoren/ungit/compare/v1.5.17...v1.5.18)\n\n### Fixes\n- simple git flow breaks ungit [#1460](https://github.com/FredrikNoren/ungit/issues/1460)\n\n### Changed\n- Bump Dependencies [#1479](https://github.com/FredrikNoren/ungit/pull/1479)\n\n## [1.5.17](https://github.com/FredrikNoren/ungit/compare/v1.5.16...v1.5.17)\n\n### Changed\n- node 16 [#1476](https://github.com/FredrikNoren/ungit/pull/1476)\n- Bump Dependencies [#1475](https://github.com/FredrikNoren/ungit/pull/1475)\n\n## [1.5.16](https://github.com/FredrikNoren/ungit/compare/v1.5.15...v1.5.16)\n\n### Added\n- Add clipboard button on commit [#1462](https://github.com/FredrikNoren/ungit/pull/1462)\n- Encode URI paths with slashes [#1378](https://github.com/FredrikNoren/ungit/pull/1378)\n\n### Changed\n- Bump Dependencies [#1456](https://github.com/FredrikNoren/ungit/pull/1456), [#1464](https://github.com/FredrikNoren/ungit/pull/1464)\n- Bump elliptic from 6.5.3 to 6.5.4 [#1468](https://github.com/FredrikNoren/ungit/pull/1468)\n- Bump y18n from 4.0.0 to 4.0.1 [#1471](https://github.com/FredrikNoren/ungit/pull/1471)\n- git 2.3x changes break unittests [#1472](https://github.com/FredrikNoren/ungit/issues/1472)\n\n## [1.5.15](https://github.com/FredrikNoren/ungit/compare/v1.5.14...v1.5.15)\n\n### Changed\n- Bump Dependencies [#1451](https://github.com/FredrikNoren/ungit/pull/1451)\n\n## [1.5.14](https://github.com/FredrikNoren/ungit/compare/v1.5.13...v1.5.14)\n\n### Changed\n- Update socket.io to version 3.0.0 [#1443](https://github.com/FredrikNoren/ungit/pull/1443)\n- Bump Dependencies [#1442](https://github.com/FredrikNoren/ungit/pull/1442), [#1444](https://github.com/FredrikNoren/ungit/pull/1444), [#1448](https://github.com/FredrikNoren/ungit/pull/1448), [#1449](https://github.com/FredrikNoren/ungit/pull/1449)\n\n## [1.5.13](https://github.com/FredrikNoren/ungit/compare/v1.5.12...v1.5.13)\n\n### Fixed\n- Unhandled rejection ERR_FEATURE_NOT_AVAILABLE_ON_PLATFORM (recursive watch) [#1389](https://github.com/FredrikNoren/ungit/issues/1389)\n\n### Changed\n- Bump Dependencies [#1438](https://github.com/FredrikNoren/ungit/pull/1438)\n\n## [1.5.12](https://github.com/FredrikNoren/ungit/compare/v1.5.11...v1.5.12)\n\n### Fixed\n- branches - can't re-enable disabled groups [#1434](https://github.com/FredrikNoren/ungit/issues/1434)\n- Support git 2.29 sha256 [#1436](https://github.com/FredrikNoren/ungit/pull/1436)\n\n### Changed\n- Bump Dependencies [#1427](https://github.com/FredrikNoren/ungit/pull/1427)\n\n## [1.5.11](https://github.com/FredrikNoren/ungit/compare/v1.5.10...v1.5.11)\n\n### Added\n- Doubleclick to checkout [#190](https://github.com/FredrikNoren/ungit/issues/190)\n\n### Changed\n- Use page.waitForTimeout API in tests [#1422](https://github.com/FredrikNoren/ungit/pull/1422)\n- Bump Dependencies [#1417](https://github.com/FredrikNoren/ungit/pull/1417), [#1423](https://github.com/FredrikNoren/ungit/pull/1423)\n\n## [1.5.10](https://github.com/FredrikNoren/ungit/compare/v1.5.9...v1.5.10)\n\n### Fixed\n- Add copyright to electron executable [#1411](https://github.com/FredrikNoren/ungit/issues/1411)\n\n### Changed\n- Generate and extract source maps [#1394](https://github.com/FredrikNoren/ungit/pull/1394)\n- Import Bootstrap from npm and upgrade to latest 3.x [#1395](https://github.com/FredrikNoren/ungit/pull/1395)\n- Bump Dependencies upgrading from electron 9.x to 10.x [#1392](https://github.com/FredrikNoren/ungit/pull/1392), [#1406](https://github.com/FredrikNoren/ungit/pull/1406)\n\n## [1.5.9](https://github.com/FredrikNoren/ungit/compare/v1.5.8...v1.5.9)\n\n### Fixed\n- Fix git ignore settings [#1393](https://github.com/FredrikNoren/ungit/pull/1393)\n\n## [1.5.8](https://github.com/FredrikNoren/ungit/compare/v1.5.7...v1.5.8)\n\n### Fixed\n- Clear git-promise timeout when git command was successful [#1357](https://github.com/FredrikNoren/ungit/pull/1357)\n- When autoFetch=false don't make remote repo calls automatically [#1381](https://github.com/FredrikNoren/ungit/pull/1381)\n- Prevent commit message <textarea> from resizing horizontally [#1390](https://github.com/FredrikNoren/ungit/pull/1390)\n- Diff out is not properly escaping [#1387](https://github.com/FredrikNoren/ungit/issues/1387)\n\n### Changed\n- Migrate clicktests from nightmare to puppeteer [#1336](https://github.com/FredrikNoren/ungit/pull/1336)\n- Prettify code with prettier [#1316](https://github.com/FredrikNoren/ungit/pull/1316)\n- Switch from JSHint to ESLint [#1360](https://github.com/FredrikNoren/ungit/pull/1360)\n- Bump Dependencies [#1355](https://github.com/FredrikNoren/ungit/pull/1355), [#1385](https://github.com/FredrikNoren/ungit/pull/1385)\n\n### Removed\n- Remove bluebird dependency [#1350](https://github.com/FredrikNoren/ungit/pull/1350)\n- Remove grunt [#895](https://github.com/FredrikNoren/ungit/issues/895)\n\n## [1.5.7](https://github.com/FredrikNoren/ungit/compare/v1.5.6...v1.5.7)\n\n### Fixed\n- Init tooltips from the app start [#1343](https://github.com/FredrikNoren/ungit/pull/1343)\n- Fixing some accessibility issues [#1318](https://github.com/FredrikNoren/ungit/pull/1318)\n- Flatten total-lines-changed object [#1330](https://github.com/FredrikNoren/ungit/pull/1330)\n- Set electron window icon explicitly so it works during debug and on linux [#1347](https://github.com/FredrikNoren/ungit/pull/1347)\n\n### Changed\n- Only display ref search button when there are hidden refs [#1311](https://github.com/FredrikNoren/ungit/pull/1311), [#1325](https://github.com/FredrikNoren/ungit/pull/1325)\n- Cleanup CSS styles [#1339](https://github.com/FredrikNoren/ungit/pull/1339), [#1328](https://github.com/FredrikNoren/ungit/pull/1328), [#1331](https://github.com/FredrikNoren/ungit/pull/1331), [#1332](https://github.com/FredrikNoren/ungit/pull/1332), [#1322](https://github.com/FredrikNoren/ungit/pull/1322)\n- Style autocompletes like dropdowns [#1327](https://github.com/FredrikNoren/ungit/pull/1327)\n- Optimizes ref-search autocomplete initialization [#1326](https://github.com/FredrikNoren/ungit/pull/1326)\n- Reduce jQuery UI imports and use Bootstrap tooltips [#1340](https://github.com/FredrikNoren/ungit/pull/1340)\n- Image cleanup [#1345](https://github.com/FredrikNoren/ungit/pull/1345)\n- Bump Dependencies [#1309](https://github.com/FredrikNoren/ungit/pull/1309)\n\n### Removed\n- Remove unused color dependency [#1341](https://github.com/FredrikNoren/ungit/pull/1341)\n- Remove image embed [#1346](https://github.com/FredrikNoren/ungit/pull/1346)\n- Remove unused tracker.js [#1344](https://github.com/FredrikNoren/ungit/pull/1344)\n\n## [1.5.6](https://github.com/FredrikNoren/ungit/compare/v1.5.5...v1.5.6)\n\n### Fixed\n- Continue rebase fails with git 2.26 [#1301](https://github.com/FredrikNoren/ungit/issues/1301)\n- Dependency updates [#1304](https://github.com/FredrikNoren/ungit/pull/1304), [#1300](https://github.com/FredrikNoren/ungit/pull/1300), [#1297](https://github.com/FredrikNoren/ungit/pull/1297), [#1295](https://github.com/FredrikNoren/ungit/pull/1295)\n- ignore nmclicktests and ci files in npm package [#1306](https://github.com/FredrikNoren/ungit/pull/1306)\n\n### Added\n- GitHub Action CI [#1298](https://github.com/FredrikNoren/ungit/pull/1298)\n- GitHub Action dependency bump [#1296](https://github.com/FredrikNoren/ungit/pull/1296)\n\n## [1.5.5](https://github.com/FredrikNoren/ungit/compare/v1.5.4...v1.5.5)\n\n### Fixed\n- Bump dependencies [#1283](https://github.com/FredrikNoren/ungit/pull/1283)\n- Running npm scripts on macOS [#1287](https://github.com/FredrikNoren/ungit/pull/1287)\n- Reduce CPU and Memory consumption in textdiff. Addresses part of [#1091](https://github.com/FredrikNoren/ungit/issues/1091)\n- Better focus handling when creating branches and tags [#1288](https://github.com/FredrikNoren/ungit/pull/1288)\n- Don't show error page when reloading the page [#1289](https://github.com/FredrikNoren/ungit/issues/1289)\n- Periodically update author date of commits again [#1286](https://github.com/FredrikNoren/ungit/pull/1286)\n\n## [1.5.4](https://github.com/FredrikNoren/ungit/compare/v1.5.3...v1.5.4)\n\n### Fixed\n- forcedLaunchPath of null fails to work [#1281](https://github.com/FredrikNoren/ungit/issues/1281)\n\n### Changed\n- Update diff2html to version 3 [#1273](https://github.com/FredrikNoren/ungit/pull/1273)\n\n### Removed\n- Remove dependency on npm [#1269](https://github.com/FredrikNoren/ungit/pull/1269)\n\n## [1.5.3](https://github.com/FredrikNoren/ungit/compare/v1.5.2...v1.5.3)\n\n### Fixed\n- Git log for merge / empty commits does not work correctly [#1270](https://github.com/FredrikNoren/ungit/issues/1270)\n\n## [1.5.2](https://github.com/FredrikNoren/ungit/compare/v1.5.1...v1.5.2)\n\n### Fixed\n- Diff does not work for first commit [#1124](https://github.com/FredrikNoren/ungit/issues/1124)\n- `--no-launchBrowser` is ignored when ungit already running [#1259](https://github.com/FredrikNoren/ungit/issues/1259)\n- Bare repositories don't work with git 2.25 [#1265](https://github.com/FredrikNoren/ungit/issues/1265)\n- ungit crashes if current directory is deleted [#1266](https://github.com/FredrikNoren/ungit/issues/1266)\n- Make clicktests more reliable [#1263](https://github.com/FredrikNoren/ungit/pull/1263)\n- Rename + changes only show rename [#1175](https://github.com/FredrikNoren/ungit/issues/1175)\n\n### Removed\n- Remove Node 8 from build matrix [#1256](https://github.com/FredrikNoren/ungit/pull/1256)\n\n## [1.5.1](https://github.com/FredrikNoren/ungit/compare/v1.5.0...v1.5.1)\n\n### Fixed\n- Fix copy and paste in electron on macOS [#1251](https://github.com/FredrikNoren/ungit/issues/1251)\n\n## [1.5.0](https://github.com/FredrikNoren/ungit/compare/v1.4.48...v1.5.0)\n\n### Added\n- Include file diff in merge commits [#1242](https://github.com/FredrikNoren/ungit/pull/1242)\n- Hide diff buttons on hover [#1225](https://github.com/FredrikNoren/ungit/pull/1225)\n- Publish electron build [#1241](https://github.com/FredrikNoren/ungit/pull/1241)\n\n### Fixed\n- Updated Octicons [#1224](https://github.com/FredrikNoren/ungit/pull/1224), [#1245](https://github.com/FredrikNoren/ungit/pull/1245), [#1246](https://github.com/FredrikNoren/ungit/pull/1246)\n- Fix stash tooltips [#1227](https://github.com/FredrikNoren/ungit/pull/1227)\n- Improve git-init experience [#1228](https://github.com/FredrikNoren/ungit/pull/1228)\n- Fix inconsistent diff options [#1229](https://github.com/FredrikNoren/ungit/issues/1229)\n- Fix clearing .gitignore [#1236](https://github.com/FredrikNoren/ungit/pull/1236)\n- Fix electron package [#1240](https://github.com/FredrikNoren/ungit/pull/1240), [#1248](https://github.com/FredrikNoren/ungit/pull/1248)\n- Minor fixes to remove warnings [#1235](https://github.com/FredrikNoren/ungit/pull/1235), [#1237](https://github.com/FredrikNoren/ungit/pull/1237), [#1238](https://github.com/FredrikNoren/ungit/pull/1238), [#1239](https://github.com/FredrikNoren/ungit/pull/1239)\n\n## [1.4.48](https://github.com/FredrikNoren/ungit/compare/v1.4.47...v1.4.48)\n\n### Fixed\n- fix the width value of the header logo [#1221](https://github.com/FredrikNoren/ungit/pull/1221)\n\n## [1.4.47](https://github.com/FredrikNoren/ungit/compare/v1.4.46...v1.4.47)\n\n### Fixed\n- make diff2html line numbers and +/- prefixes unselectable [#1214](https://github.com/FredrikNoren/ungit/issues/1214), [#1215](https://github.com/FredrikNoren/ungit/pull/1215)\n\n## [1.4.46](https://github.com/FredrikNoren/ungit/compare/v1.4.45...v1.4.46)\n\n### Fixed\n- force git out put to be in English within ungit [#1208](https://github.com/FredrikNoren/ungit/pull/1208)\n\n## [1.4.45](https://github.com/FredrikNoren/ungit/compare/v1.4.44...v1.4.45)\n\n### Fixed\n- Improve styling of .gitignore edit dialog [#1205](https://github.com/FredrikNoren/ungit/pull/1205)\n\n## [1.4.44](https://github.com/FredrikNoren/ungit/compare/v1.4.43...v1.4.44)\n\n### Added\n- add config to disable numstat in staged diff to better performance [#1193](https://github.com/FredrikNoren/ungit/issues/1193)\n\n## [1.4.43](https://github.com/FredrikNoren/ungit/compare/v1.4.42...v1.4.43)\n\n### Fixed\n- fix gitignore manual edit not being saved [#644](https://github.com/FredrikNoren/ungit/issues/644)\n- fix issue with detached git processes on some OS and timeout not being enforced.\n- simplify `maxSearchIteration` enforcement for git.log()\n- change `alwaysLoadActiveBranch` boolean config to `maxActiveBranchSearchIteration` numeric config\n- bumped node engine requirement to [10.14 Dubnium](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V10.md#10.14.2)\n\n## [1.4.42](https://github.com/FredrikNoren/ungit/compare/v1.4.41...v1.4.42)\n\n### Fixed\n- Add \"Ignore white space\" config [#1185](https://github.com/FredrikNoren/ungit/pull/1185)\n\n## [1.4.41](https://github.com/FredrikNoren/ungit/compare/v1.4.40...v1.4.41)\n\n### Removed\n- Remove Google Analytics [#1182](https://github.com/FredrikNoren/ungit/pull/1182)\n\n## [1.4.40](https://github.com/FredrikNoren/ungit/compare/v1.4.39...v1.4.40)\n\n### Removed\n- Remove Keen.io [#1180](https://github.com/FredrikNoren/ungit/pull/1180)\n\n## [1.4.39](https://github.com/FredrikNoren/ungit/compare/v1.4.38...v1.4.39)\n\n### Fixed\n- Add git bin path config. [#1151](https://github.com/FredrikNoren/ungit/issues/1151)\n\n## [1.4.38](https://github.com/FredrikNoren/ungit/compare/v1.4.37...v1.4.38)\n\n### Fixed\n- Fix: Highlight current branch in submodules\n\n## [1.4.37](https://github.com/FredrikNoren/ungit/compare/v1.4.36...v1.4.37)\n\n### Fixed\n- Sort modules by names\n\n## [1.4.36](https://github.com/FredrikNoren/ungit/compare/v1.4.35...v1.4.36)\n\n### Fixed\n- fix changing remotes in remotes dropdown [#1158](https://github.com/FredrikNoren/ungit/pull/1158)\n\n## [1.4.35](https://github.com/FredrikNoren/ungit/compare/v1.4.34...v1.4.35)\n\n### Fixed\n- allow disabling of nprogress bar [#1143](https://github.com/FredrikNoren/ungit/issues/1143)\n- set `ungitVersionCheckOverride` as boolean in config [#1102](https://github.com/FredrikNoren/ungit/issues/1102)\n\n## [1.4.34](https://github.com/FredrikNoren/ungit/compare/v1.4.33...v1.4.34)\n\n### Fixed\n- fix issues when remote tags doesn't show [#1139](https://github.com/FredrikNoren/ungit/issues/1139)\n\n## [1.4.33](https://github.com/FredrikNoren/ungit/compare/v1.4.32...v1.4.33)\n\n### Fixed\n- Bump getmac version [#1130](https://github.com/FredrikNoren/ungit/issues/1130)\n- Add config to disable animation [#1136](https://github.com/FredrikNoren/ungit/issues/1136)\n- dependency bumps\n- Remove node6. Add node8 and node9 explicitly.\n\n## [1.4.32](https://github.com/FredrikNoren/ungit/compare/v1.4.31...v1.4.32)\n\n### Fixed\n- Handle crashes with better logs\n- Wrap localStorage to support environments without access to it\n\n## [1.4.31](https://github.com/FredrikNoren/ungit/compare/v1.4.30...v1.4.31)\n\n### Fixed\n- Add error logging for npm publish\n\n## [1.4.30](https://github.com/FredrikNoren/ungit/compare/v1.4.29...v1.4.30)\n\n### Fixed\n- Add `ungitBindIp` config to allow default binding in some cases [#1112](https://github.com/FredrikNoren/ungit/issues/1112)\n\n## [1.4.29](https://github.com/FredrikNoren/ungit/compare/v1.4.28...v1.4.29)\n\n### Fixed\n- Add `--no-optional-locks` if git version is appropriate [#1105](https://github.com/FredrikNoren/ungit/issues/1105)\n- Ensure ungit server to bind to `127.0.0.1` [#988](https://github.com/FredrikNoren/ungit/issues/988)\n- Add node highlight on mouse hover on relationship path [#1093](https://github.com/FredrikNoren/ungit/issues/1093)\n\n## [1.4.28](https://github.com/FredrikNoren/ungit/compare/v1.4.27...v1.4.28)\n\n### Fixed\n- adding raven locally for offline access. [#1107](https://github.com/FredrikNoren/ungit/pull/1107)\n\n## [1.4.27](https://github.com/FredrikNoren/ungit/compare/v1.4.26...v1.4.27)\n\n### Fixed\n- logic change for the merge conflict resolution\n\n## [1.4.26](https://github.com/FredrikNoren/ungit/compare/v1.4.25...v1.4.26)\n\n### Added\n- add a way to preconfigure repo lists [#1106](https://github.com/FredrikNoren/ungit/issues/1106)\n\n## [1.4.25](https://github.com/FredrikNoren/ungit/compare/v1.4.24...v1.4.25)\n\n### Added\n- add git pgp signing docs and code [#740](https://github.com/FredrikNoren/ungit/issues/740)\n\n## [1.4.24](https://github.com/FredrikNoren/ungit/compare/v1.4.23...v1.4.24)\n\n### Fixed\n- change `/api/log` -> `/api/gitlog` as some ad blockers really hates This\n- Fix excessive error messaging when disconnected from internet\n- Fix Raven initialization error when disconnected from internet\n\n## [1.4.23](https://github.com/FredrikNoren/ungit/compare/v1.4.22...v1.4.23)\n\n### Fixed\n- add feature to do `--recurse-submodules` for git clone [#1080](https://www.gnupg.org/documentation/manpage.html\n- increase debounce 250->500 wait and 1000->2000 sec so UI can pick up server changes more accurately\n\n## [1.4.22](https://github.com/FredrikNoren/ungit/compare/v1.4.21...v1.4.22)\n\n### Fixed\n- Fix missing jQuery and jQuery UI references [#1086](https://github.com/FredrikNoren/ungit/issues/1086)\n\n## [1.4.21](https://github.com/FredrikNoren/ungit/compare/v1.4.20...v1.4.21)\n\n### Fixed\n- Treat remote fetch fail as an warning rather than error [#1081](https://github.com/FredrikNoren/ungit/issues/1081)\n\n## [1.4.20](https://github.com/FredrikNoren/ungit/compare/v1.4.19...v1.4.20)\n\n### Fixed\n- deleted checked in 3rd party codes and manage by npm.\n- remove dependencies on async lib\n\n## [1.4.19](https://github.com/FredrikNoren/ungit/compare/v1.4.18...v1.4.19)\n\n### Fixed\n- fix credential helper not fetching all the authentication data [#1078](https://github.com/FredrikNoren/ungit/pull/1078)\n\n## [1.4.18](https://github.com/FredrikNoren/ungit/compare/v1.4.17...v1.4.18)\n\n### Fixed\n- fix inaccurate git state issue when new branch name conflict and `autoCheckoutOnBranchCreate` is enabled.\n- Add content refresh on .gitignore file change\n- fix reference filtering\n\n## [1.4.17](https://github.com/FredrikNoren/ungit/compare/v1.4.16...v1.4.17)\n\n### Fixed\n- fix textarea with in dialog when editing .gitignore [#1068](https://github.com/FredrikNoren/ungit/pull/1068)\n\n## [1.4.16](https://github.com/FredrikNoren/ungit/compare/v1.4.15...v1.4.16)\n\n### Fixed\n- Move version number to below logo. [#1069](https://github.com/FredrikNoren/ungit/pull/1069)\n\n## [1.4.15](https://github.com/FredrikNoren/ungit/compare/v1.4.14...v1.4.15)\n\n### Fixed\n- fix not setting `pathToNavigateTo` properly when `launchBrowser` is false and `launchCommand` is set [#1065](https://github.com/FredrikNoren/ungit/issues/1065)\n\n## [1.4.14](https://github.com/FredrikNoren/ungit/compare/v1.4.13...v1.4.14)\n\n### Fixed\n- fix credential helper when ungit is used with rootpath [#1060](https://github.com/FredrikNoren/ungit/issues/1060)\n\n## [1.4.13](https://github.com/FredrikNoren/ungit/compare/v1.4.12...v1.4.13)\n\n### Fixed\n- Change raven web client source to CDN rather than local copy [#972](https://github.com/FredrikNoren/ungit/issues/972)\n- dependency bump\n\n## [1.4.12](https://github.com/FredrikNoren/ungit/compare/v1.4.11...v1.4.12)\n\n### Fixed\n- Adding internet disconnected state handling [#1014](https://github.com/FredrikNoren/ungit/issues/1014)\n- Allow editing .gitignore via ungit [#976](https://github.com/FredrikNoren/ungit/issues/1014)\n\n## [1.4.11](https://github.com/FredrikNoren/ungit/compare/v1.4.10...v1.4.11)\n\n### Added\n- add cancel button for empty commits and amends [#1029](https://github.com/FredrikNoren/ungit/issues/1029)\n\n### Fixed\n- differentiate remote vs local tag. [#1016](https://github.com/FredrikNoren/ungit/issues/1016)\n- fix push not throwing giterror\n- fix remote tag push not creating remote tag\n- change ref refresh logic\n- show error on incorrect credentials [#1042](https://github.com/FredrikNoren/ungit/pull/1042)\n- allow credential handling for remotes [#1039](https://github.com/FredrikNoren/ungit/issues/1039)\n- cleanup clicktest output [#1035](https://github.com/FredrikNoren/ungit/pull/1035)\n\n## [1.4.10](https://github.com/FredrikNoren/ungit/compare/v1.4.9...v1.4.10)\n\n### Added\n- add commit & push option [#1038](https://github.com/FredrikNoren/ungit/issues/1038)\n\n### Fixed\n- hide / disable push option if there is no remote [#1050](https://github.com/FredrikNoren/ungit/issues/1050)\n\n## [1.4.9](https://github.com/FredrikNoren/ungit/compare/v1.4.8...v1.4.9)\n\n### Fixed\n- handle failed promises [#1017](https://github.com/FredrikNoren/ungit/issues/1017)\n- empty commit [#1028](https://github.com/FredrikNoren/ungit/issues/1028)\n- fix commit detail layout while hovering over commit node [#1025](https://github.com/FredrikNoren/ungit/issues/1025)\n\n## [1.4.8](https://github.com/FredrikNoren/ungit/compare/v1.4.7...v1.4.8)\n\n### Fixed\n- fix remote branches display name and delete action [#1032](https://github.com/FredrikNoren/ungit/issues/1032), [#1031](https://github.com/FredrikNoren/ungit/issues/1031)\n\n## [1.4.7](https://github.com/FredrikNoren/ungit/compare/v1.4.6...v1.4.7)\n\n### Added\n- add remote branches to the branch list. [#966](https://github.com/FredrikNoren/ungit/issues/966)\n\n## [1.4.6](https://github.com/FredrikNoren/ungit/compare/v1.4.5...v1.4.6)\n\n### Fixed\n- dependency bump to fix dependency's security problem.\n- Add emphasis if remote branch delete for confirmation dialog. [#947](https://github.com/FredrikNoren/ungit/issues/947)\n\n## [1.4.5](https://github.com/FredrikNoren/ungit/compare/v1.4.4...v1.4.5)\n\n### Fixed\n- fix a bug where no diff wasn't properly showing [#969](https://github.com/FredrikNoren/ungit/issues/969)\n\n## [1.4.4](https://github.com/FredrikNoren/ungit/compare/v1.4.3...v1.4.4)\n\n### Fixed\n- fix a bug where fetch is disabled after page load\n- make `forceLaunchPath` to supersede `launchBrowser` [#1006](https://github.com/FredrikNoren/ungit/issues/1006)\n\n## [1.4.3](https://github.com/FredrikNoren/ungit/compare/v1.4.2...v1.4.3)\n\n### Fixed\n- changing to path navigation to `nprogress` bar. [#1001](https://github.com/FredrikNoren/ungit/issues/1001)\n\n## [1.4.2](https://github.com/FredrikNoren/ungit/compare/v1.4.1...v1.4.2)\n\n### Fixed\n- fix navigation redirection on git clone and adding xkcd image\n- dependency bump\n\n## [1.4.1](https://github.com/FredrikNoren/ungit/compare/v1.4.0...v1.4.1)\n\n### Fixed\n- fix the issue where browser opens before ungit start. [#994](https://github.com/FredrikNoren/ungit/issues/994)\n- including xkcd art back [#999](https://github.com/FredrikNoren/ungit/issues/999)\n\n## [1.4.0](https://github.com/FredrikNoren/ungit/compare/v1.3.3...v1.4.0)\n\n### Fixed\n- Revert to MIT [#947](https://github.com/FredrikNoren/ungit/issues/974)\n\n## [1.3.3](https://github.com/FredrikNoren/ungit/compare/v1.3.2...v1.3.3)\n\n### Fixed\n- fix `tagsToDisplay` clearing issue. [#973](https://github.com/FredrikNoren/ungit/issues/973)\n\n## [1.3.2](https://github.com/FredrikNoren/ungit/compare/v1.3.1...v1.3.2)\n\n### Added\n- Adding in ref search box and limit num of ref display [#973](https://github.com/FredrikNoren/ungit/issues/973)\n\n## [1.3.1](https://github.com/FredrikNoren/ungit/compare/v1.3.0...v1.3.1)\n\n### Added\n- Add link to plans & license in header [#947](https://github.com/FredrikNoren/ungit/issues/974)\n\n## [1.3.0](https://github.com/FredrikNoren/ungit/compare/v1.2.3...v1.3.0)\n\n### Fixed\n- Switch to Faircode paywall instead of license popup [#947](https://github.com/FredrikNoren/ungit/issues/974)\n\n## [1.2.3](https://github.com/FredrikNoren/ungit/compare/v1.2.2...v1.2.3)\n\n### Fixed\n- Bump license text to v0.2.1 (fixes typo). [Faircode License changelog](https://github.com/faircodeio/faircode-license/blob/master/CHANGELOG.md)\n\n## [1.2.2](https://github.com/FredrikNoren/ungit/compare/v1.2.1...v1.2.2)\n\n### Fixed\n-  Bump license text to v0.2 to fix two small inconsistencies: Clarify currency (USD) and remove \"no additional rights\" clause as it's problematic and superfluous. License changelog at https://github.com/faircodeio/faircode-license/blob/master/CHANGELOG.md [#947](https://github.com/FredrikNoren/ungit/issues/974)\n\n## [1.2.1](https://github.com/FredrikNoren/ungit/compare/v1.2.0...v1.2.1)\n\n### Fixed\n- fix for not launching browser when executed at the git repo [#986](https://github.com/FredrikNoren/ungit/issues/986)\n\n## [1.2.0](https://github.com/FredrikNoren/ungit/compare/v1.1.33...v1.2.0)\n\n### Fixed\n- Show license notification on first start (license changed in 1.1.32) [#947](https://github.com/FredrikNoren/ungit/issues/974)\n- fix potential memory leak with `express-session`[#977](https://github.com/FredrikNoren/ungit/issues/977)\n- Fix document title on windows [#983](https://github.com/FredrikNoren/ungit/pull/983)\n- parse local storage as json instead of regex [#981](https://github.com/FredrikNoren/ungit/pull/981)\n- resolve path keywords such as `~` at server side [#980](https://github.com/FredrikNoren/ungit/issues/975)\n\n## [1.1.33](https://github.com/FredrikNoren/ungit/compare/v1.1.32...v1.1.33)\n\n### Fixed\n- Make Logo and favicon HiDpi [#589](https://github.com/FredrikNoren/ungit/issues/589)\n- Remove forever-monitor [#961](https://github.com/FredrikNoren/ungit/issues/961)\n\n## [1.1.32](https://github.com/FredrikNoren/ungit/compare/v1.1.31...v1.1.32)\n\n### Fixed\n- Update license [#974](https://github.com/FredrikNoren/ungit/issues/974)\n\n## [1.1.31](https://github.com/FredrikNoren/ungit/compare/v1.1.30...v1.1.31)\n\n### Fixed\n- Bump dependencies\n\n## [1.1.30](https://github.com/FredrikNoren/ungit/compare/v1.1.29...v1.1.30)\n\n### Fixed\n- move unit tests to es6\n- Add squash feature [#129](https://github.com/FredrikNoren/ungit/issues/129)\n\n## [1.1.29](https://github.com/FredrikNoren/ungit/compare/v1.1.28...v1.1.29)\n\n### Fixed\n- move `Gruntfile.js` to es6\n\n## [1.1.28](https://github.com/FredrikNoren/ungit/compare/v1.1.27...v1.1.28)\n\n### Fixed\n- Refactoring to remove static data-ta tags from tests\n- `grunt nmclicktest` -> `grunt clicktest`\n- Stabilize ungit open test of clicktest via using a tag that is guaranteed to be generated\n- Add click test bailout on tes failure\n- Add parallel click test `grunt clickParallel`\n- Remove deps to fix config init bug for the `credentials-helper`. [#838](https://github.com/FredrikNoren/ungit/issues/838)\n\n## [1.1.27](https://github.com/FredrikNoren/ungit/compare/v1.1.26...v1.1.27)\n\n### Fixed\n- Add alert when moving back in time. [#914](https://github.com/FredrikNoren/ungit/issues/914)\n\n## [1.1.26](https://github.com/FredrikNoren/ungit/compare/v1.1.25...v1.1.26)\n\n### Fixed\n- fix invalid path input for autocomplete causing front end crash [#942](https://github.com/FredrikNoren/ungit/issues/942)\n- bump and checking in package-lock.json\n\n## [1.1.25](https://github.com/FredrikNoren/ungit/compare/v1.1.24...v1.1.25)\n\n### Fixed\n- Change stash pop operation to stash apply [#919](https://github.com/FredrikNoren/ungit/issues/919)\n\n## [1.1.24](https://github.com/FredrikNoren/ungit/compare/v1.1.23...v1.1.24)\n\n### Fixed\n- fix some commands not properly reporting git error [#933](https://github.com/FredrikNoren/ungit/issues/933)\n\n## [1.1.23](https://github.com/FredrikNoren/ungit/compare/v1.1.22...v1.1.23)\n\n### Fixed\n- finalize nightmare click test\n\n## [1.1.22](https://github.com/FredrikNoren/ungit/compare/v1.1.21...v1.1.22)\n\n### Fixed\n- Add a config setting to allow setting the default diff type. [#929](https://github.com/FredrikNoren/ungit/issues/929)\n\n## [1.1.21](https://github.com/FredrikNoren/ungit/compare/v1.1.20...v1.1.21)\n\n### Fixed\n- Initial refactoring of click test using nightmare and mocha\n- **Dropping support for node 4.x and 5.x!, 6.x and later is now supported.**\n\n## [1.1.20](https://github.com/FredrikNoren/ungit/compare/v1.1.19...v1.1.20)\n\n### Fixed\n- Hide credentials in remote urls at home repo list\n\n## [1.1.19](https://github.com/FredrikNoren/ungit/compare/v1.1.18...v1.1.19)\n\n### Fixed\n- Ask before deleting a stash\n\n## [1.1.18](https://github.com/FredrikNoren/ungit/compare/v1.1.17...v1.1.18)\n\n### Fixed\n- Fix checking out remote refs (again)\n\n## [1.1.17](https://github.com/FredrikNoren/ungit/compare/v1.1.16...v1.1.17)\n\n### Fixed\n- Fix checking out remote refs\n\n## [1.1.16](https://github.com/FredrikNoren/ungit/compare/v1.1.15...v1.1.16)\n\n### Fixed\n- clicktests logging correction and using wait for within tests.\n- Refactor filewatch and using normalized test path\n- throttle parallel test's parellelization limit\n- dependency bump\n- Fix context issue for `gitSetUserConfig` [#912](https://github.com/FredrikNoren/ungit/issues/912)\n\n## [1.1.15](https://github.com/FredrikNoren/ungit/compare/v1.1.14...v1.1.15)\n\n### Fixed\n- Updating crash page with instructions and adblock detection\n\n## [1.1.14](https://github.com/FredrikNoren/ungit/compare/v1.1.13...v1.1.14)\n\n### Fixed\n- Disable strict mode for startup params and config [#890](https://github.com/FredrikNoren/ungit/issues/890)\n\n## [1.1.13](https://github.com/FredrikNoren/ungit/compare/v1.1.12...v1.1.13)\n\n### Fixed\n- Fix startup args bug: [#896](https://github.com/FredrikNoren/ungit/issues/896)\n\n## [1.1.12](https://github.com/FredrikNoren/ungit/compare/v1.1.11...v1.1.12)\n\n### Fixed\n- Retain commit messages when commit fails [#882](https://github.com/FredrikNoren/ungit/pull/882)\n- Fix rare edge case where remote node is gone during reset op.\n- rescursively resolve all promises before caching them. [#878](https://github.com/FredrikNoren/ungit/pull/878)\n\n## [1.1.11](https://github.com/FredrikNoren/ungit/compare/v1.1.10...v1.1.11)\n\n### Fixed\n- Fix cli arguments [#871](https://github.com/FredrikNoren/ungit/pull/871)\n- Stop if ~/.ungitrc contains syntax error\n- Removed official support ini format of ~/.ungitrc, because internal API supports only JSON\n\n## [1.1.10](https://github.com/FredrikNoren/ungit/compare/v1.1.9...v1.1.10)\n\n### Fixed\n- Fix broken diff out in some cases when diff contains table. [#881](https://github.com/FredrikNoren/ungit/pull/881)\n\n## [1.1.9](https://github.com/FredrikNoren/ungit/compare/v1.1.8...v1.1.9)\n\n### Fixed\n- Fix around ubuntu's inability to cache promises. [#877](https://github.com/FredrikNoren/ungit/pull/878)\n\n## [1.1.8](https://github.com/FredrikNoren/ungit/compare/v1.1.7...v1.1.8)\n\n### Fixed\n- Realtime text diff via invalidate diff on directory change [#867](https://github.com/FredrikNoren/ungit/pull/867)\n- Promisify `./source/utils/cache.js` [#870](https://github.com/FredrikNoren/ungit/pull/870)\n- Fix load more text diff button. [#876](https://github.com/FredrikNoren/ungit/pull/876)\n\n## [1.1.7](https://github.com/FredrikNoren/ungit/compare/v1.1.6...v1.1.7)\n\n### Fixed\n- Fix diff flickering issue and optimization [#865](https://github.com/FredrikNoren/ungit/pull/865)\n- Fix credential dialog issue [#864](https://github.com/FredrikNoren/ungit/pull/864)\n- Fix HEAD branch order when redraw [#858](https://github.com/FredrikNoren/ungit/issues/858)\n\n## [1.1.6](https://github.com/FredrikNoren/ungit/compare/v1.1.5...v1.1.6)\n\n### Fixed\n- Fix path auto complete [#861](https://github.com/FredrikNoren/ungit/issues/861)\n\n## [1.1.5](https://github.com/FredrikNoren/ungit/compare/v1.1.4...v1.1.5)\n\n### Fixed\n- Update \"Toggle all\" button after commit or changing selected files [#859](https://github.com/FredrikNoren/ungit/issues/859)\n\n## [1.1.4](https://github.com/FredrikNoren/ungit/compare/v1.1.3...v1.1.4)\n\n### Fixed\n- [patch] Promise refactoring\n\n## [1.1.3](https://github.com/FredrikNoren/ungit/compare/v1.1.2...v1.1.3)\n\n### Fixed\n- [patch] Fix submodule navigation on windows [#577](https://github.com/FredrikNoren/ungit/issues/577)\n\n## [1.1.2](https://github.com/FredrikNoren/ungit/compare/v1.1.1...v1.1.2)\n\n### Fixed\n- Fix a bug that prevented the new version dialog from being dismissed\n\n## [1.1.1](https://github.com/FredrikNoren/ungit/compare/v1.1.0...v1.1.1)\n\n### Fixed\n- [patch] Fixed small spelling error for ignore whitespace feature [#853](https://github.com/FredrikNoren/ungit/pull/853)\n\n## [1.1.0](https://github.com/FredrikNoren/ungit/compare/v1.0.1...v1.1.0)\n\n### Added\n- Added option to ignore ungit version checks [#851](https://github.com/FredrikNoren/ungit/issues/851)\n\n## [1.0.1](https://github.com/FredrikNoren/ungit/compare/v1.0.0...v1.0.1)\n\n### Fixed\n- [patch] Fixed gravatar avatar fetch if email have different cases applied. [#847](https://github.com/FredrikNoren/ungit/issues/847)\n\n## [1.0.0](https://github.com/FredrikNoren/ungit/compare/v0.10.3...v1.0.0)\n\n### Added\n- Added search by git folder name in the search bar. [#793](https://github.com/FredrikNoren/ungit/issues/793)\n- New configuration option `logLevel` allows you to assign the level of logging you want to see in the servers output console.\n- New configuration option `mergeTool` allows you to assign a custom external merge tool for conflict resolution [#783](https://github.com/FredrikNoren/ungit/issues/783) [Doc](https://github.com/FredrikNoren/ungit/blob/master/MERGETOOL.md)\n- Whitespace ignore option for text diffs [#777](https://github.com/FredrikNoren/ungit/issues/777)\n- Fix for favorites linking in case rootPath is used @sebastianmay [#609](https://github.com/FredrikNoren/ungit/issues/609) and image diffing\n- Limit commit title to 72 characters, the rest is truncated and shown when inspecting the commit\n- Updated file watch logic to closely follow git commands in another process [#283](https://github.com/FredrikNoren/ungit/issues/283)\n- Introduced Continuous delivery. [#823](https://github.com/FredrikNoren/ungit/issues/823)\n\n### Fixed\n- File diff firing increasing number of events longer it survives.\n- Fix missing ungit logo. [#812](https://github.com/FredrikNoren/ungit/issues/812)\n- Fix when stash output is empty [#818](https://github.com/FredrikNoren/ungit/issues/818)\n- Fix minor display error for wide git repo [#830](https://github.com/FredrikNoren/ungit/pull/830)\n- Persist commit messages during merge operation [#779](https://github.com/FredrikNoren/ungit/issues/779)\n- Refresh `staging.files` object for cleaner refresh such as refresh pached line list, diff and etc.\n- Fixed an issue where patching on some key word file names such as \"test\".\n- Fix missing commit message body if commit was committed with Visual Studio or Visual Studio Code [#826](https://github.com/FredrikNoren/ungit/pull/826)\n- Fix initial page load when loaded node does not fits in screen. [#832](https://github.com/FredrikNoren/ungit/issues/832)\n\n## [0.10.3](https://github.com/FredrikNoren/ungit/compare/v0.10.2...v0.10.3)\n\n### Added\n- Show diffs for stashed changes [#444](https://github.com/FredrikNoren/ungit/issues/444)\n- Active node focused git log result [#420](https://github.com/FredrikNoren/ungit/issues/420)\n\n### Fixed\n- Missing npm as a normal dependency [#766](https://github.com/FredrikNoren/ungit/issues/766)\n\n## [0.10.2](https://github.com/FredrikNoren/ungit/compare/v0.10.1...v0.10.2)\n\n### Fixed\n- Handle SIGTERM and SIGINT [#763](https://github.com/FredrikNoren/ungit/issues/763)\n\n### Added\n- Added bare repo support [#177](https://github.com/FredrikNoren/ungit/issues/177) [#728](https://github.com/FredrikNoren/ungit/issues/728)\n- Added support for cherry-pick conflict[#701](https://github.com/FredrikNoren/ungit/issues/701)\n- Added wordwrap support for diffs [#721](https://github.com/FredrikNoren/ungit/issues/721)\n- Support for Node6 [#745](https://github.com/FredrikNoren/ungit/pull/745/files)\n- Added \"autoCheckoutOnBranchCreate\" option [#752](https://github.com/FredrikNoren/ungit/pull/752/files)\n\n### Fixed\n- Fix maxConcurrentGitOperations not limiting git processes [#707](https://github.com/FredrikNoren/ungit/issues/707)\n- Fix \".lock\" file conflicts in parallelized git operations [#515](https://github.com/FredrikNoren/ungit/issues/515)\n- Allow Ungit to function under sub dir of a git dir [#734](https://github.com/FredrikNoren/ungit/issues/734)\n- Removed deprecated npmconf package [#746](https://github.com/FredrikNoren/ungit/issues/746)\n- More helpful warning messages [#749](https://github.com/FredrikNoren/ungit/pull/749/files)\n- Deleting already deleted remote tag [#748](https://github.com/FredrikNoren/ungit/pull/748)\n- Fix to handle revert merge commit [#757](https://github.com/FredrikNoren/ungit/pull/757)\n\n### Changed\n- Cleaner rebase conflict message display [#708](https://github.com/FredrikNoren/ungit/pull/708)\n- ES6 [#672](https://github.com/FredrikNoren/ungit/pull/672)\n- Dropped support for Node 0.10 and 0.12 [#745](https://github.com/FredrikNoren/ungit/pull/745/files)\n\n## [0.10.1](https://github.com/FredrikNoren/ungit/compare/v0.10.0...v0.10.1)\n\n### Added\n- Introduced change log! [#687](https://github.com/FredrikNoren/ungit/issues/687)\n- Improved server and client error logging [#695](https://github.com/FredrikNoren/ungit/pull/695)\n\n### Fixed\n- Fix crashes due to submodule parsing [#690](https://github.com/FredrikNoren/ungit/issues/690) [#689](https://github.com/FredrikNoren/ungit/issues/689)\n- Fix duplicate remote tag issues [#685](https://github.com/FredrikNoren/ungit/issues/685)\n- Fix scrolling issue in safari [#686](https://github.com/FredrikNoren/ungit/issues/686)\n- Fix git hooks failing on non-ascii files [#676](https://github.com/FredrikNoren/ungit/issues/676)\n\n### Removed\n- Reverted on hover button effects [#688](https://github.com/FredrikNoren/ungit/issues/688)\n\n### Changed\n- Upgrade keen.io client code [#679](https://github.com/FredrikNoren/ungit/issues/679)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing Guidelines\n\nThese are the contributing guidelines as well as some documentation on how the code is structured. Read up before contributing to make everything as smooth as possible.\n\n## Posting issues\n\nJust common sense; do a quick search before posting, someone might already have created an issue (or resolved the problem!). If you're posting a bug; try to include as much relevant information as possible (ungit version, node and npm version, OS, any Git errors displayed, the output from CLI console and output from the browser console).\n\n## Pull requests\n\nAll PRs are automatically published to NPM once merged (see [#823](https://github.com/FredrikNoren/ungit/issues/823)).\nThere are two things you have to do for all PRs:\n\n- Make sure to include a note in CHANGELOG.md about the change as part of the PR.\n- If it's a code change: Bump the version in `package.json` and `package-lock.json`.\n  - Does the change fundamentally change how people use Ungit: Bump the major version.\n  - Does the change introduce new features: Bump the minor version.\n  - Otherwise (bug fixes, tweaks, and refactoring): Bump patch version.\n  - If the change doesn't affect the product (e.g. you change the README): No need to bump the version.\n\n## Writing plugins\n\nSee [PLUGINS.md](PLUGINS.md)\n\n## Developing for Ungit proper\n\nI do accept pull requests, but I also reserve the right to not do so for things I don't think fit with Ungit. If you are developing anything new you should almost always also provide tests for it, preferably click tests but it doesn't hurt to write REST-interface tests as well if applicable. Try to post pull requests early, even at a concept stage, to get feedback and increase chances it's merged.\n\n### What you need to get started\n\nYou'll need the same as for running Ungit; node, npm, and git.\n\n### Getting started\n\nTo get started developing on Ungit:\n\n 1. Make sure you have [node.js](https://nodejs.org/), [npm](https://www.npmjs.com/) and [git](https://git-scm.com/) installed.\n 2. Clone the repository to a local directory.\n 3. Run `npm install` to install dependencies.\n 4. Run `npm run build` to build (compile templates, CSS and JS).\n 5. Type `npm start` to start ungit, or `npm test` to run tests.\n 6. (Optional). Run `npm run watch` to automatically rebuild stuff when you change files.\n 7. Run `npm run lint` to verify your changes conform to the formatting.\n 8. (Optional). Run `npm run format` to automatically fix formatting and (some) linting issues.\n\n### Run ungit as a standalone application\n\nTo provide easier access to launch ungit a standalone application container using [electron](https://electronjs.org/) is available.\n\n#### To get started\n\n 1. Follow steps in 'Getting started' to get a development environment ready.\n 2. Run `npm run electronpackage`. This will create a standalone application package under `build/`\n\n#### Known limitations\n\n 1. The current standalone application does not allow you to execute more than one instance.\n 2. There is no installer package neither automatic update mechanism for standalone application in place yet.\n\n#### Additional notes\n\n 1. To create windows package with proper application description on a non-windows platform, [wine](https://www.winehq.org/) is required to be installed. If not, the windows package will be created with default resources.\n\n### Architecture overview\n\nUngit has two major parts; the server and the UI. The server exposes a REST interfaces, which enables it to be run on a remote server. The UI is a single-page web-app, built using Knockout.js.\n\n### Folders\n\n- `assets/` Raw assets used for development.\n- `bin/` \"Binary\" files, the ungit launcher script and the credentials-helper, which is invoked by git to acquire credentials when using http authentication.\n- `clicktests/` [puppeteer](https://pptr.dev/) click test; basically tests that run on the rendered DOM. Since these run all the way, from the DOM down to the server, they're also the most powerful of the tests.\n- `components/` This directory contains all view components for Ungit, each of them exposed as an Ungit plugin.\n- `public/` The UI web-app.\n- `public/css/` CSS generated by the npm build script.\n- `public/fonts/` and `public/images/` Assets which are served directly.\n- `public/js/` An ungit.js file generated by the npm build script, as well as raven files which handle exception logging.\n- `public/less/` Less files, which are the \"source\" used to generate the CSS.\n- `public/source/` JavaScript source code, which is turned into the `public/js/ungit.js` file by the npm build script.\n- `public/vendor/` Various 3rd-party libs.\n- `source/` Server and shared (i.e. used by both server and UI) source code.\n- `test/` Unit tests and REST interface tests.\n\n### Running tests\n\n`npm test` will run both unit tests, REST-interface tests, and click tests. `npm run unittest` only runs the tests in the `test/` folder, `npm run clicktest` runs only the tests in the `clicktests/` folder. Install Mocha (`npm install -g mocha`) to run specific tests in the test folder and get better stack traces: `mocha test/spec.git-api.js`.\n\n### Things to consider when developing\n\n- Try to make everything as touch friendly as possible, for instance, no mouse over tooltips (try to make it clear without that). Everything doesn't adhere 100% to that right now but I'm trying to move it more in that direction.\n- Write tests. The most important tests to write are usually the click tests since they will cover the most code (both UI and backend).\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2013-2026 Fredrik Norén\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.\n"
  },
  {
    "path": "MERGETOOL.md",
    "content": "If you have your own merge tool that you would like to use, such as Kaleidoscope or p4merge, you can configure ungit to use it by following these steps:  \n\n\n1. Configuring git\n------------------\n\nThe first step is to configure git so that it knows how to invoke your merge tool. In your home directory, open (or create) the git configuration file .gitconfig. In this file, you will want to add information about your merge tool, it should look something like this:\n\n```ini\n[mergetool \"extMerge\"]\n\tcmd = extMergeTool \"$BASE\" \"$LOCAL\" \"$REMOTE\" \"$MERGED\"\n\ttrustExitCode = false\n```\n\n* `\"extMergeTool\"` is the merge tool you are invoking. This assumes your merge tool was installed and the command is recognized by your system. You may also replace this with the path to your merge tool directly.\n* For best results, refer to the documentation of your merge tool, as it may require different command arguments.\n* The name `\"extMerge\"` can be whatever you want. I recommend that it not contain spaces or special symbols, as it may interfere when used as a command argument.\n* `\"trustExitCode\"` depends on the merge tool you are using. If `true`, git will use the return code of your merge tool to determine whether the conflict has been resolved, otherwise it will use the timestamp of the file to determine this (meaning if your merge tool saved over the file, it will assume it has been resolved).\n* Additionally, you can also provide the following if you want to identify your merge tool as the default:\n\n```ini\n[merge]\n\ttool = extMerge\n```\n\nIf you wish to test your configuration, open a console in a git repo that is currently waiting for conflict resolution and type the following command:\n`> git mergetool --tool extMerge`\nThis should invoke your merge tool and cycle through each conflicted file.\n\n\n2. Configuring ungit\n--------------------\n\nAdd the `\"mergeTool\"` option to your ungit configuration file (.ungitrc). Set this value to `true` if you have configured a default merge tool with git, otherwise use the name of the merge tool you have configured (for example `\"extMerge\"`). It should look something like this:\n\n```json\n{\n  \"mergeTool\": \"extMerge\"\n}\n```\n\n3. Use ungit's interface\n------------------------\n\nStart ungit and navigate to a repo with conflicted files. Now when you hover over the `Conflicts` label displayed on one of the conflicted files, it should expand and give you an option to `Launch Merge Tool`.\n\nOnce you have used your merge tool to resolve the conflicts, if ungit does not immediately recognize this, you may use the `Mark as Resolved` button to manually tell git that the file is now resolved.\n\n\n4. Known Issues and Troubleshooting\n-----------------------------------\n\n* In some cases, your merge tool may take a few seconds to launch. Pressing the launch button multiple times will cause your merge tool to launch that many copies.\n* Some merge tools (like `vimdiff`) are terminal-only tools and will not work with ungit. Your merge tool must work in a windowed environment. See the suggested merge tool section below.\n* If for any reason git does not recognize that your merge tool has resolved the file, `trustExitCode` may need to be set to `false`.\n* When your merge tool is launched, four auto-generated files will appear. If you have `trustExitCode` set to `false` and you cancel the merge tool, it may leave the generated files there. In this case, it is safe to manually remove them.\n\n\n5. Merge Tool Suggestions\n-------------------------\n* Mac OS X:\n  * Meld: [meldmerge.org](https://meldmerge.org)\n  * Kaleidoscope: [kaleidoscopeapp.com](https://www.kaleidoscopeapp.com)\n  * Araxis Merge: [araxis.com](https://araxis.com/merge)\n  * DeltaWalker: [deltopia.com](https://deltopia.com)\n* Windows:\n  * Beyond Compare: [scootersoftware.com](https://scootersoftware.com/)\n  * Araxis Merge: [araxis.com](https://araxis.com/merge)\n  * P4Merge: [perforce.com](https://perforce.com/products/helix-core-apps/merge-diff-tool-p4merge)\n"
  },
  {
    "path": "PLUGINS.md",
    "content": "Writing Ungit plugins\n=====================\n\nIt's super easy to write an Ungit plugin. Here's how to write a completely new (though super simple) git log ui:\n\n### 1. Create a new folder for your plugin.\nCreate a folder at `~/.ungit/plugins/MY_FANCY_PLUGIN`, then add a file called `ungit-plugin.json` with the following content:\n```JSON\n{\n  \"exports\": {\n    \"javascript\": \"example.js\"\n  }\n}\n```\n\n### 2. Add some code\nCreate an `example.js` file and add this:\n\n```JavaScript\nvar components = require('ungit-components');\n\n// We're overriding the graph component here\ncomponents.register('graph', function(args) {\n  return {\n    // This method creates and returns the DOM node that represents this component.\n    updateNode: function() {\n      var node = document.createElement('div');\n      // Request all log entries from the backend\n      args.server.get('/log', { path: args.repoPath, limit: 50 }, function(err, log) {\n        // Add all log entries to the parent node\n        log.forEach(function(entry) {\n          var entryNode = document.createElement('div');\n          entryNode.innerHTML = entry.message;\n          node.appendChild(entryNode);\n        });\n      });\n      return node;\n    }\n  };\n});\n```\n\n### 3. Done!\nJust restart Ungit, or if you have `\"dev\": true` in your `.ungitrc` you can just refresh your browser.  A [gerrit plugin example](https://github.com/FredrikNoren/ungit-gerrit) can be found here.\n\n### Ungit Plugin API version\nThe Ungit Plugin API follows semver, and the current version can be found in the package.json (ungitPluginApiVersion). On the frontend it can be accessed from `ungit.pluginApiVersion` and on the backend `env.pluginApiVersion`.\n\n### Components\n\nEach functionalities within ungit is built as components.  Each components is an ungit plugin that is checked into main repository.  All the components in Ungit is built as plugins, take a look in the [components](https://github.com/FredrikNoren/ungit/tree/master/components) directory for inspiration.\n\nAn [example](https://github.com/FredrikNoren/ungit/tree/master/components/staging) of ungit component with view can be seen below.\n\n```JSON\n{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"staging\": \"staging.html\"\n    },\n    \"javascript\": \"staging.bundle.js\",\n    \"css\": \"staging.css\"\n  }\n}\n```\n\n* Views(html) for Component\n\n   Each component can have multiple views as exampled [here](https://github.com/FredrikNoren/ungit/tree/master/components/dialogs).\n\n* CSS for Component\n   css file can be easily defined per components and in above example we can see that `staging.less` file is compiled into `staging.css` via `npm run build` script.\n\n* JS for Component\n\n   Each component gets to have one javascipt files.  However each javasciprt file can require other javascript in it's directory or other libraries.  If you are doing require by relative paths as exampled in [graph.js](https://github.com/FredrikNoren/ungit/blob/master/components/graph/graph.js), you wouldn't have to include the js in browserify job in [`scripts/build.js`](https://github.com/FredrikNoren/ungit/blob/master/scripts/build.js).\n"
  },
  {
    "path": "README.md",
    "content": "ungit\n======\n[![Release](https://img.shields.io/github/v/release/FredrikNoren/ungit)](https://github.com/FredrikNoren/ungit/releases)\n[![CI](https://github.com/FredrikNoren/ungit/actions/workflows/ci.yml/badge.svg)](https://github.com/FredrikNoren/ungit/actions/workflows/ci.yml)\n[![Join the chat at https://gitter.im/FredrikNoren/ungit](https://badges.gitter.im/FredrikNoren/ungit.svg)](https://gitter.im/FredrikNoren/ungit?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)\n\nThe easiest way to use git. On any platform. Anywhere.\n\n[![xkcd](xkcd.png \"If that doesn't fix it, git.txt contains the phone number of a friend of mine who understands git. Just wait through a few minutes of 'It's really pretty simple, just think of branches as...' and eventually you'll learn the commands that will fix everything.\")](https://xkcd.com/1597/)\n\nGit is known for being a versatile distributed source control system that is a staple of many individuals, communities, and even for [the City of Chattanooga to crowd source bicycle parking locations](https://github.com/cityofchattanooga/Bicycle-Parking).  However, it is not known for userfriendliness or easy learning curve.\n\nUngit brings user friendliness to git without sacrificing the versatility of git.\n\n * Clean and intuitive UI that makes it easy to _understand_ git.\n * Runs on any platform that node.js & git supports.\n * Web-based, meaning you can run it on your cloud/pure shell machine and use the ui from your browser (just browse to http://your-cloud-machine.com:8448).\n * Works well with GitHub.\n * [Gerrit](https://code.google.com/p/gerrit/) integration through plugin: https://github.com/FredrikNoren/ungit-gerrit\n\n[Follow @ungitui on twitter](https://twitter.com/ungitui)\n\nQuick intro to ungit: [https://youtu.be/hkBVAi3oKvo](https://youtu.be/hkBVAi3oKvo)\n\n[![Screenshot](screenshot.png)](https://youtu.be/hkBVAi3oKvo)\n\nInstalling\n----------\nRequires [node.js](https://nodejs.org) (≥ 20.19.0), [npm](https://www.npmjs.com/) (≥ 10.8.2, comes with node.js) and [git](https://git-scm.com/) (≥ 2.34.x). To install ungit just type:\n\n\tnpm install -g ungit\n\nNOTE: If your system requires root access to install global npm packages, make sure you use the -H flag:\n\n\tsudo -H npm install -g ungit\n\nPrebuilt [electron](https://electronjs.org/) packages are available [here](https://github.com/FredrikNoren/ungit/releases) (git is still required).\n\nUsing\n-----\nAnywhere you want to start, just type:\n\n\tungit\n\nThis will launch the server and open up a browser with the ui.\n\nConfiguring\n-----------\nPut a configuration file called .ungitrc in your home directory (`/home/USERNAME` on \\*nix, `C:/Users/USERNAME/` on windows). Configuration file must be in json format. See [source/config.js](source/config.js) for available options.\n\nYou can also override configuration variables at launch by specifying them as command line arguments; `ungit --port=8080`. To disable boolean features use --no: `ungit --no-autoFetch`.\n\nExample of `~/.ungitrc` configuration file to change default port and enable bugtracking:\n\n```json\n{\n\t\"port\": 8080,\n\t\"bugtracking\": true\n}\n```\n\nPGP\n---\n[Git](https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work) and [github](https://help.github.com/articles/signing-commits-using-gpg/) both supports PGP signing.  Within Ungit these features can be enabled via doing either one of the below two actions.\n\n- `git config --global commit.gpgsign true` (or without `--global` at the repo)\n- Add `isForceGPGSign: true` to `ungit.rc` file\n\nCurrently, Ungit __DOES NOT__ support GPG authentication!  While git allows robust programmatic authentication via [`credential-helper`](https://help.github.com/articles/telling-git-about-your-gpg-key/), I could not find an easy way to do something equivalent with GPG.  \n\nTherefore, password-less gpg authentication or 3rd party gpg password must be configured when using Ungit to commit with gpg.\nBelow are several way to enable password-less gpg authentication for various OSs.\n\n- [Cache GnuPG passphrase](https://superuser.com/questions/624343/keep-gnupg-credentials-cached-for-entire-user-session)\n- gpg-agent with pinentry-mac\n  1. `brew install gnupg gpg-agent pinentry-mac`\n  2. `echo \"test\" | gpg --clearsign` # See gpg authentication prompt when gpg is accessed.\n  3. Optionally you can save it to keychain. ![gpg_save_screenshot](gpg_save_screenshot.png)\n\nI understand this is not convenient, but security is hard. And I'd much rather have bit of inconvenience than Ungit having security exposure.\n\n\nExternal Merge Tools\n--------------------\nIf you have your own merge tool that you would like to use, such as Kaleidoscope or p4merge, you can configure ungit to use it. See [MERGETOOL.md](MERGETOOL.md).\n\nAuto Refresh\n------------\nUngit will watch git directory recursively upon page view and automatically refresh contents on git operations or changes on files that are not configured to be ignored in `.gitignore`.\n\nText Editor Integrations\n-------------------\n\n* [atom-ungit](https://github.com/codingtwinky/atom-ungit) for [Atom.io](https://atom.io/) by [@codingtwinky](https://github.com/codingtwinky)\n\n![atom-ungit Screenshot](https://raw.githubusercontent.com/codingtwinky/atom-ungit/master/screenshot.png)\n\n* [brackets-ungit](https://github.com/Hirse/brackets-ungit) for [Brackets.io](http://brackets.io/) by [@Hirse](https://github.com/Hirse)\n\n![brackets-ungit Screenshot](https://raw.githubusercontent.com/Hirse/brackets-ungit/master/images/viewer.png)\n\n* [vscode-ungit](https://marketplace.visualstudio.com/items?itemName=Hirse.vscode-ungit) for [Visual Studio Code](https://code.visualstudio.com/) by [@Hirse](https://github.com/Hirse)\n\n![VSCode-Ungit screenshot](https://raw.githubusercontent.com/hirse/vscode-ungit/master/screenshots/ungit.gif)\n\n\nDeveloping\n----------\n\nSee [CONTRIBUTING.md](CONTRIBUTING.md).\n\nMaintainers\n-----------\n\n* [FredrikNoren](https://github.com/FredrikNoren) [Fredrik's Patreon page for donations](https://www.patreon.com/fredriknoren)\n* [Jung-Kim](https://github.com/jung-kim) [JK's (codingtwinky) Patreon page for donations](https://www.patreon.com/jungkim)\n* [campersau](https://github.com/campersau)\n\nKnown issues\n------------\n\n* If you're running MacOSX Mavericks and Ungit crashes after a few seconds; try updating npm and node. See [#259](https://github.com/FredrikNoren/ungit/issues/259) and [#249](https://github.com/FredrikNoren/ungit/issues/249) for details.\n* Ubuntu users may have trouble installing because the node executable is named differently on Ubuntu, see [#401](https://github.com/FredrikNoren/ungit/issues/401) for details.\n* Debian Wheezy's supported git and nodejs packages are too old, therefore download newest [git](https://github.com/git/git/releases) and [nodejs](https://nodejs.org/download/) tarballs and [build from source](https://www.control-escape.com/linux/lx-swinstall-tar.html).\n* Adblocker may block Ungit! Some ad blockers, such as [Adblock plus](https://adblockplus.org) and [uBlock](https://www.ublock.org/), don't like localhost api calls and assume that it is a cross domain attack.  Please whitelist `{localhost|127.0.0.1|$UngitURL}:{ungit port number}`. [#887](https://github.com/FredrikNoren/ungit/issues/887) [#892](https://github.com/FredrikNoren/ungit/issues/892)\n* Running git in non English language will result in unexpected behavior!  Ungit parses git command results in English to detect repos' states and this causes confusion when git results are not in English. [#959](https://github.com/FredrikNoren/ungit/issues/959)\n\nChangelog\n---------\nSee [CHANGELOG.md](CHANGELOG.md).\n\nLicense (MIT)\n-------------\nSee [LICENSE.md](LICENSE.md). To read about the Faircode experiment go to [#974](https://github.com/FredrikNoren/ungit/issues/974). Ungit is now once again MIT.\n\n[![Dependency Status](https://david-dm.org/FredrikNoren/ungit.svg)](https://david-dm.org/FredrikNoren/ungit)\n[![devDependency Status](https://david-dm.org/FredrikNoren/ungit/dev-status.svg)](https://david-dm.org/FredrikNoren/ungit#info=devDependencies)\n"
  },
  {
    "path": "appveyor.yml",
    "content": "skip_branch_with_pr: true\nimage: Visual Studio 2022\n\nenvironment:\n  matrix:\n    - nodejs_version: '' # latest\n    - nodejs_version: '22.12'\n    - nodejs_version: '20.19'\n\nbranches:\n  only:\n    - master\n\ninstall:\n  - ps: |\n      try {\n        Install-Product node $env:nodejs_version x64\n      } catch {\n        Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) x64\n      }\n  - node --version\n  - npm --version\n  - npm ci\n\nbuild_script:\n  - npm run lint\n  - npm run build\n\nbefore_test:\n  - git config --global user.email \"test@testy.com\"\n  - git config --global user.name \"Test testy\"\n  - git config --global protocol.file.allow always # tests use file based submodules see #1539\n  - git --version\n\ntest_script:\n  - npm test\n"
  },
  {
    "path": "bin/credentials-helper",
    "content": "#!/usr/bin/env node\nconst http = require('http');\nconst socketId = process.argv[2];\nconst portAndRootPath = process.argv[3];\nconst remote = process.argv[4];\nconst action = process.argv[5];\n\nif (action == 'get') {\n  http\n    .get(\n      `http://localhost:${portAndRootPath}/api/credentials?socketId=${socketId}&remote=${encodeURIComponent(\n        remote\n      )}`,\n      (res) => {\n        let rawData = '';\n        res.on('data', (chunk) => {\n          rawData += chunk;\n        });\n        res.on('end', () => {\n          const data = JSON.parse(rawData);\n          console.log(`username=${data.username}`);\n          console.log(`password=${data.password ? data.password : ''}`);\n        });\n      }\n    )\n    .on('error', (err) => {\n      console.error(\"Error getting credentials, couldn't query server\", err);\n    });\n} else {\n  console.info(`Unhandled action: ${action}`);\n}\n"
  },
  {
    "path": "bin/ungit",
    "content": "#!/usr/bin/env node\n\nconst startLaunchTime = Date.now();\n\nconst config = require('../source/config');\nconst openPromise = import('open');\nconst path = require('path');\nconst child_process = require('child_process');\nconst { encodePath } = require('../source/address-parser');\n\nconst BugTracker = require('../source/bugtracker');\nconst bugtracker = new BugTracker('launcher');\n// Fastest way to find out if a port is used or not/i.e. if ungit is running\nconst net = require('net');\nconst server = net.createServer();\nlet child;\nconst cleanExit = () => {\n  if (child) {\n    child.kill('SIGINT');\n  }\n  process.exit();\n};\n\nprocess.on('SIGINT', cleanExit); // catch ctrl-c\nprocess.on('SIGTERM', cleanExit); // catch kill\nprocess.on('uncaughtException', (err) => {\n  console.error(err.stack.toString());\n\n  bugtracker.notify(err, 'ungit-launcher');\n  cleanExit();\n});\n\nconst openUngitBrowser = (pathToNavigateTo) => {\n  console.log(`Navigate to ${pathToNavigateTo}`);\n  return openPromise\n    .then((open) => {\n      return open.default(pathToNavigateTo);\n    })\n    .catch((err) => console.log(`failed to navigate to ${pathToNavigateTo}`, err));\n};\n\nconst navigate = () => {\n  let url = config.urlBase + ':' + config.port;\n  if (config.forcedLaunchPath === undefined) {\n    url += '/#/repository?path=' + encodePath(process.cwd());\n  } else if (config.forcedLaunchPath !== null && config.forcedLaunchPath !== '') {\n    url += '/#/repository?path=' + encodePath(config.forcedLaunchPath);\n  }\n\n  if (config.launchCommand) {\n    const command = config.launchCommand.replace(/%U/g, url);\n    console.log(`Running custom launch command: ${command}`);\n    child_process.exec(command, (err, stdout, stderr) => {\n      if (err) {\n        console.log('Failed to exec custom launchCommand', err, stderr);\n        return;\n      }\n      if (config.launchBrowser) {\n        openUngitBrowser(url);\n      }\n    });\n  } else if (config.launchBrowser) {\n    openUngitBrowser(url);\n  }\n};\n\nconst launch = () => {\n  child = child_process.fork(\n    path.join(__dirname, '..', 'source', 'server.js'),\n    process.argv.slice(2),\n    { cwd: path.join(process.cwd(), '..'), silent: true }\n  );\n\n  child.on('exit', (res) => {\n    console.log('Stopped keeping ungit alive');\n  });\n\n  const startupListener = (data) => {\n    if (data.toString().indexOf('## Ungit started ##') >= 0) {\n      child.removeListener('stdout', startupListener);\n      child.stdout.on('data', (data) => console.log(data.toString().trim()));\n      const launchTime = Date.now() - startLaunchTime;\n      console.log(data.toString());\n      console.log(`Took ${launchTime}ms to start server.`);\n      navigate();\n    }\n  };\n\n  child.stdout.on('data', startupListener);\n  child.stderr.on('data', (data) => console.log(`stderr: ${data.toString().trim()}`));\n};\n\nserver.listen({ port: config.port, host: config.ungitBindIp }, (err) => {\n  server.close(launch);\n});\nserver.on('error', (e) => {\n  if (e.code == 'EADDRINUSE') {\n    console.log('Ungit server already running');\n    navigate();\n  } else {\n    console.error('Failed to run server: ', e);\n    process.exit(1);\n  }\n});\n"
  },
  {
    "path": "clicktests/environment.js",
    "content": "'use strict';\nconst logger = require('../source/utils/logger');\nconst child_process = require('child_process');\nconst puppeteer = require('puppeteer');\nconst request = require('superagent');\nconst mkdirp = require('mkdirp').mkdirp;\nconst rimraf = require('rimraf').rimraf;\nconst { encodePath } = require('../source/address-parser');\nconst portfinder = require('portfinder');\nconst portrange = 45032;\n\nmodule.exports = (config) => new Environment(config);\n\nconst prependLines = (pre, text) => {\n  return text\n    .split('\\n')\n    .filter((l) => l)\n    .map((line) => pre + line)\n    .join('\\n');\n};\n\n// Environment provides\nclass Environment {\n  constructor(config) {\n    this.config = config || {};\n    this.config.rootPath = typeof this.config.rootPath === 'string' ? this.config.rootPath : '';\n    this.config.serverTimeout = this.config.serverTimeout || 35000;\n    this.config.headless = this.config.headless === undefined ? true : this.config.headless;\n    this.config.viewWidth = 1920;\n    this.config.viewHeight = 1080;\n    this.config.serverStartupOptions = this.config.serverStartupOptions || [];\n    this.shuttinDown = false;\n  }\n\n  getRootUrl() {\n    return this.rootUrl;\n  }\n\n  async init() {\n    try {\n      this.browser = await puppeteer.launch({\n        headless: this.config.headless,\n        defaultViewport: {\n          width: this.config.viewWidth,\n          height: this.config.viewHeight,\n        },\n      });\n      await this.startServer();\n      await new Promise((resolve) => setTimeout(resolve, 1000));\n    } catch (err) {\n      logger.error(err);\n      throw new Error('Cannot confirm ungit start!!', { cause: err });\n    }\n  }\n\n  async startServer() {\n    this.port = await portfinder.getPortPromise({ port: portrange });\n    this.rootUrl = `http://127.0.0.1:${this.port}${this.config.rootPath}`;\n    logger.info(`Starting ungit server:${this.port} with ${this.config.serverStartupOptions}`);\n\n    this.hasStarted = false;\n    const options = [\n      'bin/ungit',\n      '--cliconfigonly',\n      `--port=${this.port}`,\n      `--rootPath=${this.config.rootPath}`,\n      '--no-launchBrowser',\n      '--dev',\n      '--no-bugtracking',\n      `--autoShutdownTimeout=${this.config.serverTimeout}`,\n      '--logLevel=debug',\n      '--maxNAutoRestartOnCrash=0',\n      '--no-autoCheckoutOnBranchCreate',\n      '--alwaysLoadActiveBranch',\n      `--numRefsToShow=${this.config.numRefsToShow || 5}`,\n    ].concat(this.config.serverStartupOptions);\n\n    const ungitServer = (this.ungitServerProcess = child_process.spawn('node', options));\n\n    return new Promise((resolve, reject) => {\n      ungitServer.stdout.on('data', (stdout) => {\n        const stdoutStr = stdout.toString();\n        console.log(prependLines('[server] ', stdoutStr));\n\n        if (stdoutStr.indexOf('Ungit server already running') >= 0) {\n          logger.info('server-already-running');\n        }\n\n        if (stdoutStr.indexOf('## Ungit started ##') >= 0) {\n          if (this.hasStarted) {\n            reject(new Error('Ungit started twice, probably crashed.'));\n          } else {\n            this.hasStarted = true;\n            logger.info('Ungit server started.');\n            resolve();\n          }\n        }\n      });\n      ungitServer.stderr.on('data', (stderr) => {\n        const stderrStr = stderr.toString();\n        logger.error(prependLines('[server ERROR] ', stderrStr));\n        if (stderrStr.indexOf('EADDRINUSE') > -1) {\n          logger.info('retrying with different port');\n          ungitServer.kill('SIGINT');\n          reject(new Error('EADDRINUSE'));\n        }\n      });\n      ungitServer.on('exit', () => logger.info('UNGIT SERVER EXITED'));\n    });\n  }\n\n  async shutdown() {\n    this.shuttinDown = true;\n\n    await this.backgroundAction('POST', '/api/testing/cleanup');\n\n    if (this.ungitServerProcess) {\n      this.ungitServerProcess.kill('SIGINT');\n      this.ungitServerProcess = null;\n    }\n\n    if (this.browser) {\n      await this.browser.close();\n      this.browser = null;\n      this.page = null;\n    }\n  }\n\n  // server helpers\n\n  async backgroundAction(method, url, body) {\n    url = this.getRootUrl() + url;\n\n    let req;\n    if (method === 'GET') {\n      req = request.get(url).withCredentials().query(body);\n    } else if (method === 'POST') {\n      req = request.post(url).send(body);\n    } else if (method === 'DELETE') {\n      req = request.delete(url).send(body);\n    }\n\n    req.set({ encoding: 'utf8', 'cache-control': 'no-cache', 'Content-Type': 'application/json' });\n\n    const response = await req;\n    return response.body;\n  }\n\n  async createRepos(testRepoPaths, config) {\n    for (let i = 0; i < config.length; i++) {\n      const conf = config[i];\n      conf.bare = !!conf.bare;\n      await this.initRepo(conf);\n      await this.createCommits(conf, conf.initCommits);\n      testRepoPaths.push(conf.path);\n    }\n  }\n\n  async initRepo(options) {\n    if (options.path) {\n      await rimraf(options.path);\n      await mkdirp(options.path);\n    } else {\n      logger.info('Creating temp folder');\n      options.path = await this.createTempFolder();\n    }\n    await this.backgroundAction('POST', '/api/init', options);\n  }\n\n  async createTempFolder() {\n    const res = await this.backgroundAction('POST', '/api/testing/createtempdir');\n    return res.path;\n  }\n\n  async createCommits(config, limit, x) {\n    x = x || 0;\n    if (!limit || limit < 0 || x === limit) return;\n\n    await this.createTestFile(`${config.path}/testy${x}`);\n    await this.backgroundAction('POST', '/api/commit', {\n      path: config.path,\n      message: `Init Commit ${x}`,\n      files: [{ name: `testy${x}` }],\n    });\n    // `createCommits()` is used at create repo `this.page` may not be inited\n    await this.createCommits(config, limit, x + 1);\n  }\n\n  async createTestFile(filename, repoPath) {\n    await this.backgroundAction('POST', '/api/testing/createfile', {\n      file: filename,\n      path: repoPath,\n    });\n  }\n\n  // browser helpers\n\n  async goto(url) {\n    logger.info('Go to page: ' + url);\n\n    if (!this.page) {\n      const pages = await this.browser.pages();\n      this.page = pages[0];\n      this.page.on('console', (message) => {\n        const text = `[ui ${message.type()}] ${message.text()}`;\n\n        if (message.type() === 'error' && !this.shuttinDown) {\n          const stackTraceString = message\n            .stackTrace()\n            .map((trace) => `\\t${trace.lineNumber}: ${trace.url}`)\n            .join('\\n');\n          logger.error(text, stackTraceString);\n        } else {\n          // text already has timestamp and etc as it is generated by logger as well.\n          console.log(text);\n        }\n      });\n    }\n\n    await this.page.goto(url);\n  }\n\n  async openUngit(tempDirPath) {\n    await this.goto(`${this.getRootUrl()}/#/repository?path=${encodePath(tempDirPath)}`);\n    await this.waitForElementVisible('.repository-actions');\n    await this.page.waitForNetworkIdle();\n  }\n\n  waitForElementVisible(selector, timeout) {\n    logger.debug(`Waiting for visible: \"${selector}\"`);\n    return this.page.waitForSelector(selector, { visible: true, timeout: timeout || 6000 });\n  }\n  waitForElementHidden(selector, timeout) {\n    logger.debug(`Waiting for hidden: \"${selector}\"`);\n    return this.page.waitForSelector(selector, { hidden: true, timeout: timeout || 6000 });\n  }\n  wait(duration) {\n    return new Promise((resolve) => setTimeout(resolve, duration));\n  }\n\n  type(text) {\n    return this.page.keyboard.type(text);\n  }\n  async insert(selector, text) {\n    await this.waitForElementVisible(selector);\n    await this.page.$eval(selector, (ele) => (ele.value = ''));\n    await this.page.focus(selector);\n    await this.type(text);\n  }\n  press(key) {\n    return this.page.keyboard.press(key);\n  }\n\n  async click(selector, clickCount) {\n    logger.info(`clicking \"${selector}\"`);\n\n    for (let i = 0; i < 3; i++) {\n      try {\n        const toClick = await this.waitForElementVisible(selector);\n        await this.wait(200);\n        await toClick.click({ delay: 100, clickCount: clickCount });\n        break;\n      } catch (err) {\n        logger.error('error while clicking', err);\n      }\n    }\n    logger.info(`clicked \"${selector}`);\n  }\n\n  waitForBranch(branchName) {\n    const currentBranch = 'document.querySelector(\".ref.branch.current\")';\n    return this.page.waitForFunction(\n      `${currentBranch} && ${currentBranch}.innerText && ${currentBranch}.innerText.trim() === \"${branchName}\"`,\n      { polling: 250 }\n    );\n  }\n\n  async commit(commitMessage) {\n    await this.waitForElementVisible('.files .file .btn-default');\n    await this.insert('.staging input.form-control', commitMessage);\n    const postCommitProm = this.setApiListener('/commit', 'POST');\n    await this.click('.commit-btn');\n    await postCommitProm;\n    await this.waitForElementHidden('.files .file .btn-default');\n  }\n\n  async _createRef(type, name) {\n    await this.click('.current ~ .new-ref button.showBranchingForm');\n    await this.insert('.ref-icons.new-ref.editing input', name);\n    await this.wait(500);\n    const createRefProm =\n      type === 'branch'\n        ? this.setApiListener('/branches', 'POST')\n        : this.setApiListener('/tags', 'POST');\n    await this.click(`.new-ref ${type === 'branch' ? '.btn-primary' : '.btn-default'}`);\n    await createRefProm;\n    await this.waitForElementVisible(`.ref.${type}[data-ta-name=\"${name}\"]`);\n    await this.ensureRedraw();\n  }\n  createTag(name) {\n    return this._createRef('tag', name);\n  }\n  createBranch(name) {\n    return this._createRef('branch', name);\n  }\n\n  async _verifyRefAction(action) {\n    try {\n      await this.page.waitForSelector('.modal-dialog .btn-primary', {\n        visible: true,\n        timeout: 2000,\n      }); // not all ref actions opens dialog, this line may throw exception.\n      await this.awaitAndClick('.modal-dialog .btn-primary');\n    } catch {\n      /* ignore */\n    }\n    await this.waitForElementHidden(`[data-ta-action=\"${action}\"]:not([style*=\"display: none\"])`);\n    await this.ensureRedraw();\n  }\n\n  async _refAction(ref, local, action, validateFunc) {\n    if (!this[`_${action}ResponseWatcher`]) {\n      this.page.on('response', async (response) => {\n        const url = response.url();\n        const method = response.request().method();\n\n        if (validateFunc(url, method)) {\n          this.page.evaluate(`ungit._${action}Response = true`);\n        }\n      });\n      this[`_${action}ResponseWatcher`] = true;\n    }\n    await this.clickOnNode(`.branch[data-ta-name=\"${ref}\"][data-ta-local=\"${local}\"]`);\n    await this.click(`[data-ta-action=\"${action}\"]:not([style*=\"display: none\"]) .dropmask`);\n    await this._verifyRefAction(action);\n    await this.page.waitForFunction(`ungit._${action}Response`, { polling: 250 });\n    await this.page.evaluate(`ungit._${action}Response = undefined`);\n  }\n\n  async pushRefAction(ref, local) {\n    await this._refAction(ref, local, 'push', (url, method) => {\n      if (method !== 'POST') {\n        return false;\n      }\n      if (\n        url.indexOf('/push') === -1 &&\n        url.indexOf('/tags') === -1 &&\n        url.indexOf('/branches') === -1\n      ) {\n        return false;\n      }\n      return true;\n    });\n  }\n\n  async rebaseRefAction(ref, local) {\n    await this._refAction(ref, local, 'rebase', (url, method) => {\n      return method === 'POST' && url.indexOf('/rebase') >= -1;\n    });\n  }\n\n  async mergeRefAction(ref, local) {\n    await this._refAction(ref, local, 'merge', (url, method) => {\n      return method === 'POST' && url.indexOf('/merge') >= -1;\n    });\n  }\n\n  async moveRef(ref, targetNodeCommitTitle) {\n    await this.clickOnNode(`.branch[data-ta-name=\"${ref}\"]`);\n    if (!this._isMoveResponseWatcherSet) {\n      this.page.on('response', async (response) => {\n        const url = response.url();\n        if (response.request().method() !== 'POST') {\n          return;\n        }\n        if (\n          url.indexOf('/reset') === -1 &&\n          url.indexOf('/tags') === -1 &&\n          url.indexOf('/branches') === -1\n        ) {\n          return;\n        }\n        this.page.evaluate('ungit._moveEventResponded = true');\n      });\n      this._isMoveResponseWatcherSet = true;\n    }\n    await this.click(\n      `[data-ta-node-title=\"${targetNodeCommitTitle}\"] [data-ta-action=\"move\"]:not([style*=\"display: none\"]) .dropmask`\n    );\n    await this._verifyRefAction('move');\n    await this.page.waitForFunction('ungit._moveEventResponded', { polling: 250 });\n    await this.page.evaluate('ungit._moveEventResponded = undefined');\n  }\n\n  // Explicitly trigger two program events.\n  // Usually these events are triggered by mouse movements, or api calls\n  // and etc.  This function is to help mimic those movements.\n  triggerProgramEvents() {\n    return this.page.evaluate(() => {\n      const isActive = ungit.programEvents.active;\n      if (!isActive) {\n        ungit.programEvents.active = true;\n      }\n      ungit.programEvents.dispatch({ event: 'working-tree-changed' });\n      if (!isActive) {\n        ungit.programEvents.active = false;\n      }\n    });\n  }\n\n  async ensureRedraw() {\n    logger.debug('ensureRedraw triggered');\n    if (!this._gitlogResposneWatcher) {\n      this.page.on('response', async (response) => {\n        if (response.url().indexOf('/gitlog') > 0 && response.request().method() === 'GET') {\n          this.page.evaluate('ungit._gitlogResponse = true');\n        }\n      });\n      this._gitlogResposneWatcher = true;\n    }\n    await this.page.evaluate('ungit._gitlogResponse = undefined');\n    await this.triggerProgramEvents();\n    await this.page.waitForFunction('ungit._gitlogResponse', { polling: 250 });\n    await this.page.waitForFunction(\n      'ungit.__app.content().repository().graph._isLoadNodesFromApiRunning === false',\n      { polling: 250 }\n    );\n    logger.debug('ensureRedraw finished');\n  }\n\n  async awaitAndClick(selector, time = 1000) {\n    await this.wait(time);\n    await this.click(selector);\n  }\n\n  // After a click on `git-node` or `git-ref`, ensure `currentActionContext` is set\n  async clickOnNode(nodeSelector) {\n    await this.awaitAndClick(nodeSelector);\n    await this.page.waitForFunction(\n      () => {\n        const app = ungit.__app;\n        if (!app) {\n          return;\n        }\n        const path = app.content();\n        if (!path || path.constructor.name !== 'PathViewModel') {\n          return;\n        }\n        const repository = path.repository();\n        if (!repository) {\n          return;\n        }\n        const graph = repository.graph;\n        if (!graph) {\n          return;\n        }\n        return graph.currentActionContext();\n      },\n      { polling: 250 }\n    );\n    logger.debug(`clickOnNode ${nodeSelector} finished`);\n  }\n\n  // If an api call matches `apiPart` and `method` is called, set the `globalVarName`\n  // to true. Use for detect if an API call was made and responded.\n  setApiListener(apiPart, method, bodyMatcher = () => true) {\n    const randomVariable = `ungit._${Math.floor(Math.random() * 500000)}`;\n    this.page.on(\n      'response',\n      async (response) => {\n        if (response.url().indexOf(apiPart) > -1 && response.request().method() === method) {\n          if (bodyMatcher(await response.json())) {\n            // reponse body matcher is matched, set the value to true\n            this.page.evaluate(`${randomVariable} = true`);\n          }\n        }\n      },\n      { polling: 250 }\n    );\n    return this.page\n      .waitForFunction(`${randomVariable} === true`, { polling: 250 })\n      .then(() => this.page.evaluate(`${randomVariable} = undefined`));\n  }\n}\n"
  },
  {
    "path": "clicktests/spec.authentication.js",
    "content": "'use strict';\nconst testuser = { username: 'testuser', password: 'testpassword' };\nconst environment = require('./environment')({\n  serverStartupOptions: ['--authentication', `--users.${testuser.username}=${testuser.password}`],\n  showServerOutput: true,\n});\n\ndescribe('[AUTHENTICATION]', () => {\n  before('Environment init without temp folder', () => environment.init());\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open home screen should show authentication dialog', async function () {\n    this.retries(3);\n    await environment.goto(environment.getRootUrl());\n    await environment.waitForElementVisible('.login');\n  });\n\n  it('Filling out the authentication with wrong details should result in an error', async function () {\n    this.retries(3);\n    await environment.insert('.login #inputUsername', testuser.username);\n    await environment.insert('.login #inputPassword', 'notthepassword');\n    await environment.click('.login button');\n    await environment.waitForElementVisible('.login .loginError');\n  });\n\n  it('Filling out the authentication should bring you to the home screen', async function () {\n    this.retries(3);\n    await environment.insert('.login #inputUsername', testuser.username);\n    await environment.insert('.login #inputPassword', testuser.password);\n    await environment.click('.login button');\n    await environment.waitForElementVisible('.container.home');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.bare.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[BARE]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: true }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[0]);\n  });\n\n  it('update branches button without branches', async () => {\n    const apiResponseProm = environment.setApiListener('/branches?', 'GET');\n    const refResponseProm = environment.setApiListener('/refs?', 'GET');\n    await environment.click('.btn-group.branch .btn-main');\n    await apiResponseProm;\n    await refResponseProm;\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.branches.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\nconst _ = require('lodash');\n\ndescribe('[BRANCHES]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[0]);\n  });\n\n  it('add a commit', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.commit('commit-1');\n  });\n\n  // < branch search test >\n  it('add branches', async () => {\n    await environment.createBranch('search-1');\n    await environment.createBranch('search-2');\n    await environment.createBranch('search-3');\n    await environment.createBranch('search-4');\n    await environment.waitForElementVisible('[data-ta-name=\"search-4\"]');\n  });\n\n  it('add tag should make one of the branch disappear', async () => {\n    const branchesResponse = environment.setApiListener('/tags', 'POST');\n    await environment.createTag('tag-1');\n    await branchesResponse;\n    await environment.waitForElementHidden('[data-ta-name=\"search-4\"]');\n  });\n\n  it('search for the hidden branch', async () => {\n    await environment.awaitAndClick('.showSearchForm');\n    await environment.wait(500);\n    await environment.type('-4');\n    await environment.waitForElementVisible('.branch-search');\n    await environment.page.waitForFunction(\n      'document.querySelectorAll(\".ui-menu-item-wrapper\").length > 0 && document.querySelectorAll(\".ui-menu-item-wrapper\")[0].text.trim() === \"search-4\"',\n      { polling: 250 }\n    );\n    await environment.press('ArrowDown');\n    await environment.press('Enter');\n\n    await environment.waitForElementVisible('[data-ta-name=\"search-4\"]', 10000);\n  });\n\n  it('updateBranches button without branches', async () => {\n    const branchesResponse = environment.setApiListener('/branches?', 'GET', (body) => {\n      return _.isEqual(body, [\n        { name: 'master', current: true },\n        { name: 'search-1' },\n        { name: 'search-2' },\n        { name: 'search-3' },\n        { name: 'search-4' },\n      ]);\n    });\n    const refsResponse = environment.setApiListener('/refs?', 'GET', (body) => {\n      body.forEach((ref) => delete ref.sha1);\n      return _.isEqual(body, [\n        {\n          name: 'refs/heads/master',\n        },\n        {\n          name: 'refs/heads/search-1',\n        },\n        {\n          name: 'refs/heads/search-2',\n        },\n        {\n          name: 'refs/heads/search-3',\n        },\n        {\n          name: 'refs/heads/search-4',\n        },\n        {\n          name: 'refs/tags/tag-1',\n        },\n      ]);\n    });\n    await environment.click('.btn-group.branch .btn-main');\n    await branchesResponse;\n    await refsResponse;\n  });\n\n  it('add a branch', () => {\n    return environment.createBranch('branch-1');\n  });\n\n  it('updateBranches button with one branch', async () => {\n    const branchesResponse = environment.setApiListener('/branches?', 'GET', (body) => {\n      return _.isEqual(body, [\n        { name: 'branch-1' },\n        { name: 'master', current: true },\n        { name: 'search-1' },\n        { name: 'search-2' },\n        { name: 'search-3' },\n        { name: 'search-4' },\n      ]);\n    });\n    const refsResponse = environment.setApiListener('/refs?', 'GET', (body) => {\n      body.forEach((ref) => delete ref.sha1);\n      return _.isEqual(body, [\n        { name: 'refs/heads/branch-1' },\n        {\n          name: 'refs/heads/master',\n        },\n        {\n          name: 'refs/heads/search-1',\n        },\n        {\n          name: 'refs/heads/search-2',\n        },\n        {\n          name: 'refs/heads/search-3',\n        },\n        {\n          name: 'refs/heads/search-4',\n        },\n        {\n          name: 'refs/tags/tag-1',\n        },\n      ]);\n    });\n    await environment.click('.btn-group.branch .btn-main');\n    await branchesResponse;\n    await refsResponse;\n  });\n\n  it('add second branch', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);\n    await environment.commit('commit-2');\n\n    await environment.createBranch('branch-2');\n    await environment.createBranch('branch-3');\n  });\n\n  it('Check out a branch via selection', async () => {\n    await environment.click('.branch .dropdown-toggle');\n    await environment.click('[data-ta-clickable=\"checkoutrefs/heads/branch-2\"]');\n    await environment.waitForElementVisible('[data-ta-name=\"branch-2\"].current');\n  });\n\n  it('Delete a branch via selection', async () => {\n    const branchDeleteResponse = environment.setApiListener('/branches?', 'DELETE');\n    await environment.click('.branch .dropdown-toggle');\n    await environment.click('[data-ta-clickable=\"refs/heads/branch-3-remove\"]');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await branchDeleteResponse;\n  });\n\n  it('add another commit', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);\n    await environment.commit('commit-3');\n    await environment.ensureRedraw();\n  });\n\n  it('checkout cherrypick base', async () => {\n    const checkoutResponse = environment.setApiListener('/checkout', 'POST');\n    await environment.click('.branch .dropdown-toggle');\n    await environment.click('[data-ta-clickable=\"checkoutrefs/heads/branch-1\"]');\n    await checkoutResponse;\n    await environment.ensureRedraw();\n    await environment.waitForElementVisible('[data-ta-name=\"branch-1\"].current');\n  });\n\n  it('cherrypick abort case', async () => {\n    await environment.wait(1000);\n    await environment.clickOnNode('[data-ta-clickable=\"node-clickable-0\"]');\n    await environment.awaitAndClick(\n      '[data-ta-action=\"cherry-pick\"]:not([style*=\"display: none\"]) .dropmask'\n    );\n    await environment.click('.staging .btn-stg-abort');\n    await environment.awaitAndClick('.modal-dialog .btn-primary', 2000);\n    const gitlogResponse = environment.setApiListener('/gitlog', 'GET', (body) => {\n      return _.isEqual(\n        body.nodes.map((node) => node.message),\n        ['commit-3', 'commit-2', 'commit-1']\n      );\n    });\n    await environment.ensureRedraw();\n    await gitlogResponse;\n  });\n\n  it('cherrypick success case', async () => {\n    const cherrypickPostResponed = environment.setApiListener('/cherrypick', 'POST');\n    await environment.clickOnNode('[data-ta-clickable=\"node-clickable-1\"]');\n    await environment.click(\n      '[data-ta-action=\"cherry-pick\"]:not([style*=\"display: none\"]) .dropmask'\n    );\n    await cherrypickPostResponed;\n    const cherrypickGitlogResponse = environment.setApiListener('/gitlog', 'GET', (body) => {\n      return _.isEqual(\n        body.nodes.map((node) => node.message),\n        ['commit-2', 'commit-3', 'commit-2', 'commit-1']\n      );\n    });\n    await environment.ensureRedraw();\n    await cherrypickGitlogResponse;\n    await environment.waitForElementVisible('[data-ta-node-title=\"commit-2\"] .ref.branch.current');\n  });\n\n  it('test backward squash from own lineage', async () => {\n    await environment.wait(1000);\n    await environment.waitForBranch('branch-1');\n    await environment.clickOnNode('.ref.branch.current');\n    await environment.click('[data-ta-node-title=\"commit-1\"] .squash .dropmask');\n    await environment.waitForElementVisible('.staging .files .file');\n    await environment.click('.files button.discard');\n    await environment.awaitAndClick('.modal-dialog .btn-primary', 2000);\n    await environment.ensureRedraw();\n    await environment.waitForElementHidden('.staging .files .file');\n  });\n\n  it('test forward squash from different lineage', async () => {\n    await environment.clickOnNode('.ref.branch.current');\n    await environment.click('[data-ta-node-title=\"commit-3\"] .squash .dropmask');\n    await environment.ensureRedraw();\n    await environment.waitForElementVisible('.staging .files .file');\n  });\n\n  it('Auto checkout on branch creation.', async () => {\n    await environment.page.evaluate(() => (ungit.config.autoCheckoutOnBranchCreate = true));\n    await environment.createBranch('autoCheckout');\n    await environment.waitForElementVisible('[data-ta-name=\"autoCheckout\"].current');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.commands.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\nconst _ = require('lodash');\n\nconst gitCommand = (options) => {\n  return environment.backgroundAction('POST', '/api/testing/git', options);\n};\nconst testForBranchMove = async (branch, command) => {\n  const branchTagLoc = await environment.page.$eval(branch, (element) =>\n    JSON.stringify(element.getBoundingClientRect())\n  );\n\n  await gitCommand({ command: command, path: testRepoPaths[0] });\n\n  await environment.page.waitForFunction(\n    (branch, oldLoc) => {\n      const newLoc = document.querySelector(branch).getBoundingClientRect();\n      return newLoc.top !== oldLoc.top || newLoc.left !== oldLoc.left;\n    },\n    { timeout: 6000, polling: 250 },\n    branch,\n    JSON.parse(branchTagLoc)\n  );\n};\n\ndescribe('[COMMANDS]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[0]);\n  });\n\n  it('add a branch-1', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.commit('commit-1');\n    await environment.createBranch('branch-1');\n  });\n\n  it('add a branch-2', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.commit('commit-1');\n    await environment.createBranch('branch-2');\n  });\n\n  it('test branch create from command line', async () => {\n    await gitCommand({ command: ['branch', 'gitCommandBranch'], path: testRepoPaths[0] });\n    await environment.waitForElementVisible('[data-ta-name=\"gitCommandBranch\"]');\n  });\n\n  it('test branch move from command line', () => {\n    return testForBranchMove('[data-ta-name=\"gitCommandBranch\"]', [\n      'branch',\n      '-f',\n      'gitCommandBranch',\n      'branch-1',\n    ]);\n  });\n\n  it('test branch delete from command line', async () => {\n    const brachesResponseProm = environment.setApiListener('/branches?', 'GET', (body) => {\n      return _.isEqual(body, [\n        { name: 'branch-1' },\n        { name: 'branch-2' },\n        { name: 'master', current: true },\n      ]);\n    });\n    await gitCommand({ command: ['branch', '-D', 'gitCommandBranch'], path: testRepoPaths[0] });\n    await brachesResponseProm;\n    await environment.waitForElementHidden('[data-ta-name=\"gitCommandBranch\"]', 10000);\n  });\n\n  it('test tag create from command line', async () => {\n    const refsResponseProm = environment.setApiListener('/refs?', 'GET', (body) => {\n      body.forEach((ref) => delete ref.sha1);\n      return _.isEqual(body, [\n        { name: 'refs/heads/branch-1' },\n        { name: 'refs/heads/branch-2' },\n        { name: 'refs/heads/master' },\n        { name: 'refs/tags/tag1' },\n      ]);\n    });\n    await gitCommand({ command: ['tag', 'tag1'], path: testRepoPaths[0] });\n    await refsResponseProm;\n    await environment.waitForElementVisible('[data-ta-name=\"tag1\"]', 10000);\n  });\n\n  it('test tag delete from command line', async () => {\n    const refDeleteResponseProm = environment.setApiListener('/refs?', 'GET', (body) => {\n      body.forEach((ref) => delete ref.sha1);\n      return _.isEqual(body, [\n        { name: 'refs/heads/branch-1' },\n        { name: 'refs/heads/branch-2' },\n        { name: 'refs/heads/master' },\n      ]);\n    });\n    await gitCommand({ command: ['tag', '-d', 'tag1'], path: testRepoPaths[0] });\n    await refDeleteResponseProm;\n    await environment.waitForElementHidden('[data-ta-name=\"tag1\"]', 10000);\n  });\n\n  it('test reset from command line', () => {\n    return testForBranchMove('[data-ta-name=\"branch-1\"]', ['reset', 'branch-1']);\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.discard.js",
    "content": "'use strict';\n\nconst muteGraceTimeDuration = 5000;\nconst createAndDiscard = async (env, testRepoPath, dialogButtonToClick) => {\n  await env.createTestFile(testRepoPath + '/testfile2.txt', testRepoPath);\n  await env.ensureRedraw();\n  await env.waitForElementVisible('.files .file .btn-default');\n\n  await env.click('.files button.discard');\n  if (dialogButtonToClick === 'yes') {\n    await env.awaitAndClick('.modal-dialog [data-ta-action=\"yes\"]');\n  } else if (dialogButtonToClick === 'mute') {\n    await env.awaitAndClick('.modal-dialog [data-ta-action=\"mute\"]');\n  } else if (dialogButtonToClick === 'no') {\n    await env.awaitAndClick('.modal-dialog [data-ta-action=\"no\"]');\n  } else {\n    await env.waitForElementHidden('.modal-dialog [data-ta-action=\"yes\"]');\n  }\n  if (dialogButtonToClick !== 'no') {\n    await env.ensureRedraw();\n    await env.waitForElementHidden('.files .file .btn-default');\n  } else {\n    await env.waitForElementVisible('.files .file .btn-default');\n  }\n};\n\ndescribe('[DISCARD - noWarn]', () => {\n  const environment = require('./environment')({\n    serverStartupOptions: ['--disableDiscardWarning'],\n  });\n  const testRepoPaths = [];\n\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[0]);\n  });\n\n  it('Should be possible to discard a created file without warning message', () => {\n    return createAndDiscard(environment, testRepoPaths[0]);\n  });\n});\n\ndescribe('[DISCARD - withWarn]', () => {\n  const environment = require('./environment')({\n    serverStartupOptions: [\n      '--no-disableDiscardWarning',\n      '--disableDiscardMuteTime=' + muteGraceTimeDuration,\n    ],\n  });\n  const testRepoPaths = [];\n\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[0]);\n  });\n\n  it('Should be possible to select no from discard', () => {\n    return createAndDiscard(environment, testRepoPaths[0], 'no');\n  });\n\n  it('Should be possible to discard a created file', () => {\n    return createAndDiscard(environment, testRepoPaths[0], 'yes');\n  });\n\n  it('Should be possible to discard a created file and disable warn for awhile', async () => {\n    await createAndDiscard(environment, testRepoPaths[0], 'mute');\n    const start = new Date().getTime(); // this is when the \"mute\" timestamp is stamped\n    await createAndDiscard(environment, testRepoPaths[0]);\n    // ensure, at least 2 seconds has passed since mute timestamp is stamped\n    const end = new Date().getTime();\n    const diff = muteGraceTimeDuration + 500 - (end - start);\n    if (diff > 0) {\n      await environment.wait(diff);\n    }\n    await createAndDiscard(environment, testRepoPaths[0], 'yes');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.generic.js",
    "content": "'use strict';\nconst environment = require('./environment')({\n  serverStartupOptions: ['--no-disableDiscardWarning'],\n  rootPath: '/deep/root/path/to/app',\n});\nconst mkdirp = require('mkdirp').mkdirp;\nconst rimraf = require('rimraf').rimraf;\nconst testRepoPaths = [];\n\nconst changeTestFile = async (filename, repoPath) => {\n  await environment.backgroundAction('POST', '/api/testing/changefile', {\n    file: filename,\n    path: repoPath,\n  });\n  await environment.ensureRedraw();\n};\nconst amendCommit = async () => {\n  try {\n    await environment.page.waitForSelector('.amend-button', { visible: true, timeout: 2000 });\n    await environment.click('.amend-button');\n  } catch {\n    await environment.click('.amend-link');\n  }\n  await environment.ensureRedraw();\n  await environment.click('.commit-btn');\n  await environment.ensureRedraw();\n  await environment.waitForElementHidden('.files .file .btn-default');\n};\n\ndescribe('[GENERIC]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false }]);\n\n    // create a sub dir and change working dir to sub dir to prove functionality within subdir\n    testRepoPaths.push(`${testRepoPaths[0]}/asubdir`);\n    await rimraf(testRepoPaths[1]);\n    await mkdirp(testRepoPaths[1]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open repo screen', () => {\n    return environment.openUngit(testRepoPaths[1]);\n  });\n\n  it('Check for refresh button', () => {\n    return environment.click('.refresh-button');\n  });\n\n  it('Should be possible to create and commit a file', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.commit('Init');\n    await environment.waitForElementVisible('.commit');\n  });\n\n  it('Should be possible to amend a file', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.waitForElementVisible('.files .file .btn-default');\n    await amendCommit();\n    await environment.waitForElementVisible('.commit');\n  });\n\n  it('Should be possible to cancel amend a file', async () => {\n    await environment.click('.amend-link');\n    await environment.click('.btn-stg-cancel');\n    await environment.waitForElementVisible('.empty-commit-link');\n  });\n\n  it('Should be able to add a new file to .gitignore', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/addMeToIgnore.txt`, testRepoPaths[0]);\n    await environment.waitForElementVisible('.files .file .btn-default');\n    await environment.page.waitForFunction(\n      'document.querySelectorAll(\".files .file .btn-default\").length === 1',\n      { polling: 250 }\n    );\n    await environment.click('.files button.ignore');\n    await environment.page.waitForFunction(\n      'document.querySelector(\".name.btn.btn-default\").innerText.trim() === \".gitignore\"',\n      { polling: 250 }\n    );\n    await environment.click('.files button.ignore');\n    await environment.waitForElementHidden('.files .file .btn-default');\n  });\n\n  it('Test showing commit diff between two commits', async () => {\n    await environment.clickOnNode('[data-ta-clickable=\"node-clickable-0\"]');\n    await environment.waitForElementVisible('.diff-wrapper');\n    await environment.click('.commit-diff-filename');\n    await environment.waitForElementVisible('.commit-line-diffs');\n  });\n\n  it('Test showing commit side by side diff between two commits', async () => {\n    await environment.click('.commit-sideBySideDiff');\n    await environment.waitForElementVisible('.commit-line-diffs');\n  });\n\n  it('Test wordwrap', async () => {\n    await environment.click('.commit-wordwrap');\n    await environment.waitForElementVisible('.word-wrap');\n  });\n\n  it('Test whitespace', async () => {\n    await environment.click('.commit-whitespace');\n    await environment.click('[data-ta-clickable=\"node-clickable-0\"]');\n  });\n\n  it('Should be possible to discard a created file and ensure patching is not available for new file', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);\n    await environment.waitForElementVisible('.files .file .btn-default');\n    await environment.click('.files button');\n    await environment.waitForElementHidden('[data-ta-container=\"patch-file\"]');\n    await environment.click('.files button.discard');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await environment.waitForElementHidden('.files .file .btn-default');\n  });\n\n  it('Should be possible to create a branch', async () => {\n    await environment.createBranch('testbranch');\n  });\n\n  it('Should be possible to create and destroy a branch', async () => {\n    await environment.createBranch('willbedeleted');\n    await environment.clickOnNode('.branch[data-ta-name=\"willbedeleted\"]');\n    await environment.click('[data-ta-action=\"delete\"]:not([style*=\"display: none\"]) .dropmask');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await environment.ensureRedraw();\n    await environment.waitForElementHidden('.branch[data-ta-name=\"willbedeleted\"]');\n  });\n\n  it('Should be possible to create and destroy a tag', async () => {\n    await environment.createTag('tagwillbedeleted');\n    await environment.clickOnNode('.graph .ref.tag[data-ta-name=\"tagwillbedeleted\"]');\n    await environment.click('[data-ta-action=\"delete\"]:not([style*=\"display: none\"]) .dropmask');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await environment.ensureRedraw();\n    await environment.waitForElementHidden('.graph .ref.tag[data-ta-name=\"tagwillbedeleted\"]');\n  });\n\n  it('Commit changes to a file', async () => {\n    await changeTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.waitForElementVisible('.files .file .btn-default');\n    await environment.insert('.staging input.form-control', 'My commit message');\n    await environment.click('.commit-btn');\n    await environment.ensureRedraw();\n    await environment.waitForElementHidden('.files .file .btn-default');\n  });\n\n  it('Show stats for changed file and discard it', async () => {\n    await changeTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.waitForElementVisible('.files .file .additions');\n    await environment.waitForElementVisible('.files .file .deletions');\n    await environment.click('.files button.discard');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await environment.ensureRedraw();\n    await environment.waitForElementHidden('.files .file .btn-default');\n  });\n\n  it.skip('Should be possible to patch a file', async () => {\n    await changeTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    //   .patch('patch')\n    environment.waitForElementVisible('.commit');\n  });\n\n  it('Checkout testbranch with action', async () => {\n    await environment.clickOnNode('.branch[data-ta-name=\"testbranch\"]');\n    await environment.click('[data-ta-action=\"checkout\"]:not([style*=\"display: none\"]) .dropmask');\n    await environment.ensureRedraw();\n    await environment.waitForElementVisible('.ref.branch[data-ta-name=\"testbranch\"].current');\n  });\n\n  it('Create another commit', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testy2.txt`, testRepoPaths[0]);\n    await environment.commit('Branch commit');\n    await environment.ensureRedraw();\n  });\n\n  it('Rebase', async () => {\n    await environment.rebaseRefAction('testbranch', true);\n  });\n\n  it('Checkout master with double click', async () => {\n    await environment.click('.branch[data-ta-name=\"master\"]', 2);\n    await environment.waitForElementVisible('.ref.branch[data-ta-name=\"master\"].current');\n  });\n\n  it('Create yet another commit', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testy3.txt`, testRepoPaths[0]);\n    await environment.commit('Branch commit');\n    await environment.ensureRedraw();\n  });\n\n  it('Merge', async () => {\n    await environment.mergeRefAction('testbranch', true);\n  });\n\n  it('Revert merge', async () => {\n    await environment.clickOnNode('[data-ta-clickable=\"node-clickable-0\"]');\n    await environment.click('[data-ta-action=\"revert\"]');\n    await environment.ensureRedraw();\n    await environment.waitForElementVisible(\n      '[data-ta-node-title^=\"Revert \\\\\"Merge branch \\'testbranch\\'\"] .commit-container'\n    );\n  });\n\n  it('Should be possible to move a branch', async () => {\n    await environment.createBranch('movebranch');\n    await environment.moveRef('movebranch', 'Init');\n  });\n\n  it('Should be possible to cancel creation of an empty commit', async () => {\n    await environment.click('.empty-commit-link');\n    await environment.click('.btn-stg-cancel');\n    await environment.waitForElementVisible('.empty-commit-link');\n  });\n\n  it('Should be possible to create an empty commit', async () => {\n    await environment.click('.empty-commit-link');\n    await environment.click('.commit-btn');\n    await environment.waitForElementVisible('.commit');\n  });\n\n  it('Should be possible to amend an empty commit', async () => {\n    await environment.click('.empty-commit-link');\n    await environment.click('.commit-btn');\n    await environment.waitForElementVisible('.commit');\n    await amendCommit();\n    await environment.waitForElementVisible('.commit');\n  });\n\n  it('Should be possible to cancel amend of an empty commit', async () => {\n    await environment.click('.amend-link');\n    await environment.click('.btn-stg-cancel');\n    await environment.waitForElementVisible('.empty-commit-link');\n  });\n\n  it('Should be possible to click refresh button', () => {\n    return environment.click('button.refresh-button');\n  });\n\n  it('Go to home screen', async () => {\n    await environment.click('.navbar .backlink');\n    await environment.waitForElementVisible('.home');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.load-ahead.js",
    "content": "'use strict';\nconst environment = require('./environment')({\n  serverStartupOptions: ['--numberOfNodesPerLoad=1'],\n});\nconst testRepoPaths = [];\n\ndescribe('[LOAD-AHEAD]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[0]);\n  });\n\n  it('Should be possible to create and commit 1', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.commit('commit-1');\n    await environment.createBranch('branch-1');\n  });\n\n  it('Should be possible to create and commit 2', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);\n    await environment.commit('commit-2');\n  });\n\n  it('Should be possible to create and commit 3', async () => {\n    await environment.click('.branch .dropdown-toggle');\n    await environment.click('[data-ta-clickable=\"checkoutrefs/heads/branch-1\"]');\n    await environment.waitForElementVisible('[data-ta-name=\"branch-1\"].current');\n  });\n\n  it('Create a branch during collapsed mode', () => {\n    return environment.createBranch('new-branch');\n  });\n\n  it('Load ahead', async () => {\n    await environment.click('.load-ahead-button');\n    await environment.waitForElementVisible('[data-ta-clickable=\"node-clickable-1\"]');\n    await environment.waitForElementHidden('.loadAhead');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.no-header.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[NO-HEADER]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', async () => {\n    await environment.openUngit(testRepoPaths[0]);\n    await environment.waitForElementVisible('.repository-view');\n    await environment.waitForElementHidden('[data-ta-container=\"remote-error-popup\"]');\n  });\n\n  it('Check for refresh button', async () => {\n    await environment.click('.refresh-button');\n    await environment.waitForElementHidden('[data-ta-container=\"remote-error-popup\"]');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.remotes.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst mkdirp = require('mkdirp').mkdirp;\nconst rimraf = require('rimraf').rimraf;\nconst { encodePath } = require('../source/address-parser');\nconst testRepoPaths = [];\n\ndescribe('[REMOTES]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: true }, { bare: false, initCommits: 2 }]);\n\n    testRepoPaths.push(`${testRepoPaths[1]}-cloned`); // A directory to test cloning\n    await rimraf(testRepoPaths[2]); // clean clone test dir\n    await mkdirp(testRepoPaths[2]); // create clone test dir\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[1]);\n  });\n\n  it('Should not be possible to push without remote', async () => {\n    await environment.click('.branch[data-ta-name=\"master\"][data-ta-local=\"true\"]');\n    await environment.ensureRedraw();\n    await environment.waitForElementHidden('[data-ta-action=\"push\"]:not([style*=\"display: none\"])');\n  });\n\n  it('Should not be possible to commit & push without remote', async () => {\n    await environment.click('.amend-link');\n    await environment.click('.commit-grp .dropdown-toggle');\n    await environment.ensureRedraw();\n    await environment.waitForElementVisible('.commitnpush.disabled');\n  });\n\n  it('Adding a remote', async () => {\n    await environment.click('.fetchButton .dropdown-toggle');\n    await environment.click('.add-new-remote');\n\n    await environment.insert('.modal #Name', 'myremote');\n    await environment.insert('.modal #Url', testRepoPaths[0]);\n    await environment.awaitAndClick('.modal .modal-footer .btn-primary');\n    await environment.ensureRedraw();\n    await environment.click('.fetchButton .dropdown-toggle');\n    await environment.waitForElementVisible(\n      '.fetchButton .dropdown-menu [data-ta-clickable=\"myremote\"]'\n    );\n  });\n\n  it('Fetch from newly added remote', async () => {\n    const remoteGetResponseProm = environment.setApiListener('/remote/tags?', 'GET');\n    await environment.click('.fetchButton .btn-main');\n    await remoteGetResponseProm;\n  });\n\n  it('Remote delete check', async () => {\n    await environment.click('.fetchButton .dropdown-toggle');\n    await environment.click('[data-ta-clickable=\"myremote-remove\"]');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await environment.ensureRedraw();\n    await environment.click('.fetchButton .dropdown-toggle');\n    await environment.waitForElementHidden('[data-ta-clickable=\"myremote\"]');\n  });\n\n  // ----------- CLONING -------------\n  it('navigate to empty folder path', async () => {\n    await environment.goto(\n      `${environment.getRootUrl()}/#/repository?path=${encodePath(testRepoPaths[2])}`\n    );\n    await environment.waitForElementVisible('.uninited');\n  });\n\n  it('Clone repository should bring you to repo page', async () => {\n    await environment.insert('#cloneFromInput', testRepoPaths[1]);\n    await environment.insert('#cloneToInput', testRepoPaths[2]);\n    await environment.click('.uninited button[type=\"submit\"]');\n    await environment.waitForElementVisible('.repository-view');\n    await environment.wait(1000); // ensure click bindings are initialized\n  });\n\n  it('Should be possible to fetch', async () => {\n    const remoteGetResponseProm = environment.setApiListener('/remote/tags?', 'GET');\n    await environment.click('.fetchButton .btn-main');\n    await remoteGetResponseProm;\n  });\n\n  it('Should be possible to create and push a branch', async () => {\n    await environment.createBranch('branchinclone');\n    await environment.pushRefAction('branchinclone', true);\n    await environment.waitForElementVisible('[data-ta-name=\"origin/branchinclone\"]');\n  });\n\n  it('Should be possible to force push a branch', async () => {\n    await environment.moveRef('branchinclone', 'Init Commit 0');\n    await environment.pushRefAction('branchinclone', true);\n    await environment.waitForElementHidden('[data-ta-action=\"push\"]:not([style*=\"display: none\"])');\n  });\n\n  it('Check for fetching remote branches for the branch list', async () => {\n    await environment.click('.branch .dropdown-toggle');\n    await environment.click('.options input');\n\n    await environment.ensureRedraw();\n\n    await environment.click('.options input');\n    await environment.waitForElementVisible('li .octicon-globe');\n  });\n\n  it('checkout remote branches with matching local branch at wrong place', async () => {\n    await environment.moveRef('branchinclone', 'Init Commit 1');\n    await environment.click('.branch .dropdown-toggle');\n    await environment.click('[data-ta-clickable=\"checkoutrefs/remotes/origin/branchinclone\"]');\n    await environment.ensureRedraw();\n    await environment.waitForElementVisible('[data-ta-name=\"branchinclone\"][data-ta-local=\"true\"]');\n  });\n\n  it('Should be possible to commitnpush', async () => {\n    await environment.createTestFile(`${testRepoPaths[2]}/commitnpush.txt`, testRepoPaths[2]);\n    await environment.waitForElementVisible('.files .file .btn-default');\n    await environment.insert('.staging input.form-control', 'Commit & Push');\n    await environment.wait(250);\n    await environment.click('.commit-grp .dropdown-toggle');\n    await environment.click('.commitnpush');\n    await environment.waitForElementVisible(\n      '[data-ta-node-title=\"Commit & Push\"] .commit-container'\n    );\n  });\n\n  it('Should be possible to commitnpush with ff', async () => {\n    await environment.click('.amend-link');\n    await environment.insert('.staging input.form-control', 'Commit & Push with ff');\n    await environment.click('.commit-grp .dropdown-toggle');\n    await environment.click('.commitnpush');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await environment.waitForElementVisible(\n      '[data-ta-node-title=\"Commit & Push with ff\"] .commit-container'\n    );\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.screens.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst mkdirp = require('mkdirp').mkdirp;\nconst rimraf = require('rimraf').rimraf;\nconst { encodePath } = require('../source/address-parser');\nconst testRepoPaths = [];\n\ndescribe('[SCREENS]', () => {\n  before('Environment init', () => environment.init());\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open home screen', async () => {\n    await environment.goto(environment.getRootUrl());\n    await environment.waitForElementVisible('.home');\n  });\n\n  it('Open path screen', async () => {\n    testRepoPaths.push(await environment.createTempFolder());\n\n    await environment.goto(\n      `${environment.getRootUrl()}/#/repository?path=${encodePath(testRepoPaths[0])}`\n    );\n    await environment.waitForElementVisible('.uninited');\n  });\n\n  it('Init repository should bring you to repo page', async () => {\n    await environment.click('.uninited button.btn-primary');\n    await environment.waitForElementVisible('.repository-view');\n  });\n\n  it('Clicking logo should bring you to home screen', async () => {\n    await environment.click('.navbar .backlink');\n    await environment.waitForElementVisible('.home');\n    await environment.wait(1000);\n  });\n\n  it('Entering an invalid path and create directory in that location', async () => {\n    await environment.insert(\n      '.navbar .path-input-form input',\n      `${testRepoPaths[0]}-test0/not/existing`\n    );\n    await environment.press('Enter');\n    await environment.waitForElementVisible('.invalid-path');\n    await environment.click('.invalid-path button');\n    await environment.waitForElementVisible('.uninited button.btn-primary');\n    await environment.wait(1000);\n  });\n\n  it('Entering an invalid path should bring you to an error screen', async () => {\n    await environment.insert('.navbar .path-input-form input', '/a/path/that/doesnt/exist');\n    await environment.press('Enter');\n    await environment.waitForElementVisible('.invalid-path');\n    await environment.wait(1000);\n  });\n\n  it('Entering a path to a repo should bring you to that repo', async () => {\n    await environment.insert('.navbar .path-input-form input', testRepoPaths[0]);\n    await environment.press('Enter');\n    await environment.waitForElementVisible('.repository-view');\n    await environment.wait(1000);\n  });\n\n  // getting odd cross-domain-error.\n  it('Create test directory with ampersand and open it', async () => {\n    const specialRepoPath = `${testRepoPaths[0]}-test1/test & repo`;\n\n    await rimraf(specialRepoPath);\n    await mkdirp(specialRepoPath);\n\n    await environment.goto(\n      `${environment.getRootUrl()}/#/repository?path=${encodePath(specialRepoPath)}`\n    );\n\n    await environment.waitForElementVisible('.uninited');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.stash.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[STASH]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [{ bare: false, initCommits: 1 }]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[0]);\n  });\n\n  it('Should be possible to stash a file', async () => {\n    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);\n    await environment.waitForElementVisible('.files .file .btn-default');\n    await environment.click('.stash-all');\n    await environment.click('.stash-toggle');\n    await environment.waitForElementVisible('.stash .list-group-item');\n  });\n\n  it('Should be possible to open stash diff', async () => {\n    await environment.click('.toggle-show-commit-diffs');\n    await environment.waitForElementVisible('.stash .diff-wrapper');\n  });\n\n  it('Should be possible to pop a stash', async () => {\n    await environment.click('.stash .stash-apply');\n    await environment.waitForElementVisible('.files .file .btn-default');\n  });\n});\n"
  },
  {
    "path": "clicktests/spec.submodules.js",
    "content": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[SUMBODULES]', () => {\n  before('Environment init', async () => {\n    await environment.init();\n    await environment.createRepos(testRepoPaths, [\n      { bare: false, initCommits: 1 },\n      { bare: false },\n    ]);\n  });\n\n  after('Environment stop', () => environment.shutdown());\n\n  it('Open path screen', () => {\n    return environment.openUngit(testRepoPaths[1]);\n  });\n\n  it('Submodule add', async () => {\n    await environment.click('.submodule .dropdown-toggle');\n    await environment.click('.fetchButton .add-submodule');\n\n    await environment.insert('.modal #Path', 'subrepo');\n    await environment.insert('.modal #Url', testRepoPaths[0]);\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await environment.ensureRedraw();\n  });\n\n  it('Submodule update', async () => {\n    await environment.click('.submodule .dropdown-toggle');\n    await environment.waitForElementVisible(\n      '.fetchButton .dropdown-menu [data-ta-clickable=\"subrepo\"]'\n    );\n    const submoduleResponseProm = environment.setApiListener('/submodules/update', 'POST');\n    await environment.awaitAndClick('.fetchButton .update-submodule');\n    await submoduleResponseProm;\n  });\n\n  it('Submodule delete check', async () => {\n    const submoduleDeleteResponseProm = environment.setApiListener('/submodules?', 'DELETE');\n    await environment.click('.submodule .dropdown-toggle');\n    await environment.click('[data-ta-clickable=\"subrepo-remove\"]');\n    await environment.awaitAndClick('.modal-dialog .btn-primary');\n    await submoduleDeleteResponseProm;\n  });\n});\n"
  },
  {
    "path": "components/ComponentRoot.ts",
    "content": "declare var ungit: any;\n\nexport class ComponentRoot {\n  _apiCache: string;\n  defaultDebounceOption = {\n    maxWait: 1500,\n    leading: false,\n    trailing: true\n  }\n\n  constructor() { }\n\n  isSamePayload(value: any) {\n    const jsonString = JSON.stringify(value);\n\n    if (this._apiCache === jsonString) {\n      ungit.logger.debug(`ignoring redraw for same ${this.constructor.name} payload.`);\n      return true;\n    }\n    ungit.logger.debug(`redrawing ${this.constructor.name} payload.  \\n${jsonString}`);\n\n    this._apiCache = jsonString\n    return false;\n  }\n\n  clearApiCache() {\n    this._apiCache = undefined\n  }\n}\n"
  },
  {
    "path": "components/app/app.html",
    "content": "<!-- ko component: header -->\n<!-- /ko -->\n\n<div class=\"app\">\n  <div class=\"container\" data-bind=\"shown: shown\">\n    <div class=\"alert alert-danger\" data-bind=\"visible: gitVersionErrorVisible\">\n      <span data-bind=\"text: gitVersionError\"></span>\n      <button type=\"button\" class=\"close\" data-bind=\"click: dismissGitVersionError\">&times;</button>\n    </div>\n\n    <div class=\"alert alert-info\" data-bind=\"visible: showNewVersionAvailable\">\n      A new version of ungit (<span data-bind=\"text: latestVersion\"></span>) is\n      <a href=\"https://github.com/FredrikNoren/ungit\">available</a>! Run\n      <code data-bind=\"text: newVersionInstallCommand\"></code> to install. See what's new in the\n      <a href=\"https://github.com/FredrikNoren/ungit/blob/master/CHANGELOG.md\">changelog</a>.\n      <button type=\"button\" class=\"close\" data-bind=\"click: dismissNewVersion\">&times;</button>\n    </div>\n\n    <div class=\"alert alert-info clearfix\" data-bind=\"visible: showBugtrackingNagscreen\">\n      <button type=\"button\" class=\"close\" data-bind=\"click: dismissBugtrackingNagscreen\">\n        &times;\n      </button>\n      <p><strong>Help make ungit better with the press of a button!</strong></p>\n\n      <button class=\"btn btn-primary\" data-bind=\"click: enableBugtracking\">\n        Enable automatic bug reports\n      </button>\n      <button class=\"btn btn-default\" data-bind=\"click: dismissBugtrackingNagscreen\">\n        Naah, I&#39;ll skip that\n      </button>\n    </div>\n  </div>\n\n  <!-- ko if: content -->\n  <div class=\"container-fluid\" data-bind=\"component: content\"></div>\n  <!-- /ko -->\n</div>\n\n<!-- ko if: modal -->\n<!-- ko template: { name: templateChooser, data: modal } -->\n<!-- /ko -->\n<!-- /ko -->"
  },
  {
    "path": "components/app/app.js",
    "content": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst storage = require('ungit-storage');\nconst $ = require('jquery');\n\ncomponents.register('app', (args) => {\n  return new AppViewModel(args.appContainer, args.server);\n});\n\nclass AppViewModel {\n  constructor(appContainer, server) {\n    this.appContainer = appContainer;\n    this.server = server;\n    this.template = 'app';\n    if (window.location.search.indexOf('noheader=true') < 0) {\n      this.header = components.create('header', { app: this });\n    }\n    this.modal = ko.observable(null);\n    this.repoList = ko.observableArray(this.getRepoList()); // visitedRepositories is legacy, remove in the next version\n    this.repoList.subscribe((newValue) => {\n      storage.setItem('repositories', JSON.stringify(newValue));\n    });\n    this.content = ko.observable(components.create('home', { app: this }));\n    this.currentVersion = ko.observable();\n    this.latestVersion = ko.observable();\n    this.showNewVersionAvailable = ko.observable();\n    this.newVersionInstallCommand =\n      (ungit.platform == 'win32' ? '' : 'sudo -H ') + 'npm update -g ungit';\n    this.bugtrackingEnabled = ko.observable(ungit.config.bugtracking);\n    this.bugtrackingNagscreenDismissed = ko.observable(\n      storage.getItem('bugtrackingNagscreenDismissed')\n    );\n    this.showBugtrackingNagscreen = ko.computed(() => {\n      return !this.bugtrackingEnabled() && !this.bugtrackingNagscreenDismissed();\n    });\n    this.gitVersionErrorDismissed = ko.observable(storage.getItem('gitVersionErrorDismissed'));\n    this.gitVersionError = ko.observable();\n    this.gitVersionErrorVisible = ko.computed(() => {\n      return (\n        !ungit.config.gitVersionCheckOverride &&\n        this.gitVersionError() &&\n        !this.gitVersionErrorDismissed()\n      );\n    });\n  }\n  getRepoList() {\n    const localStorageRepo = JSON.parse(\n      storage.getItem('repositories') || storage.getItem('visitedRepositories') || '[]'\n    );\n    const newRepos = localStorageRepo\n      .concat(ungit.config.defaultRepositories || [])\n      .filter((v, i, a) => a.indexOf(v) === i)\n      .sort();\n    storage.setItem('repositories', JSON.stringify(newRepos));\n    return newRepos;\n  }\n  updateNode(parentElement) {\n    ko.renderTemplate('app', this, {}, parentElement);\n  }\n  shown() {\n    // The ungit.config constiable collections configuration from all different paths and only updates when\n    // ungit is restarted\n    if (!ungit.config.bugtracking) {\n      // Whereas the userconfig only reflects what's in the ~/.ungitrc and updates directly,\n      // but is only used for changing around the configuration. We need to check this here\n      // since ungit may have crashed without the server crashing since we enabled bugtracking,\n      // and we don't want to show the nagscreen twice in that case.\n      this.server\n        .getPromise('/userconfig')\n        .then((userConfig) => this.bugtrackingEnabled(userConfig.bugtracking))\n        .catch((e) => this.server.unhandledRejection(e));\n    }\n\n    this.server\n      .getPromise('/latestversion')\n      .then((version) => {\n        if (!version) return;\n        this.currentVersion(version.currentVersion);\n        this.latestVersion(version.latestVersion);\n        this.showNewVersionAvailable(!ungit.config.ungitVersionCheckOverride && version.outdated);\n      })\n      .catch((e) => this.server.unhandledRejection(e));\n    this.server\n      .getPromise('/gitversion')\n      .then((gitversion) => {\n        if (gitversion && !gitversion.satisfied) {\n          this.gitVersionError(gitversion.error);\n        }\n      })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n  updateAnimationFrame(deltaT) {\n    if (this.content() && this.content().updateAnimationFrame)\n      this.content().updateAnimationFrame(deltaT);\n  }\n  onProgramEvent(event) {\n    if (event.event === 'request-credentials') {\n      this._handleCredentialsRequested(event);\n    } else if (event.event === 'request-remember-repo') {\n      this._handleRequestRememberRepo(event);\n    } else if (event.event === 'modal-show-dialog') {\n      this.showModal(event.modal);\n    } else if (event.event === 'modal-close-dialog') {\n      $('.modal.fade').modal('hide');\n      this.modal(undefined);\n    }\n\n    if (this.content() && this.content().onProgramEvent) {\n      this.content().onProgramEvent(event);\n    }\n    if (this.header && this.header.onProgramEvent) {\n      this.header.onProgramEvent(event);\n    }\n  }\n  _handleRequestRememberRepo(event) {\n    const repoPath = event.repoPath;\n    if (this.repoList.indexOf(repoPath) != -1) return;\n    this.repoList.push(repoPath);\n  }\n  _handleCredentialsRequested(event) {\n    // Only show one credentials dialog if we're asked to show another one while the first one is open\n    // This happens for instance when we fetch nodes and remote tags at the same time\n    if (!this._isShowingCredentialsDialog) {\n      this._isShowingCredentialsDialog = true;\n      components.showModal('credentialsmodal', { remote: event.remote });\n    }\n  }\n  showModal(modal) {\n    this.modal(modal);\n\n    // when dom is ready, open the modal\n    const checkExists = setInterval(() => {\n      const modalDom = $('.modal.fade');\n      if (modalDom.length) {\n        clearInterval(checkExists);\n        modalDom.modal();\n        modalDom.on('hidden.bs.modal', function () {\n          modal.close();\n        });\n      }\n    }, 200);\n  }\n  gitSetUserConfig(bugTracking) {\n    this.server.getPromise('/userconfig').then((userConfig) => {\n      userConfig.bugtracking = bugTracking;\n      return this.server.postPromise('/userconfig', userConfig).then(() => {\n        this.bugtrackingEnabled(bugTracking);\n      });\n    });\n  }\n  enableBugtracking() {\n    this.gitSetUserConfig(true);\n  }\n  dismissBugtrackingNagscreen() {\n    storage.setItem('bugtrackingNagscreenDismissed', true);\n    this.bugtrackingNagscreenDismissed(true);\n  }\n  dismissGitVersionError() {\n    storage.setItem('gitVersionErrorDismissed', true);\n    this.gitVersionErrorDismissed(true);\n  }\n  dismissNewVersion() {\n    this.showNewVersionAvailable(false);\n  }\n  templateChooser(data) {\n    if (!data) return '';\n    return data.template;\n  }\n}\n"
  },
  {
    "path": "components/app/app.less",
    "content": ".app {\n  margin-top: 15px;\n\n  .container-fluid {\n    padding-left: 40px;\n    padding-right: 40px;\n  }\n}\n"
  },
  {
    "path": "components/app/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"app\": \"app.html\"\n    },\n    \"javascript\": \"app.bundle.js\",\n    \"css\": \"app.css\"\n  }\n}\n"
  },
  {
    "path": "components/branches/branches.html",
    "content": "<div class=\"btn-group branch\">\n  <button type=\"button\" class=\"btn btn-default btn-main\" data-bind=\"click: updateRefs\">\n    <span data-bind=\"html: branchIcon\"></span>\n    <span data-bind=\"text: refsLabel\"></span>\n  </button>\n  <button\n    type=\"button\"\n    class=\"btn btn-default dropdown-toggle\"\n    data-toggle=\"dropdown\"\n    aria-haspopup=\"true\"\n    aria-expanded=\"false\"\n  >\n    <span class=\"caret\"></span>\n    <span class=\"sr-only\">Toggle Branch List</span>\n  </button>\n  <ul class=\"dropdown-menu dropdown-menu-right\" role=\"menu\">\n    <li class=\"dropdown-header options\" onclick=\"event.stopPropagation()\">\n      <label>\n        <input\n          class=\"glyphicon\"\n          type=\"checkbox\"\n          data-bind=\"checked: isShowRemote, css: { 'glyphicon-check': isShowRemote, 'glyphicon-unchecked': !isShowRemote() }\"\n        />\n        Remote\n      </label>\n      <label>\n        <input\n          class=\"glyphicon\"\n          type=\"checkbox\"\n          data-bind=\"checked: isShowBranch, css: { 'glyphicon-check': isShowBranch, 'glyphicon-unchecked': !isShowBranch() }\"\n        />\n        Branch\n      </label>\n      <label>\n        <input\n          class=\"glyphicon\"\n          type=\"checkbox\"\n          data-bind=\"checked: isShowTag, css: { 'glyphicon-check': isShowTag, 'glyphicon-unchecked': !isShowTag() }\"\n        />\n        Tag\n      </label>\n    </li>\n    <li class=\"divider\" role=\"separator\"></li>\n    <!-- ko foreach: branchesAndLocalTags -->\n    <li>\n      <a\n        class=\"linked-remove\"\n        href=\"#\"\n        data-bind=\"html: displayHtml(), click: $parent.checkoutBranch.bind($parent), attr: { 'data-ta-clickable': 'checkout' + name }\"\n      ></a>\n      <a\n        class=\"list-link list-remove\"\n        href=\"#\"\n        data-bind=\"html: $parent.closeIcon, click: $parent.branchRemove.bind($parent), attr: { 'data-ta-clickable': name + '-remove' }\"\n      ></a>\n    </li>\n    <!-- /ko -->\n  </ul>\n</div>\n"
  },
  {
    "path": "components/branches/branches.js",
    "content": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst storage = require('ungit-storage');\nconst showRemote = 'showRemote';\nconst showBranch = 'showBranch';\nconst showTag = 'showTag';\nconst { ComponentRoot } = require('../ComponentRoot');\n\ncomponents.register('branches', (args) => {\n  return new BranchesViewModel(args.server, args.graph, args.repoPath);\n});\n\nclass BranchesViewModel extends ComponentRoot {\n  constructor(server, graph, repoPath) {\n    super();\n    this.repoPath = repoPath;\n    this.server = server;\n    this.updateRefs = _.debounce(this._updateRefs, 250, this.defaultDebounceOption);\n    this.branchesAndLocalTags = ko.observableArray();\n    this.current = ko.observable();\n    this.isShowRemote = ko.observable(storage.getItem(showRemote) != 'false');\n    this.isShowBranch = ko.observable(storage.getItem(showBranch) != 'false');\n    this.isShowTag = ko.observable(storage.getItem(showTag) != 'false');\n    this.graph = graph;\n    const setLocalStorageAndUpdate = (localStorageKey, value) => {\n      storage.setItem(localStorageKey, value);\n      this.updateRefs();\n      return value;\n    };\n    this.shouldAutoFetch = ungit.config.autoFetch;\n    this.isShowRemote.subscribe(() => {\n      this.clearApiCache();\n      setLocalStorageAndUpdate(showRemote);\n    });\n    this.isShowBranch.subscribe(() => {\n      this.clearApiCache();\n      setLocalStorageAndUpdate(showBranch);\n    });\n    this.isShowTag.subscribe(() => {\n      this.clearApiCache();\n      setLocalStorageAndUpdate(showTag);\n    });\n    this.refsLabel = ko.computed(() => this.current() || 'master (no commits yet)');\n    this.branchIcon = octicons['git-branch'].toSVG({ height: 18 });\n    this.closeIcon = octicons.x.toSVG({ height: 18 });\n  }\n\n  checkoutBranch(branch) {\n    branch.checkout();\n  }\n  updateNode(parentElement) {\n    ko.renderTemplate('branches', this, {}, parentElement);\n  }\n  clickFetch() {\n    this.updateRefs(true);\n  }\n  onProgramEvent(event) {\n    if (\n      event.event === 'request-app-content-refresh' ||\n      event.event === 'branch-updated' ||\n      event.event === 'git-directory-changed' ||\n      event.event === 'current-remote-changed'\n    ) {\n      this.updateRefs();\n    }\n  }\n  async _updateRefs(forceRemoteFetch) {\n    forceRemoteFetch = forceRemoteFetch || this.shouldAutoFetch || '';\n\n    const branchesProm = this.server.getPromise('/branches', { path: this.repoPath() });\n    const refsProm = this.server.getPromise('/refs', {\n      path: this.repoPath(),\n      remoteFetch: forceRemoteFetch,\n    });\n\n    try {\n      // set current branch\n      (await branchesProm).forEach((b) => {\n        if (b.current) {\n          this.current(b.name);\n        }\n      });\n    } catch (e) {\n      this.current('~error');\n      ungit.logger.warn('error while setting current branch', e);\n    }\n\n    try {\n      // update branches and tags references.\n      const refs = await refsProm;\n      if (this.isSamePayload(refs)) {\n        return;\n      }\n\n      const version = Date.now();\n      const sorted = refs\n        .map((r) => {\n          const ref = this.graph.getRef(r.name.replace('refs/tags', 'tag: refs/tags'));\n          ref.node(this.graph.getNode(r.sha1));\n          ref.version = version;\n          return ref;\n        })\n        .sort((a, b) => {\n          if (a.current() || b.current()) {\n            return a.current() ? -1 : 1;\n          } else if (a.isRemoteBranch === b.isRemoteBranch) {\n            if (a.name < b.name) {\n              return -1;\n            }\n            if (a.name > b.name) {\n              return 1;\n            }\n            return 0;\n          } else {\n            return a.isRemoteBranch ? 1 : -1;\n          }\n        })\n        .filter((ref) => {\n          if (ref.localRefName == 'refs/stash') return false;\n          if (ref.localRefName.endsWith('/HEAD')) return false;\n          if (!this.isShowRemote() && ref.isRemote) return false;\n          if (!this.isShowBranch() && ref.isBranch) return false;\n          if (!this.isShowTag() && ref.isTag) return false;\n          return true;\n        });\n      this.branchesAndLocalTags(sorted);\n      this.graph.refs().forEach((ref) => {\n        // ref was removed from another source\n        if (!ref.isRemoteTag && ref.value !== 'HEAD' && (!ref.version || ref.version < version)) {\n          ref.remove(true);\n        }\n      });\n    } catch (e) {\n      ungit.logger.error('error during branch update: ', e);\n    }\n  }\n\n  branchRemove(branch) {\n    let details = `\"${branch.refName}\"`;\n    if (branch.isRemoteBranch) {\n      details = `<code style='font-size: 100%'>REMOTE</code> ${details}`;\n    }\n    components.showModal('yesnomodal', {\n      title: 'Are you sure?',\n      details: 'Deleting ' + details + ' branch cannot be undone with ungit.',\n      closeFunc: (isYes) => {\n        if (!isYes) return;\n        return branch.remove();\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "components/branches/branches.less",
    "content": "@import 'public/less/variables.less';\n\n.branch {\n  .options {\n    font-size: inherit;\n    color: @gray-dark;\n\n    label {\n      cursor: pointer;\n      font-weight: normal;\n      margin: 0 15px 0 0;\n\n      &:last-child {\n        margin: 0;\n      }\n    }\n\n    input {\n      -moz-appearance: none;\n      -webkit-appearance: none;\n      appearance: none;\n      cursor: pointer;\n    }\n  }\n\n  .dropdown-menu.octicon {\n    width: 18px;\n  }\n}\n"
  },
  {
    "path": "components/branches/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"branches\": \"branches.html\"\n    },\n    \"javascript\": \"branches.bundle.js\",\n    \"css\": \"branches.css\"\n  }\n}\n"
  },
  {
    "path": "components/commit/commit.html",
    "content": "<div\n  class=\"commit\"\n  data-bind=\"css: { highlighted: highlighted, hover: nodeIsMousehover, selected: selected }\"\n>\n  <div\n    class=\"commit-box panel panel-default\"\n    data-bind=\"element: element, click: stopClickPropagation\"\n  >\n    <div class=\"panel-body\">\n      <div class=\"arrow shadow\"></div>\n      <div class=\"arrow\"></div>\n      <div class=\"clearfix\">\n        <img\n          class=\"pull-left img-circle gravatar\"\n          data-bind=\"attr: { src: `https://www.gravatar.com/avatar/${authorGravatar()}?default=404`, alt: `Profile Picture of ${authorName()}` }\"\n          onerror=\"this.style.display='none';\"\n        />\n        <div>\n          <div>\n            <span\n              class=\"title\"\n              data-bind=\"text: (title().length > 72 ? title().substring(0, 72) + '...' : title)\"\n            ></span>\n            <span class=\"text-muted\"\n              >by <a data-bind=\"text: authorName, attr: { href: 'mailto:' + authorEmail() }\"></a\n            ></span>\n            <!-- ko if: pgpVerifiedString() -->\n            <span\n              class=\"text-muted\"\n              data-bind=\"html: pgpIcon, attr: { title: pgpVerifiedString() }\"\n              data-toggle=\"tooltip\"\n            ></span>\n            <!-- /ko -->\n          </div>\n          <div class=\"text-muted nodeSummaryContainer\">\n            <span\n              data-bind=\"text: authorDateFromNow, attr: { title: authorDate }\"\n              data-toggle=\"tooltip\"\n              data-placement=\"bottom\"\n            ></span>\n            | +<span data-bind=\"text: numberOfAddedLines\"></span>, -<span\n              data-bind=\"text: numberOfRemovedLines\"\n            ></span>\n            |\n            <span title=\"Commit\" data-bind=\"text: sha1.substring(0, 8)\"></span>\n            <!-- ko if: navigator.clipboard -->\n            <button class=\"btn btn-default btn-xs\" type=\"button\" data-bind=\"click: copyHash\"><span class=\"glyphicon glyphicon-copy\"></span></button>\n            <!-- /ko -->\n            <!-- ko foreach: parents -->\n            | <a href=\"#\" title=\"Parent Commit\" data-bind=\"text: $data.substring(0, 8), click: $parent.gotoCommit.bind($parent, $data)\"></a>\n            <!-- /ko -->\n          </div>\n        </div>\n      </div>\n      <!-- ko if: selected() || nodeIsMousehover() -->\n      <div class=\"details\">\n        <div\n          class=\"body\"\n          data-bind=\"visible: title().length > 72, text: '...' + title().substring(72)\"\n        ></div>\n        <div class=\"body\" data-bind=\"text: body, visible: body\"></div>\n        <div\n          class=\"diff-wrapper\"\n          data-bind=\"visible: showCommitDiff, style: diffStyle, click: stopClickPropagation\"\n        >\n          <div class=\"diff-inner\" data-bind=\"component: commitDiff\"></div>\n        </div>\n      </div>\n      <!-- /ko -->\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "components/commit/commit.js",
    "content": "const ko = require('knockout');\nconst md5 = require('blueimp-md5');\nconst moment = require('moment');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\n\ncomponents.register('commit', (args) => new CommitViewModel(args));\n\nclass CommitViewModel {\n  constructor(gitNode) {\n    this.graph = gitNode.graph;\n    this.repoPath = gitNode.graph.repoPath;\n    this.sha1 = gitNode.sha1;\n    this.server = gitNode.graph.server;\n    this.highlighted = gitNode.highlighted;\n    this.nodeIsMousehover = gitNode.nodeIsMousehover;\n    this.selected = gitNode.selected;\n    this.pgpVerifiedString = gitNode.pgpVerifiedString;\n    this.pgpIcon = octicons.verified.toSVG({ height: 18 });\n    this.element = ko.observable();\n    this.message = ko.observable();\n    this.title = ko.observable();\n    this.body = ko.observable();\n    this.authorDate = ko.observable();\n    this.authorDateFromNow = ko.observable();\n    this.authorName = ko.observable();\n    this.authorEmail = ko.observable();\n    this.fileLineDiffs = ko.observable();\n    this.numberOfAddedLines = ko.observable();\n    this.numberOfRemovedLines = ko.observable();\n    this.parents = ko.observable();\n    this.authorGravatar = ko.computed(() => md5((this.authorEmail() || '').trim().toLowerCase()));\n\n    this.showCommitDiff = ko.computed(\n      () => this.fileLineDiffs() && this.fileLineDiffs().length > 0\n    );\n\n    this.diffStyle = ko.computed(() => {\n      const marginLeft = Math.min(gitNode.branchOrder() * 70, 450) * -1;\n      if (this.selected() && this.element())\n        return { 'margin-left': `${marginLeft}px`, width: `${window.innerWidth - 220}px` };\n      else return {};\n    });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('commit', this, {}, parentElement);\n  }\n\n  setData(args) {\n    const message = args.message.split('\\n');\n    this.message(args.message);\n    this.title(message[0]);\n    this.body(message.slice(message[1] ? 1 : 2).join('\\n'));\n    this.authorDate(moment(new Date(args.authorDate)));\n    this.authorDateFromNow(this.authorDate().fromNow());\n    this.authorName(args.authorName);\n    this.authorEmail(args.authorEmail);\n    this.numberOfAddedLines(args.additions);\n    this.numberOfRemovedLines(args.deletions);\n    this.parents(args.parents || []);\n    this.fileLineDiffs(args.fileLineDiffs);\n    this.commitDiff = ko.observable(\n      components.create('commitDiff', {\n        fileLineDiffs: this.fileLineDiffs(),\n        sha1: this.sha1,\n        repoPath: this.repoPath,\n        server: this.server,\n        showDiffButtons: this.selected,\n      })\n    );\n  }\n\n  updateLastAuthorDateFromNow(deltaT) {\n    this.lastUpdatedAuthorDateFromNow = this.lastUpdatedAuthorDateFromNow || 0;\n    this.lastUpdatedAuthorDateFromNow += deltaT;\n    if (this.lastUpdatedAuthorDateFromNow > 60 * 1000) {\n      this.lastUpdatedAuthorDateFromNow = 0;\n      this.authorDateFromNow(this.authorDate().fromNow());\n    }\n  }\n\n  updateAnimationFrame(deltaT) {\n    this.updateLastAuthorDateFromNow(deltaT);\n  }\n\n  stopClickPropagation(data, event) {\n    event.stopImmediatePropagation();\n  }\n\n  copyHash() {\n    navigator.clipboard.writeText(this.sha1);\n  }\n\n  gotoCommit(sha1) {\n    const node = this.graph.nodesById[sha1];\n    if (node) {\n      node.toggleSelected();\n    }\n  }\n}\n"
  },
  {
    "path": "components/commit/commit.less",
    "content": "@import 'public/less/variables.less';\n\n.commit {\n  position: relative;\n\n  &.highlighted {\n    z-index: 2;\n\n    .commit-box {\n      box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.2);\n      background: #4b5766;\n      left: -5px;\n\n      .arrow {\n        border-left-color: #4b5766;\n\n        &.shadow {\n          display: block;\n        }\n      }\n    }\n  }\n\n  &.hover {\n    z-index: 3;\n  }\n\n  &.selected {\n    .details {\n      .diff-wrapper {\n        margin-bottom: 5px;\n        transition: width 0.1s, left 0.05s;\n        box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);\n\n        .diff-inner {\n          box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.2);\n          padding: 10px;\n          padding-top: 0;\n        }\n\n        .btn-group {\n          margin-top: 10px;\n        }\n      }\n    }\n  }\n\n  .commit-box {\n    .arrow {\n      right: -30px;\n      top: 27px;\n      border-left-color: @panel-bg;\n\n      &.shadow {\n        display: none;\n        right: -34px;\n        top: 32px;\n        border-left-color: rgba(0, 0, 0, 0.2);\n      }\n    }\n  }\n\n  .commit-box > .panel-body {\n    position: relative;\n    padding: 10px;\n    margin-bottom: 0;\n    width: @log-width-small;\n    min-height: 85px;\n\n    .gravatar {\n      display: none;\n      margin-right: 10px;\n    }\n\n    .title {\n      font-size: 1.3em;\n      word-wrap: break-word;\n      display: block;\n    }\n\n    .details {\n      .body {\n        font-family: 'Source Code Pro', monospace;\n        white-space: pre-wrap;\n        word-wrap: break-word;\n        color: #8f9fa6;\n      }\n\n      .diff-wrapper {\n        margin-top: 10px;\n        background: #4b5766;\n        border-radius: 3px;\n      }\n    }\n  }\n}\n\n@media (min-width: @screen-md-min) {\n  .commit {\n    .commit-box > .panel-body {\n      width: @log-width-large;\n\n      .gravatar {\n        display: block;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "components/commit/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"commit\": \"commit.html\"\n    },\n    \"javascript\": \"commit.bundle.js\",\n    \"css\": \"commit.css\"\n  }\n}\n"
  },
  {
    "path": "components/commitdiff/commitdiff.html",
    "content": "<div class=\"btn-toolbar\" data-bind=\"visible: showDiffButtons\">\n  <div class=\"btn-group btn-group-xs\">\n    <button\n      class=\"btn btn-default commit-whitespace\"\n      data-bind=\"click: whiteSpace.toggle, css: {active: whiteSpace.isActive}\"\n      data-toggle=\"tooltip\"\n      data-placement=\"bottom\"\n      data-container=\"body\"\n      title=\"Hide whitespace changes in diff\"\n    >\n      <span data-bind=\"text: whiteSpace.text\"></span>\n    </button>\n  </div>\n  <div class=\"btn-group btn-group-xs\">\n    <button\n      class=\"btn btn-default commit-sideBySideDiff\"\n      data-bind=\"click: textDiffType.toggle, css: {active: textDiffType.isActive}\"\n      data-toggle=\"tooltip\"\n      data-placement=\"bottom\"\n      data-container=\"body\"\n      title=\"Show side by side diff view\"\n    >\n      <span data-bind=\"text: textDiffType.text\"></span>\n    </button>\n  </div>\n  <div class=\"btn-group btn-group-xs\">\n    <button\n      class=\"btn btn-default commit-wordwrap\"\n      data-bind=\"click: wordWrap.toggle, css: {active: wordWrap.isActive}\"\n      data-toggle=\"tooltip\"\n      data-placement=\"bottom\"\n      data-container=\"body\"\n      title=\"Wrap words per line\"\n    >\n      <span data-bind=\"text: wordWrap.text\"></span>\n    </button>\n  </div>\n</div>\n\n<div data-bind=\"foreach: commitLineDiffs\" class=\"commitdiff\">\n  <div class=\"file\">\n    <div class=\"head commit-diff-filename\" data-bind=\"click: fileNameClick\">\n      <span data-bind=\"text: displayName\"></span>\n      <span class=\"file-stats\" data-bind=\"visible: added() != '-'\">\n        (\n        <span data-bind=\"text: added\"></span>\n        <span data-bind=\"text: removed\"></span>\n        )\n      </span>\n    </div>\n    <div class=\"diffContainer commit-line-diffs\" data-bind=\"component: specificDiff\"></div>\n  </div>\n</div>\n"
  },
  {
    "path": "components/commitdiff/commitdiff.js",
    "content": "const ko = require('knockout');\nconst CommitLineDiff = require('./commitlinediff.js').CommitLineDiff;\nconst components = require('ungit-components');\n\ncomponents.register('commitDiff', (args) => new CommitDiff(args));\n\nclass CommitDiff {\n  constructor(args) {\n    this.sha1 = args.sha1;\n\n    this.showDiffButtons = args.showDiffButtons;\n    this.textDiffType = args.textDiffType = args.textDiffType || components.create('textdiff.type');\n    this.wordWrap = args.wordWrap = args.wordWrap || components.create('textdiff.wordwrap');\n    this.whiteSpace = args.whiteSpace = args.whiteSpace || components.create('textdiff.whitespace');\n\n    this.commitLineDiffs = args.fileLineDiffs.map(\n      (fileLineDiff) => new CommitLineDiff(args, fileLineDiff)\n    );\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('commitdiff', this, {}, parentElement);\n  }\n}\n"
  },
  {
    "path": "components/commitdiff/commitdiff.less",
    "content": ".commitdiff {\n  width: 100%;\n\n  .file {\n    margin-top: 5px;\n    background: rgba(255, 255, 255, 0.16);\n    border-radius: 3px;\n\n    .head {\n      display: block;\n      cursor: pointer;\n      padding: 3px;\n      padding-left: 6px;\n      padding-right: 6px;\n      color: rgba(255, 255, 255, 0.79);\n      word-wrap: break-word;\n\n      .file-stats {\n        span:nth-of-type(1)::before {\n          content: '+';\n        }\n\n        span:nth-of-type(1) {\n          color: #9bf3a9;\n        }\n\n        span:nth-of-type(2)::before {\n          content: '-';\n        }\n\n        span:nth-of-type(2) {\n          color: #ec9d93;\n        }\n      }\n    }\n\n    .diff {\n      background: rgba(0, 0, 0, 0.11);\n\n      .textDiff {\n        color: rgba(255, 255, 255, 0.3);\n      }\n    }\n  }\n}\n\n.loadMore {\n  .btn {\n    display: block;\n    margin: 20px 20px 10px 20px;\n  }\n}\n"
  },
  {
    "path": "components/commitdiff/commitlinediff.js",
    "content": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\n\nclass CommitLineDiff {\n  constructor(args, fileLineDiff) {\n    this.added = ko.observable(fileLineDiff.additions);\n    this.removed = ko.observable(fileLineDiff.deletions);\n    this.fileName = ko.observable(fileLineDiff.fileName);\n    this.oldFileName = ko.observable(fileLineDiff.oldFileName);\n    this.displayName = ko.observable(fileLineDiff.displayName);\n    this.fileType = fileLineDiff.type;\n    this.isShowingDiffs = ko.observable(false);\n    this.repoPath = args.repoPath;\n    this.server = args.server;\n    this.sha1 = args.sha1;\n    this.textDiffType = args.textDiffType;\n    this.wordWrap = args.wordWrap;\n    this.whiteSpace = args.whiteSpace;\n    this.specificDiff = ko.observable(this.getSpecificDiff());\n  }\n\n  getSpecificDiff() {\n    return components.create(`${this.fileType}diff`, {\n      filename: this.fileName(),\n      oldFilename: this.oldFileName(),\n      repoPath: this.repoPath,\n      server: this.server,\n      sha1: this.sha1,\n      textDiffType: this.textDiffType,\n      isShowingDiffs: this.isShowingDiffs,\n      whiteSpace: this.whiteSpace,\n      wordWrap: this.wordWrap,\n    });\n  }\n\n  fileNameClick() {\n    this.isShowingDiffs(!this.isShowingDiffs());\n    programEvents.dispatch({ event: 'graph-render' });\n  }\n}\n\nexports.CommitLineDiff = CommitLineDiff;\n"
  },
  {
    "path": "components/commitdiff/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"commitdiff\": \"commitdiff.html\"\n    },\n    \"javascript\": \"commitdiff.bundle.js\",\n    \"css\": \"commitdiff.css\"\n  }\n}\n"
  },
  {
    "path": "components/crash/crash.html",
    "content": "<div class=\"container\">\n  <div class=\"panel panel-default crash\">\n    <div class=\"panel-body\">\n      <h1>Whooops</h1>\n      Unfortunately ungit interrupted its work because: <code data-bind=\"html: eventcause\"></code\n      ><br />\n      The following tips will help solve this problem depending on the error:\n      <br /><br />\n      <ul>\n        <li>\n          General:\n          <ul>\n            <li>something went wrong, reload the page to start over</li>\n            <li>check server out or logs for errors</li>\n            <li>~/.ungitrc must contain valid JSON. Minimal valid json is \"{}\"</li>\n          </ul>\n        </li>\n\n        <li>\n          Connection Lost:\n          <ul>\n            <li>check status of server or network connection to the ungit server</li>\n          </ul>\n        </li>\n\n        <li>\n          Ad Blockers and Privacy Extensions:\n          <ul>\n            <li>\n              add ungit server url to adblocker exception (with port definition) or disable for a\n              while\n            </li>\n          </ul>\n        </li>\n\n        <li>\n          Git does not have enough permissions:\n          <ul>\n            <li>check if you could write to .git directory (preferably on unix systems)</li>\n          </ul>\n        </li>\n\n        <li>\n          Other:\n          <ul>\n            <li>\n              Find or report bug at\n              <a href=\"https://github.com/FredrikNoren/ungit/issues\">ungit github</a>.<br />\n              Just common sense; do a quick search before posting, someone might already have\n              created an issue (or resolved the problem!).<br />\n              If you're posting a bug; try to include as much relevant information as possible\n              (ungit version, node and npm version, os, any git errors displayed, output from cli\n              console and output from the browser console).\n            </li>\n          </ul>\n        </li>\n      </ul>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "components/crash/crash.js",
    "content": "const ko = require('knockout');\nconst components = require('ungit-components');\n\ncomponents.register('crash', (err) => new CrashViewModel(err));\n\nclass CrashViewModel {\n  constructor(err) {\n    this.eventcause = err ? err : 'unknown error';\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('crash', this, {}, parentElement);\n  }\n}\n"
  },
  {
    "path": "components/crash/crash.less",
    "content": ".crash {\n  margin-top: 20px;\n}\n"
  },
  {
    "path": "components/crash/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"crash\": \"crash.html\"\n    },\n    \"javascript\": \"crash.bundle.js\",\n    \"css\": \"crash.css\"\n  }\n}\n"
  },
  {
    "path": "components/gitErrors/gitErrors.html",
    "content": "<!-- ko foreach: gitErrors -->\n<div class=\"alert\" data-bind=\"css: { 'alert-danger': !isWarning, 'alert-warning': isWarning }\">\n  <button type=\"button\" class=\"close\" data-bind=\"html: $parent.closeIcon, click: dismiss\"></button>\n  <h3>\n    <span data-bind=\"html: $parent.alertIcon\"></span>\n    Unhandled git error!\n  </h3>\n  <p>\n    Ungit tried to run a git command that resulted in an unhandled error.\n    <span data-bind=\"visible: bugReportWasSent\">An automatic bug report was sent.</span>\n  </p>\n  <p data-bind=\"visible: tip, text: tip\"></p>\n  <h4>Command</h4>\n  <pre data-bind=\"text: command\"></pre>\n  <h4>Error</h4>\n  <pre data-bind=\"text: error\"></pre>\n  <h4>Stderr</h4>\n  <pre data-bind=\"text: stderr\"></pre>\n  <h4>Stdout</h4>\n  <pre data-bind=\"text: stdout\"></pre>\n</div>\n<!-- /ko -->\n"
  },
  {
    "path": "components/gitErrors/gitErrors.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\n\ncomponents.register('gitErrors', (args) => new GitErrorsViewModel(args.server, args.repoPath));\n\nclass GitErrorsViewModel {\n  constructor(server, repoPath) {\n    this.server = server;\n    this.repoPath = repoPath;\n    this.gitErrors = ko.observableArray();\n    this.closeIcon = octicons.x.toSVG({ height: 18 });\n    this.alertIcon = octicons.alert.toSVG({ height: 24 });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('gitErrors', this, {}, parentElement);\n  }\n\n  onProgramEvent(event) {\n    if (event.event == 'git-error') this._handleGitError(event);\n  }\n\n  _handleGitError(event) {\n    if (event.data.repoPath != this.repoPath()) return;\n    this.gitErrors.push(new GitErrorViewModel(this, this.server, event.data));\n  }\n}\n\nclass GitErrorViewModel {\n  constructor(gitErrors, server, data) {\n    const self = this;\n    this.gitErrors = gitErrors;\n    this.server = server;\n    this.tip = data.tip;\n    this.isWarning = data.isWarning || false;\n    this.command = data.command;\n    this.error = data.error;\n    this.stdout = data.stdout;\n    this.stderr = data.stderr;\n    this.showEnableBugtracking = ko.observable(false);\n    this.bugReportWasSent = ungit.config.bugtracking;\n\n    if (!data.shouldSkipReport && !ungit.config.bugtracking) {\n      this.server.getPromise('/userconfig').then((userConfig) => {\n        self.showEnableBugtracking(!userConfig.bugtracking);\n      });\n    }\n  }\n\n  dismiss() {\n    this.gitErrors.gitErrors.remove(this);\n  }\n}\n"
  },
  {
    "path": "components/gitErrors/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"gitErrors\": \"gitErrors.html\"\n    },\n    \"javascript\": \"gitErrors.bundle.js\"\n  }\n}\n"
  },
  {
    "path": "components/graph/animateable.js",
    "content": "const ko = require('knockout');\nconst Selectable = require('./selectable');\n\nrequire('mina');\n\nclass Animateable extends Selectable {\n  constructor(graph) {\n    super(graph);\n    this.element = ko.observable();\n    this.previousGraph = undefined;\n    this.element.subscribe((val) => {\n      if (val) this.animate(true);\n    });\n    this.animate = (forceRefresh) => {\n      const currentGraph = this.getGraphAttr();\n      if (\n        this.element() &&\n        (forceRefresh || JSON.stringify(currentGraph) !== JSON.stringify(this.previousGraph))\n      ) {\n        // dom is valid and force refresh is requested or dom moved, redraw\n        if (ungit.config.isAnimate) {\n          const now = Date.now();\n          window.mina(\n            this.previousGraph || currentGraph,\n            currentGraph,\n            now,\n            now + 750,\n            window.mina.time,\n            (val) => {\n              this.setGraphAttr(val);\n            },\n            window.mina.elastic\n          );\n        } else {\n          this.setGraphAttr(currentGraph);\n        }\n        this.previousGraph = currentGraph;\n      }\n    };\n  }\n}\nmodule.exports = Animateable;\n"
  },
  {
    "path": "components/graph/edge.js",
    "content": "const ko = require('knockout');\nconst Animateable = require('./animateable');\n\nclass EdgeViewModel extends Animateable {\n  constructor(graph, nodeAsha1, nodeBsha1) {\n    super(graph);\n    this.nodeA = graph.getNode(nodeAsha1);\n    this.nodeB = graph.getNode(nodeBsha1);\n    this.getGraphAttr = ko.computed(() => {\n      if (this.nodeA.isViewable() && (!this.nodeB.isViewable() || !this.nodeB.isInited)) {\n        return [\n          this.nodeA.cx(),\n          this.nodeA.cy(),\n          this.nodeA.cx(),\n          this.nodeA.cy(),\n          this.nodeA.cx(),\n          graph.graphHeight(),\n          this.nodeA.cx(),\n          graph.graphHeight(),\n        ];\n      } else if (this.nodeB.isInited && this.nodeB.cx() && this.nodeB.cy()) {\n        return [\n          this.nodeA.cx(),\n          this.nodeA.cy(),\n          this.nodeA.cx(),\n          this.nodeA.cy(),\n          this.nodeB.cx(),\n          this.nodeB.cy(),\n          this.nodeB.cx(),\n          this.nodeB.cy(),\n        ];\n      } else {\n        return [0, 0, 0, 0, 0, 0, 0, 0];\n      }\n    });\n    this.getGraphAttr.subscribe(this.animate.bind(this));\n  }\n\n  setGraphAttr(val) {\n    this.element().setAttribute('d', `M${val.slice(0, 4).join(',')}L${val.slice(4, 8).join(',')}`);\n  }\n\n  edgeMouseOver() {\n    if (this.nodeA) {\n      this.nodeA.isEdgeHighlighted(true);\n    }\n    if (this.nodeB) {\n      this.nodeB.isEdgeHighlighted(true);\n    }\n  }\n\n  edgeMouseOut() {\n    if (this.nodeA) {\n      this.nodeA.isEdgeHighlighted(false);\n    }\n    if (this.nodeB) {\n      this.nodeB.isEdgeHighlighted(false);\n    }\n  }\n}\n\nmodule.exports = EdgeViewModel;\n"
  },
  {
    "path": "components/graph/git-graph-actions.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\nconst RefViewModel = require('./git-ref.js');\nconst HoverActions = require('./hover-actions');\n\nconst RebaseViewModel = HoverActions.RebaseViewModel;\nconst MergeViewModel = HoverActions.MergeViewModel;\nconst ResetViewModel = HoverActions.ResetViewModel;\nconst PushViewModel = HoverActions.PushViewModel;\nconst SquashViewModel = HoverActions.SquashViewModel;\n\nclass ActionBase {\n  constructor(graph, text, style, icon) {\n    this.graph = graph;\n    this.server = graph.server;\n    this.isRunning = ko.observable(false);\n    this.isHighlighted = ko.computed(\n      () => !graph.hoverGraphAction() || graph.hoverGraphAction() == this\n    );\n    this.text = text;\n    this.style = style;\n    this.icon = icon;\n    this.cssClasses = ko.computed(() => {\n      if (!this.isHighlighted() || this.isRunning()) {\n        return `${this.style} dimmed`;\n      } else {\n        return this.style;\n      }\n    });\n  }\n\n  doPerform() {\n    if (this.isRunning()) return;\n    this.graph.hoverGraphAction(null);\n    this.isRunning(true);\n    return this.perform()\n      .catch((e) => this.server.unhandledRejection(e))\n      .finally(() => {\n        this.isRunning(false);\n      });\n  }\n\n  dragEnter() {\n    if (!this.visible()) return;\n    this.graph.hoverGraphAction(this);\n  }\n\n  dragLeave() {\n    if (!this.visible()) return;\n    this.graph.hoverGraphAction(null);\n  }\n\n  mouseover() {\n    this.graph.hoverGraphAction(this);\n  }\n\n  mouseout() {\n    this.graph.hoverGraphAction(null);\n  }\n}\n\nclass Move extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Move', 'move', octicons['arrow-left'].toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      return (\n        this.graph.currentActionContext() instanceof RefViewModel &&\n        this.graph.currentActionContext().node() != this.node\n      );\n    });\n  }\n\n  perform() {\n    return this.graph.currentActionContext().moveTo(this.node.sha1);\n  }\n}\n\nclass Reset extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Reset', 'reset', octicons.trash.toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      if (!(this.graph.currentActionContext() instanceof RefViewModel)) return false;\n      const context = this.graph.currentActionContext();\n      if (context.node() != this.node) return false;\n      const remoteRef = context.getRemoteRef(this.graph.currentRemote());\n      return (\n        remoteRef &&\n        remoteRef.node() &&\n        context &&\n        context.node() &&\n        remoteRef.node() != context.node() &&\n        remoteRef.node().date < context.node().date\n      );\n    });\n  }\n\n  createHoverGraphic() {\n    const context = this.graph.currentActionContext();\n    if (!context) return null;\n    const remoteRef = context.getRemoteRef(this.graph.currentRemote());\n    const nodes = context.node().getPathToCommonAncestor(remoteRef.node()).slice(0, -1);\n    return new ResetViewModel(nodes);\n  }\n\n  perform() {\n    const context = this.graph.currentActionContext();\n    const remoteRef = context.getRemoteRef(this.graph.currentRemote());\n    return new Promise((resolve, reject) => {\n      components.showModal('yesnomodal', {\n        title: 'Are you sure?',\n        details: 'Resetting to ref: ' + remoteRef.name + ' cannot be undone with ungit.',\n        closeFunc: async (isYes) => {\n          if (isYes) {\n            await this.server\n              .postPromise('/reset', {\n                path: this.graph.repoPath(),\n                to: remoteRef.name,\n                mode: 'hard',\n              })\n              .then(resolve)\n              .catch(reject);\n            context.node(remoteRef.node());\n          }\n          this.isRunning(false);\n        },\n      });\n    });\n  }\n}\n\nclass Rebase extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Rebase', 'rebase', octicons['repo-forked'].toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      return (\n        this.graph.currentActionContext() instanceof RefViewModel &&\n        (!ungit.config.showRebaseAndMergeOnlyOnRefs || this.node.refs().length > 0) &&\n        this.graph.currentActionContext().current() &&\n        this.graph.currentActionContext().node() != this.node\n      );\n    });\n  }\n\n  createHoverGraphic() {\n    let onto = this.graph.currentActionContext();\n    if (!onto) return;\n    if (onto instanceof RefViewModel) onto = onto.node();\n    const path = onto.getPathToCommonAncestor(this.node);\n    return new RebaseViewModel(this.node, path);\n  }\n\n  perform() {\n    return this.server\n      .postPromise('/rebase', { path: this.graph.repoPath(), onto: this.node.sha1 })\n      .catch((err) => {\n        if (err.errorCode != 'merge-failed') {\n          this.server.unhandledRejection(err);\n        } else {\n          ungit.logger.warn('rebase failed', err);\n        }\n      });\n  }\n}\n\nclass Merge extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Merge', 'merge', octicons['git-merge'].toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      if (!this.graph.checkedOutRef() || !this.graph.checkedOutRef().node()) return false;\n      return (\n        this.graph.currentActionContext() instanceof RefViewModel &&\n        !this.graph.currentActionContext().current() &&\n        this.graph.checkedOutRef().node() == this.node\n      );\n    });\n  }\n\n  createHoverGraphic() {\n    let node = this.graph.currentActionContext();\n    if (!node) return null;\n    if (node instanceof RefViewModel) node = node.node();\n    return new MergeViewModel(this.graph, this.node, node);\n  }\n\n  perform() {\n    return this.server\n      .postPromise('/merge', {\n        path: this.graph.repoPath(),\n        with: this.graph.currentActionContext().localRefName,\n      })\n      .catch((err) => {\n        if (err.errorCode != 'merge-failed') {\n          this.server.unhandledRejection(err);\n        } else {\n          ungit.logger.warn('merge failed', err);\n        }\n      });\n  }\n}\n\nclass Push extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Push', 'push', octicons['repo-push'].toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      return (\n        this.graph.currentActionContext() instanceof RefViewModel &&\n        this.graph.currentActionContext().node() == this.node &&\n        this.graph.currentActionContext().canBePushed(this.graph.currentRemote())\n      );\n    });\n  }\n\n  createHoverGraphic() {\n    const context = this.graph.currentActionContext();\n    if (!context) return null;\n    const remoteRef = context.getRemoteRef(this.graph.currentRemote());\n    if (!remoteRef) return null;\n    return new PushViewModel(remoteRef.node(), context.node());\n  }\n\n  perform() {\n    const ref = this.graph.currentActionContext();\n    const remoteRef = ref.getRemoteRef(this.graph.currentRemote());\n\n    if (remoteRef) {\n      return remoteRef.moveTo(ref.node().sha1);\n    } else {\n      return ref\n        .createRemoteRef()\n        .then(() => {\n          if (this.graph.HEAD().name == ref.name) {\n            this.graph.HEADref().node(ref.node());\n          }\n        })\n        .finally(() => programEvents.dispatch({ event: 'request-fetch-tags' }));\n    }\n  }\n}\n\nclass Checkout extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Checkout', 'checkout', octicons['desktop-download'].toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      if (this.graph.currentActionContext() instanceof RefViewModel)\n        return (\n          this.graph.currentActionContext().node() == this.node &&\n          !this.graph.currentActionContext().current()\n        );\n      return ungit.config.allowCheckoutNodes && this.graph.currentActionContext() == this.node;\n    });\n  }\n\n  perform() {\n    return this.graph.currentActionContext().checkout();\n  }\n}\n\nclass Delete extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Delete', 'delete', octicons.x.toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      return (\n        this.graph.currentActionContext() instanceof RefViewModel &&\n        this.graph.currentActionContext().node() == this.node &&\n        !this.graph.currentActionContext().current()\n      );\n    });\n  }\n\n  perform() {\n    const context = this.graph.currentActionContext();\n    let details = `\"${context.refName}\"`;\n    if (context.isRemoteBranch) {\n      details = `<code _style=\"font-size: 100%\">REMOTE</code> ${details}`;\n    }\n    details = `Deleting ${details} branch or tag cannot be undone with ungit.`;\n\n    return new Promise((resolve, reject) => {\n      components.showModal('yesnomodal', {\n        title: 'Are you sure?',\n        details: details,\n        closeFunc: async (isYes) => {\n          if (isYes) {\n            await context.remove().then(resolve).catch(reject);\n          }\n          this.isRunning(false);\n        },\n      });\n    });\n  }\n}\n\nclass CherryPick extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Cherry pick', 'cherry-pick', octicons.cpu.toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      const context = this.graph.currentActionContext();\n      return context === this.node && this.graph.HEAD() && context.sha1 !== this.graph.HEAD().sha1;\n    });\n  }\n\n  perform() {\n    return this.server\n      .postPromise('/cherrypick', { path: this.graph.repoPath(), name: this.node.sha1 })\n      .catch((err) => {\n        if (err.errorCode != 'merge-failed') {\n          this.server.unhandledRejection(err);\n        } else {\n          ungit.logger.warn('cherrypick failed', err);\n        }\n      });\n  }\n}\n\nclass Uncommit extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Uncommit', 'uncommit', octicons.zap.toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      return this.graph.currentActionContext() == this.node && this.graph.HEAD() == this.node;\n    });\n  }\n\n  perform() {\n    return this.server\n      .postPromise('/reset', { path: this.graph.repoPath(), to: 'HEAD^', mode: 'mixed' })\n      .then(() => {\n        let targetNode = this.node.belowNode;\n        while (targetNode && !targetNode.ancestorOfHEAD()) {\n          targetNode = targetNode.belowNode;\n        }\n        this.graph.HEADref().node(targetNode ? targetNode : null);\n        this.graph.checkedOutRef().node(targetNode ? targetNode : null);\n      });\n  }\n}\n\nclass Revert extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Revert', 'revert', octicons.history.toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      return this.graph.currentActionContext() == this.node;\n    });\n  }\n\n  perform() {\n    return this.server.postPromise('/revert', {\n      path: this.graph.repoPath(),\n      commit: this.node.sha1,\n    });\n  }\n}\n\nclass Squash extends ActionBase {\n  constructor(graph, node) {\n    super(graph, 'Squash', 'squash', octicons.fold.toSVG({ height: 18 }));\n    this.node = node;\n    this.visible = ko.computed(() => {\n      if (this.isRunning()) return true;\n      return (\n        this.graph.currentActionContext() instanceof RefViewModel &&\n        this.graph.currentActionContext().current() &&\n        this.graph.currentActionContext().node() != this.node\n      );\n    });\n  }\n\n  createHoverGraphic() {\n    let onto = this.graph.currentActionContext();\n    if (!onto) return;\n    if (onto instanceof RefViewModel) onto = onto.node();\n\n    return new SquashViewModel(this.node, onto);\n  }\n\n  perform() {\n    let onto = this.graph.currentActionContext();\n    if (!onto) return;\n    if (onto instanceof RefViewModel) onto = onto.node();\n    // remove last element as it would be a common ancestor.\n    const path = this.node.getPathToCommonAncestor(onto).slice(0, -1);\n\n    if (path.length > 0) {\n      // squashing branched out lineage\n      // c is checkout with squash target of e, results in staging changes\n      // from d and e on top of c\n      //\n      // a - b - (c)        a - b - (c) - [de]\n      //  \\           ->     \\\n      //   d  - <e>           d - <e>\n      return this.server.postPromise('/squash', {\n        path: this.graph.repoPath(),\n        target: this.node.sha1,\n      });\n    } else {\n      // squashing backward from same lineage\n      // c is checkout with squash target of a, results in current ref moved\n      // to a and staging changes within b and c on top of a\n      //\n      // <a> - b - (c)       (a) - b - c\n      //                ->     \\\n      //                        [bc]\n      return this.graph\n        .currentActionContext()\n        .moveTo(this.node.sha1, true)\n        .then(() =>\n          this.server.postPromise('/squash', { path: this.graph.repoPath(), target: onto.sha1 })\n        );\n    }\n  }\n}\n\nconst GraphActions = {\n  Move: Move,\n  Rebase: Rebase,\n  Merge: Merge,\n  Push: Push,\n  Reset: Reset,\n  Checkout: Checkout,\n  Delete: Delete,\n  CherryPick: CherryPick,\n  Uncommit: Uncommit,\n  Revert: Revert,\n  Squash: Squash,\n};\nmodule.exports = GraphActions;\n"
  },
  {
    "path": "components/graph/git-node.js",
    "content": "const $ = require('jquery');\nconst ko = require('knockout');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\nconst Animateable = require('./animateable');\nconst GraphActions = require('./git-graph-actions');\n\nconst maxBranchesToDisplay = parseInt((ungit.config.numRefsToShow / 5) * 3); // 3/5 of refs to show to branches\nconst maxTagsToDisplay = ungit.config.numRefsToShow - maxBranchesToDisplay; // 2/5 of refs to show to tags\n\nclass GitNodeViewModel extends Animateable {\n  constructor(graph, sha1) {\n    super(graph);\n    this.graph = graph;\n    this.sha1 = sha1;\n    this.isInited = false;\n    this.title = ko.observable();\n    this.parents = ko.observableArray();\n    this.commitTime = undefined; // commit time in string\n    this.date = undefined; // commit time in numeric format for sort\n    this.color = ko.observable();\n    this.ideologicalBranch = ko.observable();\n    this.remoteTags = ko.observableArray();\n    this.branchesAndLocalTags = ko.observableArray();\n    this.signatureDate = ko.observable();\n    this.signatureMade = ko.observable();\n    this.pgpVerifiedString = ko.computed(() => {\n      if (this.signatureMade()) {\n        return `PGP by: ${this.signatureMade()} at ${this.signatureDate()}`;\n      }\n    });\n\n    this.refs = ko.computed(() => {\n      const rs = this.branchesAndLocalTags().concat(this.remoteTags());\n      rs.sort((a, b) => {\n        if (b.current()) return 1;\n        if (a.current()) return -1;\n        if (a.isLocal && !b.isLocal) return -1;\n        if (!a.isLocal && b.isLocal) return 1;\n        return a.refName < b.refName ? -1 : 1;\n      });\n      return rs;\n    });\n    // These are split up like this because branches and local tags can be found in the git log,\n    // whereas remote tags needs to be fetched with another command (which is much slower)\n    this.branches = ko.observableArray();\n    this.branchesToDisplay = ko.observableArray();\n    this.tags = ko.observableArray();\n    this.tagsToDisplay = ko.observableArray();\n    this.refs.subscribe((newValue) => {\n      if (newValue) {\n        this.branches(newValue.filter((r) => r.isBranch));\n        this.tags(newValue.filter((r) => r.isTag));\n        this.branchesToDisplay(\n          this.branches.slice(\n            0,\n            ungit.config.numRefsToShow - Math.min(this.tags().length, maxTagsToDisplay)\n          )\n        );\n        this.tagsToDisplay(\n          this.tags.slice(0, ungit.config.numRefsToShow - this.branchesToDisplay().length)\n        );\n      } else {\n        this.branches.removeAll();\n        this.tags.removeAll();\n        this.branchesToDisplay.removeAll();\n        this.tagsToDisplay.removeAll();\n      }\n    });\n    this.ancestorOfHEAD = ko.observable(false);\n    this.nodeIsMousehover = ko.observable(false);\n    this.commitContainerVisible = ko.computed(\n      () => this.ancestorOfHEAD() || this.nodeIsMousehover() || this.selected()\n    );\n    this.isEdgeHighlighted = ko.observable(false);\n    // for small empty black circle to highlight a node\n    this.isNodeAccented = ko.computed(() => this.selected() || this.isEdgeHighlighted());\n    // to show changed files and diff boxes on the left of node\n    this.highlighted = ko.computed(() => this.nodeIsMousehover() || this.selected());\n    this.selected.subscribe(() => {\n      programEvents.dispatch({ event: 'graph-render' });\n    });\n    this.showNewRefAction = ko.computed(() => !graph.currentActionContext());\n    this.showRefSearch = ko.computed(\n      () => this.branches().length + this.tags().length > ungit.config.numRefsToShow\n    );\n    this.newBranchName = ko.observable();\n    this.newBranchNameHasFocus = ko.observable(true);\n    this.branchingFormVisible = ko.observable(false);\n    this.canCreateRef = ko.computed(\n      () =>\n        this.newBranchName() && this.newBranchName().trim() && !this.newBranchName().includes(' ')\n    );\n    this.branchOrder = ko.observable();\n    this.aboveNode = undefined;\n    this.belowNode = undefined;\n    this.refSearchFormVisible = ko.observable(false);\n    this.commitComponent = components.create('commit', this);\n    this.r = ko.observable();\n    this.cx = ko.observable();\n    this.cy = ko.observable();\n\n    this.dropareaGraphActions = [\n      new GraphActions.Move(this.graph, this),\n      new GraphActions.Rebase(this.graph, this),\n      new GraphActions.Merge(this.graph, this),\n      new GraphActions.Push(this.graph, this),\n      new GraphActions.Reset(this.graph, this),\n      new GraphActions.Checkout(this.graph, this),\n      new GraphActions.Delete(this.graph, this),\n      new GraphActions.CherryPick(this.graph, this),\n      new GraphActions.Uncommit(this.graph, this),\n      new GraphActions.Revert(this.graph, this),\n      new GraphActions.Squash(this.graph, this),\n    ];\n  }\n\n  getGraphAttr() {\n    return [this.cx(), this.cy()];\n  }\n\n  setGraphAttr(val) {\n    this.element().setAttribute('x', val[0] - 30);\n    this.element().setAttribute('y', val[1] - 30);\n  }\n\n  render() {\n    this.refSearchFormVisible(false);\n    if (!this.isInited) return;\n    if (this.ancestorOfHEAD()) {\n      this.r(30);\n      this.cx(610);\n\n      if (!this.aboveNode) {\n        this.cy(120);\n      } else if (this.aboveNode.ancestorOfHEAD()) {\n        this.cy(this.aboveNode.cy() + 120);\n      } else {\n        this.cy(this.aboveNode.cy() + 60);\n      }\n    } else {\n      this.r(15);\n      this.cx(610 + 90 * this.branchOrder());\n      this.cy(this.aboveNode ? this.aboveNode.cy() + 60 : 120);\n    }\n\n    if (this.aboveNode && this.aboveNode.selected()) {\n      this.cy(this.aboveNode.cy() + this.aboveNode.commitComponent.element().offsetHeight + 30);\n    }\n\n    this.color(this.ideologicalBranch() ? this.ideologicalBranch().color : '#666');\n    this.animate();\n  }\n\n  setData(logEntry) {\n    this.title(logEntry.message.split('\\n')[0]);\n    this.parents(logEntry.parents || []);\n    this.commitTime = logEntry.commitDate;\n    this.date = Date.parse(this.commitTime);\n    this.commitComponent.setData(logEntry);\n    this.signatureMade(logEntry.signatureMade);\n    this.signatureDate(logEntry.signatureDate);\n\n    (logEntry.refs || []).forEach((ref) => {\n      this.graph.getRef(ref).node(this);\n    });\n    this.isInited = true;\n  }\n\n  showBranchingForm() {\n    this.branchingFormVisible(true);\n    this.newBranchNameHasFocus(true);\n  }\n\n  showRefSearchForm(obj, event) {\n    this.refSearchFormVisible(true);\n\n    const textBox = event.currentTarget.parentElement.querySelector('input[type=\"search\"]');\n    const $textBox = $(textBox);\n\n    if (!$textBox.autocomplete('instance')) {\n      const renderItem = (ul, item) => $(`<li><a>${item.displayHtml()}</a></li>`).appendTo(ul);\n      $textBox.autocomplete({\n        classes: {\n          'ui-autocomplete': 'dropdown-menu',\n        },\n        source: this.refs().filter((ref) => !ref.isHEAD),\n        minLength: 0,\n        create: (event) => {\n          $(event.target).data('ui-autocomplete')._renderItem = renderItem;\n        },\n        select: (_event, ui) => {\n          const ref = ui.item;\n          const ray = ref.isTag ? this.tagsToDisplay : this.branchesToDisplay;\n\n          // if ref is in display, remove it, else remove last in array.\n          ray.splice(ray.indexOf(ref), 1);\n          ray.unshift(ref);\n          this.refSearchFormVisible(false);\n\n          // Clear search input on selection\n          return false;\n        },\n      });\n      $textBox.on('focus', (event) => {\n        $(event.target).autocomplete('search', event.target.value);\n      });\n      $textBox.autocomplete('search', '');\n    }\n  }\n\n  createBranch() {\n    if (!this.canCreateRef()) return;\n    this.graph.server\n      .postPromise('/branches', {\n        path: this.graph.repoPath(),\n        name: this.newBranchName(),\n        sha1: this.sha1,\n      })\n      .then(() => {\n        this.graph.getRef(`refs/heads/${this.newBranchName()}`).node(this);\n        if (ungit.config.autoCheckoutOnBranchCreate) {\n          return this.graph.server.postPromise('/checkout', {\n            path: this.graph.repoPath(),\n            name: this.newBranchName(),\n          });\n        }\n      })\n      .catch((e) => this.graph.server.unhandledRejection(e))\n      .finally(() => {\n        this.branchingFormVisible(false);\n        this.newBranchName('');\n        programEvents.dispatch({ event: 'branch-updated' });\n      });\n  }\n\n  createTag() {\n    if (!this.canCreateRef()) return;\n    this.graph.server\n      .postPromise('/tags', {\n        path: this.graph.repoPath(),\n        name: this.newBranchName(),\n        sha1: this.sha1,\n      })\n      .then(() => this.graph.getRef(`refs/tags/${this.newBranchName()}`).node(this))\n      .catch((e) => this.graph.server.unhandledRejection(e))\n      .finally(() => {\n        this.branchingFormVisible(false);\n        this.newBranchName('');\n      });\n  }\n\n  toggleSelected() {\n    this.selected(!this.selected());\n    if (this.selected()) {\n      const commitElement = this.commitComponent.element();\n      const commitRect = commitElement.getBoundingClientRect();\n      if (\n        commitRect.top <\n          +window.getComputedStyle(document.documentElement).scrollPaddingTop.replace('px', '') ||\n        commitRect.bottom > document.documentElement.clientHeight\n      ) {\n        commitElement.scrollIntoView();\n      }\n    }\n    return false;\n  }\n\n  removeRef(ref) {\n    if (ref.isRemoteTag) {\n      this.remoteTags.remove(ref);\n    } else {\n      this.branchesAndLocalTags.remove(ref);\n    }\n  }\n\n  pushRef(ref) {\n    if (ref.isRemoteTag && !this.remoteTags().includes(ref)) {\n      this.remoteTags.push(ref);\n    } else if (!this.branchesAndLocalTags().includes(ref)) {\n      this.branchesAndLocalTags.push(ref);\n    }\n  }\n\n  updateAnimationFrame(deltaT) {\n    this.commitComponent.updateAnimationFrame(deltaT);\n  }\n\n  getPathToCommonAncestor(node) {\n    const path = [];\n    let thisNode = this;\n    while (thisNode && !node.isAncestor(thisNode)) {\n      path.push(thisNode);\n      thisNode = this.graph.nodesById[thisNode.parents()[0]];\n    }\n    if (thisNode) path.push(thisNode);\n    return path;\n  }\n\n  isAncestor(node) {\n    if (node == this) return true;\n    for (const v in this.parents()) {\n      const n = this.graph.nodesById[this.parents()[v]];\n      if (n && n.isAncestor(node)) return true;\n    }\n    return false;\n  }\n\n  getRightToLeftStrike() {\n    return `M ${this.cx() - 30} ${this.cy() - 30} L ${this.cx() + 30} ${this.cy() + 30}`;\n  }\n\n  getLeftToRightStrike() {\n    return `M ${this.cx() + 30} ${this.cy() - 30} L ${this.cx() - 30} ${this.cy() + 30}`;\n  }\n\n  nodeMouseover() {\n    this.nodeIsMousehover(true);\n  }\n\n  nodeMouseout() {\n    this.nodeIsMousehover(false);\n  }\n\n  isViewable() {\n    return this.graph.nodes().includes(this);\n  }\n}\n\nmodule.exports = GitNodeViewModel;\n"
  },
  {
    "path": "components/graph/git-ref.js",
    "content": "const ko = require('knockout');\nconst md5 = require('blueimp-md5');\nconst octicons = require('octicons');\nconst programEvents = require('ungit-program-events');\nconst components = require('ungit-components');\nconst Selectable = require('./selectable');\n\nclass RefViewModel extends Selectable {\n  constructor(fullRefName, graph) {\n    super(graph);\n    this.graph = graph;\n    this.name = fullRefName;\n    this.node = ko.observable();\n    this.localRefName = this.name; // origin/master or master\n    this.refName = this.name; // master\n    this.isRemoteTag = this.name.indexOf('remote-tag: ') == 0;\n    this.isLocalTag = this.name.indexOf('tag: ') == 0;\n    this.isTag = this.isLocalTag || this.isRemoteTag;\n    const isRemoteBranchOrHEAD = this.name.indexOf('refs/remotes/') == 0;\n    this.isLocalHEAD = this.name == 'HEAD';\n    this.isRemoteHEAD = this.name.includes('/HEAD');\n    this.isLocalBranch = this.name.indexOf('refs/heads/') == 0;\n    this.isRemoteBranch = isRemoteBranchOrHEAD && !this.isRemoteHEAD;\n    this.isStash = this.name.indexOf('refs/stash') == 0;\n    this.isHEAD = this.isLocalHEAD || this.isRemoteHEAD;\n    this.isBranch = this.isLocalBranch || this.isRemoteBranch;\n    this.isRemote = isRemoteBranchOrHEAD || this.isRemoteTag;\n    this.isLocal = this.isLocalBranch || this.isLocalTag;\n    if (this.isLocalBranch) {\n      this.localRefName = this.name.slice('refs/heads/'.length);\n      this.refName = this.localRefName;\n    }\n    if (this.isRemoteBranch) {\n      this.localRefName = this.name.slice('refs/remotes/'.length);\n    }\n    if (this.isLocalTag) {\n      this.localRefName = this.name.slice('tag: refs/tags/'.length);\n      this.refName = this.localRefName;\n    }\n    if (this.isRemoteTag) {\n      this.localRefName = this.name.slice('remote-tag: '.length);\n    }\n    const splitedName = this.localRefName.split('/');\n    if (this.isRemote) {\n      // get rid of the origin/ part of origin/branchname\n      this.remote = splitedName[0];\n      this.refName = splitedName.slice(1).join('/');\n    }\n    this.show = true;\n    this.server = this.graph.server;\n    this.isDragging = ko.observable(false);\n    this.current = ko.computed(\n      () => this.isLocalBranch && this.graph.checkedOutBranch() == this.refName\n    );\n    this.color = this._colorFromHashOfString(this.name);\n\n    this.node.subscribe(\n      (oldNode) => {\n        if (oldNode) oldNode.removeRef(this);\n      },\n      null,\n      'beforeChange'\n    );\n    this.node.subscribe((newNode) => {\n      if (newNode) newNode.pushRef(this);\n    });\n\n    // This optimization is for autocomplete display\n    this.value = splitedName[splitedName.length - 1];\n    this.label = this.localRefName;\n\n    this.displayHtml = (largeCurrent) => {\n      const size = largeCurrent && this.current() ? 26 : 18;\n      let prefix = '';\n      if (this.isRemote) {\n        prefix = `<span>${octicons.globe.toSVG({ height: size })}</span> `;\n      }\n      if (this.isBranch) {\n        prefix += `<span>${octicons['git-branch'].toSVG({ height: size })}</span> `;\n      } else if (this.isTag) {\n        prefix += `<span>${octicons.tag.toSVG({ height: size })}</span> `;\n      }\n      return prefix + this.localRefName;\n    };\n  }\n\n  _colorFromHashOfString(string) {\n    return `#${md5(string).toString().slice(0, 6)}`;\n  }\n\n  dragStart() {\n    this.graph.currentActionContext(this);\n    this.isDragging(true);\n    if (document.activeElement) document.activeElement.blur();\n  }\n\n  dragEnd() {\n    this.graph.currentActionContext(null);\n    this.isDragging(false);\n  }\n\n  moveTo(target, rewindWarnOverride) {\n    let promise;\n    if (this.isLocal) {\n      const toNode = this.graph.nodesById[target];\n      const args = {\n        path: this.graph.repoPath(),\n        name: this.refName,\n        sha1: target,\n        force: true,\n        to: target,\n        mode: 'hard',\n      };\n      let operation;\n      if (this.current()) {\n        operation = '/reset';\n      } else if (this.isTag) {\n        operation = '/tags';\n      } else {\n        operation = '/branches';\n      }\n\n      if (!rewindWarnOverride && this.node().date > toNode.date) {\n        promise = new Promise((resolve, reject) => {\n          components.showModal('yesnomodal', {\n            title: 'Are you sure?',\n            details: 'This operation potentially going back in history.',\n            closeFunc: (isYes) => {\n              if (isYes) {\n                return this.server.postPromise(operation, args).then(resolve).catch(reject);\n              }\n            },\n          });\n        });\n      } else {\n        promise = this.server.postPromise(operation, args);\n      }\n    } else {\n      const pushReq = {\n        path: this.graph.repoPath(),\n        remote: this.remote,\n        refSpec: target,\n        remoteBranch: this.refName,\n      };\n      promise = this.server.postPromise('/push', pushReq).catch((err) => {\n        if (err.errorCode === 'non-fast-forward') {\n          return new Promise((resolve, reject) => {\n            components.showModal('yesnomodal', {\n              title: 'Force push?',\n              details: \"The remote branch can't be fast-forwarded.\",\n              closeFunc: (isYes) => {\n                if (!isYes) return resolve(false);\n                pushReq.force = true;\n                this.server.postPromise('/push', pushReq).then(resolve).catch(reject);\n              },\n            });\n          });\n        } else {\n          this.server.unhandledRejection(err);\n        }\n      });\n    }\n\n    return promise\n      .then((res) => {\n        if (!res) return;\n        const targetNode = this.graph.getNode(target);\n        if (this.graph.checkedOutBranch() == this.refName) {\n          this.graph.HEADref().node(targetNode);\n        }\n        this.node(targetNode);\n      })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  remove(isClientOnly) {\n    let url = this.isTag ? '/tags' : '/branches';\n    if (this.isRemote) url = `/remote${url}`;\n\n    return (\n      isClientOnly\n        ? Promise.resolve()\n        : this.server.delPromise(url, {\n            path: this.graph.repoPath(),\n            remote: this.isRemote ? this.remote : null,\n            name: this.refName,\n          })\n    )\n      .then(() => {\n        if (this.node()) this.node().removeRef(this);\n        this.graph.refs.remove(this);\n        delete this.graph.refsByRefName[this.name];\n      })\n      .catch((e) => this.server.unhandledRejection(e))\n      .finally(() => {\n        if (!isClientOnly) {\n          if (url == '/remote/tags') {\n            programEvents.dispatch({ event: 'request-fetch-tags' });\n          } else {\n            programEvents.dispatch({ event: 'branch-updated' });\n          }\n        }\n      });\n  }\n\n  getLocalRef() {\n    return this.graph.getRef(this.getLocalRefFullName(), false);\n  }\n\n  getLocalRefFullName() {\n    if (this.isRemoteBranch) return `refs/heads/${this.refName}`;\n    if (this.isRemoteTag) return `tag: ${this.refName}`;\n    return null;\n  }\n\n  getRemoteRef(remote) {\n    return this.graph.getRef(this.getRemoteRefFullName(remote), false);\n  }\n\n  getRemoteRefFullName(remote) {\n    if (this.isLocalBranch) return `refs/remotes/${remote}/${this.refName}`;\n    if (this.isLocalTag) return `remote-tag: ${remote}/${this.refName}`;\n    return null;\n  }\n\n  canBePushed(remote) {\n    if (!this.isLocal) return false;\n    if (!remote) return false;\n    const remoteRef = this.getRemoteRef(remote);\n    if (!remoteRef) return true;\n    return this.node() != remoteRef.node();\n  }\n\n  createRemoteRef() {\n    return this.server\n      .postPromise('/push', {\n        path: this.graph.repoPath(),\n        remote: this.graph.currentRemote(),\n        refSpec: this.refName,\n        remoteBranch: this.refName,\n      })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  checkout() {\n    const isRemote = this.isRemoteBranch;\n    const isLocalCurrent = this.getLocalRef() && this.getLocalRef().current();\n\n    return Promise.resolve()\n      .then(() => {\n        if (isRemote && !isLocalCurrent) {\n          return this.server.postPromise('/branches', {\n            path: this.graph.repoPath(),\n            name: this.refName,\n            sha1: this.name,\n            force: true,\n          });\n        }\n      })\n      .then(() =>\n        this.server.postPromise('/checkout', { path: this.graph.repoPath(), name: this.refName })\n      )\n      .then(() => {\n        if (isRemote && isLocalCurrent) {\n          return this.server.postPromise('/reset', {\n            path: this.graph.repoPath(),\n            to: this.name,\n            mode: 'hard',\n          });\n        }\n      })\n      .then(() => {\n        this.graph.HEADref().node(this.node());\n      })\n      .catch((err) => {\n        if (err.errorCode != 'merge-failed') {\n          this.server.unhandledRejection(err);\n        } else {\n          ungit.logger.warn('checkout failed', err);\n        }\n      });\n  }\n}\n\nmodule.exports = RefViewModel;\n"
  },
  {
    "path": "components/graph/graph-graphics.html",
    "content": "<svg\n  class=\"graphLog\"\n  xmlns=\"http://www.w3.org/2000/svg\"\n  version=\"1.1\"\n  data-bind=\"attr: { width: graphWidth, height: graphHeight }\"\n>\n  <defs>\n    <marker\n      id=\"rebaseArrowEnd\"\n      viewBox=\"0 0 10 10\"\n      refX=\"0\"\n      refY=\"5\"\n      markerUnits=\"strokeWidth\"\n      markerWidth=\"4\"\n      markerHeight=\"3\"\n      orient=\"auto\"\n    >\n      <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"#22252E\" />\n    </marker>\n    <marker\n      id=\"pushArrowEnd\"\n      viewBox=\"0 0 10 10\"\n      refX=\"0\"\n      refY=\"5\"\n      markerUnits=\"strokeWidth\"\n      markerWidth=\"4\"\n      markerHeight=\"3\"\n      orient=\"auto\"\n    >\n      <path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"rgb(61, 139, 255)\" />\n    </marker>\n  </defs>\n  <g>\n    <!-- ko if: commitNodeEdge -->\n    <g\n      class=\"load-ahead-button\"\n      data-bind=\"attr: { opacity: commitOpacity, visible: commitNodeEdge}, click: loadAhead\"\n    >\n      <path\n        data-bind=\"attr: { d: commitNodeEdge }\"\n        stroke=\"#4A4A4A\"\n        stroke-width=\"8\"\n        stroke-dasharray=\"10, 5\"\n      />\n      <circle\n        data-bind=\"attr: { stroke: commitNodeColor }\"\n        cx=\"610\"\n        cy=\"35\"\n        r=\"30\"\n        stroke-dasharray=\"10, 7\"\n        stroke-width=\"10\"\n        fill=\"transparent\"\n      />\n      <!-- ko if: skip() > 0 -->\n      <circle\n        class=\"loadAhead\"\n        data-bind=\"attr: { fill: commitNodeColor }\"\n        cx=\"610\"\n        cy=\"35\"\n        r=\"15\"\n      />\n      <!-- /ko -->\n    </g>\n    <!-- /ko -->\n\n    <!-- ko with: hoverGraphActionGraphic -->\n    <!-- ko foreach: bgEdges -->\n    <path\n      data-bind=\"attr: { d: d, stroke: stroke, 'stroke-width': strokeWidth, 'stroke-dasharray': strokeDasharray, 'marker-end': markerEnd }\"\n    />\n    <!-- /ko -->\n    <!-- /ko -->\n\n    <!-- ko foreach: edges -->\n    <path\n      data-bind=\"element: element, event: { mouseover: edgeMouseOver, mouseout: edgeMouseOut }\"\n      stroke=\"#4A4A4A\"\n      stroke-width=\"8\"\n    />\n    <!-- /ko -->\n\n    <!-- ko foreach: nodes -->\n    <svg data-bind=\"element: element\">\n      <circle\n        data-bind=\"attr: { r: r, fill: color, 'data-ta-clickable': 'node-clickable-' + $index() }, event: { mouseover: nodeMouseover, mouseout: nodeMouseout }, click: toggleSelected\"\n        cx=\"30\"\n        cy=\"30\"\n      />\n      <!-- ko if: isNodeAccented -->\n      <circle\n        data-bind=\" attr: { r: r() - 4 }, click: toggleSelected\"\n        stroke=\"#252833\"\n        stroke-width=\"4\"\n        fill=\"transparent\"\n        cx=\"30\"\n        cy=\"30\"\n      />\n      <!-- /ko -->\n    </svg>\n    <!-- /ko -->\n\n    <!-- ko with: hoverGraphActionGraphic -->\n    <!-- ko foreach: nodes -->\n    <circle\n      data-bind=\"attr: { cx: cx, cy: cy, r: r, fill: fill, stroke: stroke, 'stroke-width': strokeWidth, 'stroke-dasharray': strokeDasharray }\"\n    />\n    <!-- /ko -->\n    <!-- ko foreach: fgEdges -->\n    <path\n      data-bind=\"attr: { d: d, stroke: stroke, 'stroke-width': strokeWidth, 'stroke-dasharray': strokeDasharray, 'marker-end': markerEnd }\"\n    />\n    <!-- /ko -->\n    <!-- /ko -->\n  </g>\n</svg>\n"
  },
  {
    "path": "components/graph/graph.html",
    "content": "<div class=\"graph\" data-bind=\"scrolledToEnd: scrolledToEnd, click: handleBubbledClick\">\n  <!-- ko template: { name: 'graphGraphics' } --><!-- /ko -->\n\n  <div class=\"nodes\" data-bind=\"foreach: nodes\">\n    <div\n      class=\"nodeContainer animation\"\n      data-bind=\"style: { left: '0px', top: cy() + 'px' }, attr: { 'data-ta-node-title': title }\"\n    >\n      <div\n        class=\"commit-container animation\"\n        data-bind=\"visible: commitContainerVisible, style: { left: cx() - 620 + 'px' }\"\n      >\n        <!-- ko component: commitComponent -->\n        <!-- /ko -->\n      </div>\n\n      <div class=\"rightSideContainer\" data-bind=\"style: { left: cx() + r() - 433 + 'px' }\">\n        <!-- ko foreach: branchesToDisplay -->\n        <span\n          class=\"ref branch\"\n          draggable=\"true\"\n          tabIndex=\"-1\"\n          data-bind=\"css: { current: current, remote: isRemoteBranch, dragging: isDragging, focused: selected },\n              html: displayHtml(true),\n              click: selected,\n              event: { dblclick: checkout },\n              dragStart: dragStart, dragEnd: dragEnd, attr: { 'data-ta-name': localRefName, 'data-ta-local': isLocal }\"\n        >\n        </span>\n        <!-- /ko -->\n\n        <!-- ko foreach: tagsToDisplay -->\n        <span\n          class=\"ref tag\"\n          draggable=\"true\"\n          tabIndex=\"0\"\n          data-bind=\"css: { current: current, remote: isRemoteTag, dragging: isDragging, focused: selected },\n              html: displayHtml(true),\n              click: selected,\n              event: { dblclick: checkout },\n              dragStart: dragStart, dragEnd: dragEnd, attr: { 'data-ta-name': localRefName }\"\n        >\n        </span>\n        <!-- /ko -->\n\n        <!-- ko foreach: dropareaGraphActions -->\n        <span\n          class=\"graphAction\"\n          data-bind=\"css: cssClasses, visible: visible, attr: { 'data-ta-action': style }, event: { mouseover: mouseover, mouseout: mouseout }\"\n        >\n          <span data-bind=\"html: icon\"></span>\n          <span data-bind=\"text: text\"></span>\n          <div\n            class=\"dropmask\"\n            tabindex=\"0\"\n            role=\"button\"\n            data-bind=\"dropOver: visible, drop: doPerform, dragEnter: dragEnter, dragLeave: dragLeave, click: doPerform\"\n          ></div>\n        </span>\n        <!-- /ko -->\n\n        <!-- ko if: showNewRefAction -->\n        <span class=\"ref-icons new-ref\" data-bind=\"css: { editing: branchingFormVisible }\">\n          <button\n            class=\"showBranchingForm\"\n            type=\"button\"\n            data-bind=\"html: $parent.plusIcon, click: showBranchingForm, visible: !branchingFormVisible()\"\n            data-toggle=\"tooltip\"\n            data-placement=\"bottom\"\n            title=\"Create a branch or tag\"\n          ></button>\n          <!-- ko if: branchingFormVisible -->\n          <form\n            class=\"form-inline\"\n            data-bind=\"hasfocus2: branchingFormVisible, submit: createBranch\"\n          >\n            <input\n              class=\"name form-control\"\n              type=\"text\"\n              aria-label=\"New branch name\"\n              data-bind=\"value: newBranchName, hasfocus: newBranchNameHasFocus, valueUpdate: 'afterkeydown'\"\n            />\n            <button\n              class=\"btn btn-primary\"\n              type=\"submit\"\n              data-bind=\"click: createBranch, enable: canCreateRef\"\n            >\n              Branch\n            </button>\n            <button\n              class=\"btn btn-default\"\n              type=\"button\"\n              data-bind=\"click: createTag, enable: canCreateRef\"\n            >\n              Tag\n            </button>\n          </form>\n          <!-- /ko -->\n        </span>\n        <!-- /ko -->\n\n        <!-- ko if: showRefSearch -->\n        <span class=\"ref-icons\" data-bind=\"css: { editing: branchingFormVisible }\">\n          <button\n            class=\"showSearchForm\"\n            type=\"button\"\n            data-bind=\"html: $parent.searchIcon, click: showRefSearchForm, visible: !refSearchFormVisible()\"\n            data-toggle=\"tooltip\"\n            data-placement=\"bottom\"\n            title=\"Search for a branch or tag\"\n          ></button>\n          <div class=\"form-inline branch-search\" data-bind=\"visible: refSearchFormVisible()\">\n            <input\n              class=\"name form-control\"\n              type=\"search\"\n              aria-label=\"Filter branches and tags\"\n              data-bind=\"hasfocus: refSearchFormVisible, valueUpdate: 'afterkeyup'\"\n            />\n          </div>\n        </span>\n        <!-- /ko -->\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "components/graph/graph.js",
    "content": "const ko = require('knockout');\nconst _ = require('lodash');\nconst moment = require('moment');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst GitNodeViewModel = require('./git-node');\nconst GitRefViewModel = require('./git-ref');\nconst EdgeViewModel = require('./edge');\nconst { ComponentRoot } = require('../ComponentRoot');\nconst numberOfNodesPerLoad = ungit.config.numberOfNodesPerLoad;\n\ncomponents.register('graph', (args) => new GraphViewModel(args.server, args.repoPath));\n\nclass GraphViewModel extends ComponentRoot {\n  constructor(server, repoPath) {\n    super();\n    this._isLoadNodesFromApiRunning = false;\n    this.updateBranches = _.debounce(this._updateBranches, 250, this.defaultDebounceOption);\n    this.loadNodesFromApi = _.debounce(this._loadNodesFromApi, 250, this.defaultDebounceOption);\n    this._markIdeologicalStamp = 0;\n    this.repoPath = repoPath;\n    this.limit = ko.observable(numberOfNodesPerLoad);\n    this.skip = ko.observable(0);\n    this.server = server;\n    this.currentRemote = ko.observable();\n    this.nodes = ko.observableArray();\n    this.edges = ko.observableArray();\n    this.refs = ko.observableArray();\n    this.nodesById = {};\n    this.edgesById = {};\n    this.refsByRefName = {};\n    this.checkedOutBranch = ko.observable();\n    this.checkedOutRef = ko.computed(() =>\n      this.checkedOutBranch() ? this.getRef(`refs/heads/${this.checkedOutBranch()}`) : null\n    );\n    this.HEADref = ko.observable();\n    this.HEAD = ko.computed(() => (this.HEADref() ? this.HEADref().node() : undefined));\n    this.commitNodeColor = ko.computed(() => (this.HEAD() ? this.HEAD().color() : '#4A4A4A'));\n    this.commitNodeEdge = ko.computed(() => {\n      if (!this.HEAD() || !this.HEAD().cx() || !this.HEAD().cy()) return;\n      return `M 610 68 L ${this.HEAD().cx()} ${this.HEAD().cy()}`;\n    });\n    this.currentActionContext = ko.observable();\n    this.scrolledToEnd = _.debounce(\n      () => {\n        this.limit(numberOfNodesPerLoad + this.limit());\n        this.loadNodesFromApi();\n      },\n      500,\n      true\n    );\n    this.loadAhead = _.debounce(\n      () => {\n        if (this.skip() <= 0) return;\n        this.skip(Math.max(this.skip() - numberOfNodesPerLoad, 0));\n        this.loadNodesFromApi();\n      },\n      500,\n      true\n    );\n    this.commitOpacity = ko.observable(1.0);\n    this.heighstBranchOrder = 0;\n    this.hoverGraphActionGraphic = ko.observable();\n    this.hoverGraphActionGraphic.subscribe(\n      (value) => {\n        if (value && value.destroy) value.destroy();\n      },\n      null,\n      'beforeChange'\n    );\n\n    this.hoverGraphAction = ko.observable();\n    this.hoverGraphAction.subscribe((value) => {\n      if (value && value.createHoverGraphic) {\n        this.hoverGraphActionGraphic(value.createHoverGraphic());\n      } else {\n        this.hoverGraphActionGraphic(null);\n      }\n    });\n\n    this.loadNodesFromApi();\n    this.updateBranches();\n    this.graphWidth = ko.observable();\n    this.graphHeight = ko.observable(800);\n    this.searchIcon = octicons.search.toSVG({ height: 18 });\n    this.plusIcon = octicons.plus.toSVG({ height: 18 });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('graph', this, {}, parentElement);\n  }\n\n  getNode(sha1, logEntry) {\n    let nodeViewModel = this.nodesById[sha1];\n    if (!nodeViewModel) nodeViewModel = this.nodesById[sha1] = new GitNodeViewModel(this, sha1);\n    if (logEntry) nodeViewModel.setData(logEntry);\n    return nodeViewModel;\n  }\n\n  getRef(ref, constructIfUnavailable) {\n    if (constructIfUnavailable === undefined) constructIfUnavailable = true;\n    let refViewModel = this.refsByRefName[ref];\n    if (!refViewModel && constructIfUnavailable) {\n      refViewModel = this.refsByRefName[ref] = new GitRefViewModel(ref, this);\n      this.refs.push(refViewModel);\n      if (refViewModel.name === 'HEAD') {\n        this.HEADref(refViewModel);\n      }\n    }\n    return refViewModel;\n  }\n\n  async _loadNodesFromApi() {\n    this._isLoadNodesFromApiRunning = true;\n    ungit.logger.debug('graph.loadNodesFromApi() triggered');\n    const nodeSize = this.nodes().length;\n    const edges = [];\n\n    try {\n      const log = await this.server.getPromise('/gitlog', {\n        path: this.repoPath(),\n        limit: this.limit(),\n        skip: this.skip(),\n      });\n      if (this.isSamePayload(log)) {\n        return;\n      }\n      const nodes = this.computeNode(\n        (log.nodes || []).map((logEntry) => {\n          return this.getNode(logEntry.sha1, logEntry); // convert to node object\n        })\n      );\n\n      // create edges\n      nodes.forEach((node) => {\n        node.parents().forEach((parentSha1) => {\n          edges.push(this.getEdge(node.sha1, parentSha1));\n        });\n        node.render();\n      });\n\n      this.edges(edges);\n      this.nodes(nodes);\n      if (nodes.length > 0) {\n        this.graphHeight(nodes[nodes.length - 1].cy() + 80);\n      }\n      this.graphWidth(1000 + this.heighstBranchOrder * 90);\n    } catch (e) {\n      this.server.unhandledRejection(e);\n    } finally {\n      if (window.innerHeight - this.graphHeight() > 0 && nodeSize != this.nodes().length) {\n        this.scrolledToEnd();\n      }\n      this._isLoadNodesFromApiRunning = false;\n      ungit.logger.debug('graph.loadNodesFromApi() finished');\n    }\n  }\n\n  traverseNodeLeftParents(node, callback) {\n    callback(node);\n    const parent = this.nodesById[node.parents()[0]];\n    if (parent) {\n      this.traverseNodeLeftParents(parent, callback);\n    }\n  }\n\n  computeNode(nodes) {\n    this.markNodesIdeologicalBranches(this.refs());\n\n    const updateTimeStamp = moment().valueOf();\n    if (this.HEAD()) {\n      this.traverseNodeLeftParents(this.HEAD(), (node) => {\n        node.ancestorOfHEADTimeStamp = updateTimeStamp;\n      });\n    }\n\n    // Filter out nodes which doesn't have a branch (staging and orphaned nodes)\n    nodes = nodes.filter(\n      (node) =>\n        (node.ideologicalBranch() && !node.ideologicalBranch().isStash) ||\n        node.ancestorOfHEADTimeStamp == updateTimeStamp\n    );\n\n    let branchSlotCounter = this.HEAD() ? 1 : 0;\n\n    // Then iterate from the bottom to fix the orders of the branches\n    for (let i = nodes.length - 1; i >= 0; i--) {\n      const node = nodes[i];\n      if (node.ancestorOfHEADTimeStamp == updateTimeStamp) continue;\n      const ideologicalBranch = node.ideologicalBranch();\n\n      // First occurrence of the branch, find an empty slot for the branch\n      if (ideologicalBranch.lastSlottedTimeStamp != updateTimeStamp) {\n        ideologicalBranch.lastSlottedTimeStamp = updateTimeStamp;\n        ideologicalBranch.branchOrder = branchSlotCounter++;\n      }\n\n      node.branchOrder(ideologicalBranch.branchOrder);\n    }\n\n    this.heighstBranchOrder = branchSlotCounter - 1;\n    let prevNode;\n    nodes.forEach((node) => {\n      node.ancestorOfHEAD(node.ancestorOfHEADTimeStamp == updateTimeStamp);\n      if (node.ancestorOfHEAD()) node.branchOrder(0);\n      node.aboveNode = prevNode;\n      if (prevNode) prevNode.belowNode = node;\n      prevNode = node;\n    });\n\n    return nodes;\n  }\n\n  getEdge(nodeAsha1, nodeBsha1) {\n    const id = `${nodeAsha1}-${nodeBsha1}`;\n    let edge = this.edgesById[id];\n    if (!edge) {\n      edge = this.edgesById[id] = new EdgeViewModel(this, nodeAsha1, nodeBsha1);\n    }\n    return edge;\n  }\n\n  markNodesIdeologicalBranches(refs) {\n    refs = refs.filter((r) => !!r.node());\n    refs = refs.sort((a, b) => {\n      if (a.isLocal && !b.isLocal) return -1;\n      if (b.isLocal && !a.isLocal) return 1;\n      if (a.isBranch && !b.isBranch) return -1;\n      if (b.isBranch && !a.isBranch) return 1;\n      if (a.isHEAD && !b.isHEAD) return 1;\n      if (!a.isHEAD && b.isHEAD) return -1;\n      if (a.isStash && !b.isStash) return 1;\n      if (b.isStash && !a.isStash) return -1;\n      if (a.node() && a.node().date && b.node() && b.node().date)\n        return a.node().date - b.node().date;\n      return a.refName < b.refName ? -1 : 1;\n    });\n    const stamp = this._markIdeologicalStamp++;\n    refs.forEach((ref) => {\n      this.traverseNodeParents(ref.node(), (node) => {\n        if (node.stamp == stamp) return false;\n        node.stamp = stamp;\n        node.ideologicalBranch(ref);\n        return true;\n      });\n    });\n  }\n\n  traverseNodeParents(node, callback) {\n    if (!callback(node)) return false;\n    for (let i = 0; i < node.parents().length; i++) {\n      // if parent, travers parent\n      const parent = this.nodesById[node.parents()[i]];\n      if (parent) {\n        this.traverseNodeParents(parent, callback);\n      }\n    }\n  }\n\n  handleBubbledClick(elem, event) {\n    // If the clicked element is bound to the current action context,\n    // then let's not deselect it.\n    if (ko.dataFor(event.target) === this.currentActionContext()) return;\n    if (this.currentActionContext() && this.currentActionContext() instanceof GitNodeViewModel) {\n      this.currentActionContext().toggleSelected();\n    } else {\n      this.currentActionContext(null);\n    }\n    // If the click was on an input element, then let's allow the default action to proceed.\n    // This is especially needed since for some strange reason any submit (ie. enter in a textbox)\n    // will trigger a click event on the submit input of the form, which will end up here,\n    // and if we don't return true, then the submit event is never fired, breaking stuff.\n    if (event.target.nodeName === 'INPUT') return true;\n  }\n\n  onProgramEvent(event) {\n    if (event.event == 'git-directory-changed' || event.event === 'working-tree-changed') {\n      this.loadNodesFromApi();\n      this.updateBranches();\n    } else if (event.event == 'request-app-content-refresh') {\n      this.loadNodesFromApi();\n    } else if (event.event == 'remote-tags-update') {\n      this.setRemoteTags(event.tags);\n    } else if (event.event == 'current-remote-changed') {\n      this.currentRemote(event.newRemote);\n    } else if (event.event == 'graph-render') {\n      this.nodes().forEach((node) => {\n        node.render();\n      });\n    }\n  }\n\n  updateAnimationFrame(deltaT) {\n    this.nodes().forEach((node) => {\n      node.updateAnimationFrame(deltaT);\n    });\n  }\n\n  async _updateBranches() {\n    const checkout = await this.server.getPromise('/checkout', { path: this.repoPath() });\n\n    try {\n      ungit.logger.debug('setting checkedOutBranch', checkout);\n      this.checkedOutBranch(checkout);\n    } catch (err) {\n      if (err.errorCode != 'not-a-repository') {\n        this.server.unhandledRejection(err);\n      } else {\n        ungit.logger.warn('updateBranches failed', err);\n      }\n    }\n  }\n\n  setRemoteTags(remoteTags) {\n    const version = Date.now();\n\n    const sha1Map = {}; // map holding true sha1 per tags\n    remoteTags.forEach((tag) => {\n      if (tag.name.includes('^{}')) {\n        // This tag is a dereference tag, use this sha1.\n        const tagRef = tag.name.slice(0, tag.name.length - '^{}'.length);\n        sha1Map[tagRef] = tag.sha1;\n      } else if (!sha1Map[tag.name]) {\n        // If sha1 wasn't previously set, use this sha1\n        sha1Map[tag.name] = tag.sha1;\n      }\n    });\n\n    remoteTags.forEach((ref) => {\n      if (!ref.name.includes('^{}')) {\n        const name = `remote-tag: ${ref.remote}/${ref.name.split('/')[2]}`;\n        this.getRef(name).node(this.getNode(sha1Map[ref.name]));\n        this.getRef(name).version = version;\n      }\n    });\n    this.refs().forEach((ref) => {\n      // tag is removed from another source\n      if (ref.isRemoteTag && (!ref.version || ref.version < version)) {\n        ref.remove(true);\n      }\n    });\n  }\n\n  checkHeadMove(toNode) {\n    if (this.HEAD() === toNode) {\n      this.HEADref().node(toNode);\n    }\n  }\n}\n"
  },
  {
    "path": "components/graph/graph.less",
    "content": "@import 'public/less/variables.less';\n\n.graph {\n  position: relative;\n  display: inline-block;\n  width: 100%;\n\n  .graphLog {\n    left: 575px;\n\n    .loadAhead {\n      cursor: pointer;\n      animation: throb 1s ease alternate infinite;\n    }\n  }\n\n  @keyframes throb {\n    50% {\n      r: 20;\n    }\n\n    100% {\n      r: 18;\n    }\n  }\n\n  .nodeContainer {\n    position: absolute;\n    left: 30px;\n    top: 120px;\n    width: 100%;\n\n    .commit-container {\n      position: absolute;\n      top: -43px;\n    }\n\n    .rightSideContainer {\n      position: absolute;\n      display: flex;\n      margin-left: (@log-width-small + 40px);\n      top: -22px;\n      white-space: nowrap;\n      height: 40px;\n    }\n\n    .ref {\n      display: inline-block;\n      opacity: 0.6;\n      cursor: move;\n      margin: 7px 0 4px 0;\n      outline: none;\n      padding: 2px 5px 2px 5px;\n      border: 3px solid transparent;\n      border-radius: 10px;\n      transition: border-color 0.5s;\n      -webkit-transition: border-color 0.5s;\n\n      &.dragging.focused {\n        border-color: transparent;\n      }\n\n      &.focused {\n        border-color: #ffffff;\n      }\n\n      &.current {\n        font-weight: bold;\n        opacity: 1;\n        font-size: 20px;\n        margin-top: 2px;\n        margin-bottom: -2px;\n      }\n\n      &.remote {\n        color: #5db4ff;\n      }\n\n      &.tag {\n        color: #eef266;\n      }\n    }\n\n    .graphAction {\n      color: #ffffff;\n      cursor: pointer;\n      transition: all 0.5s ease 0.2s;\n      transition-property: opacity;\n      margin: 8px 2.5px 4px 2.5px;\n      padding: 5px 10px;\n      position: relative;\n      border-radius: 3px;\n\n      .dropmask {\n        position: absolute;\n        left: 0;\n        top: 0;\n        width: 100%;\n        height: 100%;\n      }\n\n      &.dimmed {\n        opacity: 0.5;\n      }\n\n      &.push {\n        background: rgba(61, 139, 255, 0.9);\n      }\n\n      &.pull {\n        background: rgba(38, 189, 189, 0.9);\n      }\n\n      &.reset {\n        background: rgba(255, 129, 31, 0.9);\n      }\n\n      &.rebase {\n        background: rgba(65, 222, 60, 0.9);\n      }\n\n      &.squash {\n        background: rgba(100, 60, 222, 0.9);\n      }\n\n      &.move {\n        background: rgba(0, 0, 0, 0.1);\n      }\n\n      &.merge {\n        background: rgba(208, 135, 212, 0.9);\n      }\n\n      &.checkout {\n        background: rgba(205, 219, 55, 0.9);\n      }\n\n      &.delete {\n        background: rgba(214, 77, 56, 0.9);\n      }\n\n      &.cherry-pick {\n        background: rgba(110, 156, 110, 0.9);\n      }\n\n      &.uncommit {\n        background: rgba(158, 53, 20, 0.9);\n      }\n\n      &.revert {\n        background: rgba(179, 135, 43, 0.9);\n      }\n    }\n\n    .ref-icons {\n      opacity: 1;\n      padding-top: 2px;\n      padding-bottom: 2px;\n      margin-left: 5px;\n\n      .showBranchingForm {\n        background: transparent;\n        border: 0;\n        cursor: pointer;\n        padding: 0;\n        margin: 0;\n        margin-top: 9px;\n        -webkit-text-stroke-width: 1px;\n        color: rgba(255, 255, 255, 0.3);\n\n        &:hover {\n          color: rgba(255, 255, 255, 0.8);\n        }\n      }\n\n      .showSearchForm {\n        background: transparent;\n        border: 0;\n        cursor: pointer;\n        padding: 0;\n        margin: 0;\n        margin-top: 9px;\n        -webkit-text-stroke-width: 1px;\n        color: rgba(255, 255, 255, 0.3);\n\n        &:hover {\n          color: rgba(255, 255, 255, 0.8);\n        }\n      }\n\n      .form-inline {\n        display: inline-block;\n      }\n\n      input.name {\n        width: 150px;\n      }\n    }\n  }\n\n  .graphFooter {\n    height: 60px;\n  }\n}\n\n.ui-autocomplete {\n  min-width: 150px;\n\n  .octicon {\n    width: 18px;\n  }\n}\n"
  },
  {
    "path": "components/graph/hover-actions.js",
    "content": "const getEdgeModelWithD = (d, stroke, strokeWidth, strokeDasharray, markerEnd) => ({\n  d,\n  stroke: stroke ? stroke : '#4A4A4A',\n  strokeWidth: strokeWidth ? strokeWidth : '8',\n  strokeDasharray: strokeDasharray ? strokeDasharray : '10, 5',\n  markerEnd: markerEnd ? markerEnd : '',\n});\nconst getEdgeModel = (scx, scy, tcx, tcy, stroke, strokeWidth, strokeDasharray, markerEnd) => {\n  return getEdgeModelWithD(\n    `M ${scx} ${scy} L ${tcx} ${tcy}`,\n    stroke,\n    strokeWidth,\n    strokeDasharray,\n    markerEnd\n  );\n};\nconst getNodeModel = (cx, cy, r, fill, stroke, strokeWidth, strokeDasharray) => ({\n  cx,\n  cy,\n  r,\n  fill,\n  stroke: stroke ? stroke : '#41DE3C',\n  strokeWidth: strokeWidth ? strokeWidth : '8',\n  strokeDasharray: strokeDasharray ? strokeDasharray : '10, 5',\n});\n\nclass HoverViewModel {\n  constructor() {\n    this.bgEdges = [];\n    this.nodes = [];\n    this.fgEdges = [];\n  }\n}\n\nclass MergeViewModel extends HoverViewModel {\n  constructor(graph, headNode, node) {\n    super();\n    this.graph = graph;\n    this.bgEdges = [\n      getEdgeModel(headNode.cx(), headNode.cy() - 110, headNode.cx(), headNode.cy()),\n      getEdgeModel(headNode.cx(), headNode.cy() - 110, node.cx(), node.cy()),\n    ];\n    this.nodes = [\n      getNodeModel(\n        headNode.cx(),\n        headNode.cy() - 110,\n        Math.max(headNode.r(), node.r()),\n        '#252833',\n        '#41DE3C',\n        '8',\n        '10, 5'\n      ),\n    ];\n\n    graph.commitOpacity(0.1);\n  }\n\n  destroy() {\n    this.graph.commitOpacity(1.0);\n  }\n}\n\nexports.MergeViewModel = MergeViewModel;\n\nclass RebaseViewModel extends HoverViewModel {\n  constructor(onto, nodesThatWillMove) {\n    super();\n    nodesThatWillMove = nodesThatWillMove.slice(0, -1);\n\n    if (nodesThatWillMove.length == 0) return;\n\n    this.bgEdges.push(getEdgeModel(onto.cx(), onto.cy(), onto.cx(), onto.cy() - 60));\n    nodesThatWillMove.forEach((node, i) => {\n      const cy = onto.cy() + -90 * (i + 1);\n      this.nodes.push(getNodeModel(onto.cx(), cy, 28, 'transparent'));\n      if (i + 1 < nodesThatWillMove.length) {\n        this.bgEdges.push(getEdgeModel(onto.cx(), cy - 25, onto.cx(), cy - 65));\n      }\n    });\n  }\n}\nexports.RebaseViewModel = RebaseViewModel;\n\nclass ResetViewModel extends HoverViewModel {\n  constructor(nodes) {\n    super();\n    nodes.forEach((node) => {\n      this.fgEdges.push(\n        getEdgeModelWithD(node.getLeftToRightStrike(), 'rgb(255, 129, 31)', '8', '0, 0')\n      );\n      this.fgEdges.push(\n        getEdgeModelWithD(node.getRightToLeftStrike(), 'rgb(255, 129, 31)', '8', '0, 0')\n      );\n    });\n  }\n}\nexports.ResetViewModel = ResetViewModel;\n\nclass PushViewModel extends HoverViewModel {\n  constructor(fromNode, toNode) {\n    super();\n    this.fgEdges = [\n      getEdgeModel(\n        fromNode.cx(),\n        fromNode.cy(),\n        toNode.cx(),\n        toNode.cy() + 40,\n        'rgb(61, 139, 255)',\n        '15',\n        '10, 5',\n        'url(#pushArrowEnd)'\n      ),\n    ];\n  }\n}\nexports.PushViewModel = PushViewModel;\n\nclass SquashViewModel extends HoverViewModel {\n  constructor(from, onto) {\n    super();\n    let path = from.getPathToCommonAncestor(onto);\n\n    if (path.length == 0) {\n      return;\n    } else if (path.length == 1) {\n      path = onto.getPathToCommonAncestor(from);\n    } else {\n      this.nodes.push(getNodeModel(onto.cx(), onto.cy() - 120, 28, 'transparent'));\n    }\n\n    path.slice(0, -1).forEach((node) => {\n      this.nodes.push(getNodeModel(node.cx(), node.cy(), node.r() + 2, 'rgba(100, 60, 222, 0.8)'));\n    });\n  }\n}\nexports.SquashViewModel = SquashViewModel;\n"
  },
  {
    "path": "components/graph/selectable.js",
    "content": "var ko = require('knockout');\n\nclass Selectable {\n  constructor(graph) {\n    this.selected = ko.computed({\n      read() {\n        return graph.currentActionContext() == this;\n      },\n      write(val) {\n        // val is this if we're called from a click ko binding\n        if (val === this || val === true) {\n          graph.currentActionContext(this);\n        } else if (graph.currentActionContext() == this) {\n          graph.currentActionContext(null);\n        }\n      },\n      owner: this,\n    });\n  }\n}\nmodule.exports = Selectable;\n"
  },
  {
    "path": "components/graph/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"graph\": \"graph.html\",\n      \"graphGraphics\": \"graph-graphics.html\"\n    },\n    \"javascript\": \"graph.bundle.js\",\n    \"css\": \"graph.css\"\n  }\n}\n"
  },
  {
    "path": "components/header/header.html",
    "content": "<div class=\"navbar navbar-default navbar-fixed-top\">\n  <a\n    class=\"backlink\"\n    href=\"#/\"\n    data-toggle=\"tooltip\"\n    data-placement=\"bottom\"\n    title=\"Navigate to ungit home page\"\n  >\n    <span\n      class=\"back-icon octicon-circled\"\n      data-bind=\"html: backIcon, css: { 'back-icon-shown': showBackButton }\"\n    ></span>\n    <img class=\"headerLogo\" src=\"images/logo.png\" alt=\"Ungit Logo\" />\n  </a>\n\n  <div class=\"form-container\">\n    <form class=\"path-input-form\" data-bind=\"submit: submitPath\">\n      <input\n        class=\"form-control input-lg\"\n        type=\"text\"\n        data-bind=\"value: path, autocomplete: path\"\n        placeholder=\"Enter path to repository\"\n        aria-label=\"Path to repository\"\n      />\n      <button\n        class=\"btn btn-default add-to-repolist\"\n        type=\"button\"\n        data-bind=\"html: addIcon, visible: showAddToRepoListButton, click: addCurrentPathToRepoList\"\n        data-toggle=\"tooltip\"\n        data-placement=\"bottom\"\n        title=\"Add current git directory to Ungit home page\"\n      ></button>\n    </form>\n  </div>\n  <div class=\"toolbar\"><!-- ko component: refreshButton --><!-- /ko --></div>\n\n  <div class=\"arrow\"></div>\n  <div class=\"version\">\n    <span data-bind=\"text: currentVersion\" />\n  </div>\n</div>\n\n<div class=\"navbarPadder\"></div>\n"
  },
  {
    "path": "components/header/header.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst navigation = require('ungit-navigation');\nconst programEvents = require('ungit-program-events');\nconst { encodePath } = require('ungit-address-parser');\n\ncomponents.register('header', (args) => new HeaderViewModel(args.app));\n\nclass HeaderViewModel {\n  constructor(app) {\n    this.app = app;\n    this.showBackButton = ko.observable(false);\n    this.path = ko.observable();\n    this.currentVersion = ungit.version;\n    this.refreshButton = components.create('refreshbutton', { isLarge: true });\n    this.showAddToRepoListButton = ko.computed(\n      () => this.path() && !this.app.repoList().includes(this.path())\n    );\n    this.addIcon = octicons.plus.toSVG({ height: 18 });\n    this.backIcon = octicons['arrow-left'].toSVG({ height: 24 });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('header', this, {}, parentElement);\n  }\n\n  submitPath() {\n    navigation.browseTo(`repository?path=${encodePath(this.path())}`);\n  }\n\n  onProgramEvent(event) {\n    if (event.event == 'navigation-changed') {\n      this.showBackButton(event.path != '');\n      if (event.path == '') this.path('');\n    } else if (event.event == 'navigated-to-path') {\n      this.path(event.path);\n    }\n  }\n\n  addCurrentPathToRepoList() {\n    programEvents.dispatch({ event: 'request-remember-repo', repoPath: this.path() });\n    return true;\n  }\n}\n"
  },
  {
    "path": "components/header/header.less",
    "content": "@import 'public/less/variables.less';\n\nhtml {\n  scroll-behavior: smooth;\n  scroll-padding-top: 100px;\n}\n\n.navbarPadder {\n  height: 81px;\n}\n\n.navbar {\n  padding-top: 13px;\n  z-index: 5;\n  box-shadow: 0 15px 15px #252833;\n  height: 81px;\n\n  .backlink {\n    margin-left: 10px;\n    margin-top: 7px;\n    position: absolute;\n    color: #686868;\n\n    &:hover {\n      text-decoration: none;\n      color: #a5a5a5;\n    }\n\n    .back-icon {\n      -webkit-transition: opacity 0.5s ease-in-out;\n      -moz-transition: opacity 0.5s ease-in-out;\n      transition: opacity 0.5s ease-in-out;\n      opacity: 0;\n\n      &.back-icon-shown {\n        opacity: 1;\n      }\n    }\n  }\n\n  .form-container {\n    margin-left: 180px;\n    margin-right: 72px;\n\n    .path-input-form {\n      width: 100%;\n      position: relative;\n\n      .form-control {\n        font-size: 1.7em;\n        width: 100%;\n      }\n\n      .add-to-repolist {\n        position: absolute;\n        right: 4px;\n        top: 6px;\n        background: transparent;\n        border: 0;\n        opacity: 0.3;\n\n        &:hover {\n          opacity: 0.8;\n        }\n      }\n    }\n  }\n\n  .arrow {\n    bottom: 0;\n    left: 300px;\n    border-bottom-color: @body-bg;\n  }\n\n  .version-and-license {\n    position: absolute;\n    right: 10px;\n    bottom: 2px;\n    font-size: 12px;\n  }\n}\n\n.toolbar {\n  position: absolute;\n  right: 10px;\n  top: 13px;\n}\n\n.headerLogo {\n  width: 100px;\n  height: auto;\n}\n\n.version {\n  padding-left: 140px;\n}\n"
  },
  {
    "path": "components/header/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"header\": \"header.html\"\n    },\n    \"javascript\": \"header.bundle.js\",\n    \"css\": \"header.css\"\n  }\n}\n"
  },
  {
    "path": "components/home/home.html",
    "content": "<div class=\"container home animated fadeInLeft\" data-bind=\"shown: shown\">\n  <div class=\"nux\" data-bind=\"visible: showNux\">\n    <img src=\"images/logo.png\" class=\"logo-large\" />\n    <div class=\"alert alert-info\">\n      <h4>Enter a path to a repository to get started!</h4>\n      Then press the <span data-bind=\"html: addIcon\"></span> symbol to make it show up here.\n    </div>\n  </div>\n  <div class=\"list-group\" data-bind=\"foreach: repos\">\n    <a\n      class=\"list-group-item repository\"\n      data-bind=\"attr: { href: link }, css: { 'path-removed': pathRemoved }\"\n    >\n      <span class=\"arrow-icon octicon-circled pull-left\" data-bind=\"html: arrowIcon\"></span>\n      <h4 class=\"list-group-item-heading\" data-bind=\"text: title\"></h4>\n      <p class=\"list-group-item-text\" data-bind=\"text: remote\"></p>\n      <button\n        type=\"button\"\n        class=\"btn btn-default list-item-remove\"\n        data-bind=\"html: removeIcon, click: remove\"\n      ></button>\n    </a>\n  </div>\n</div>\n"
  },
  {
    "path": "components/home/home.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst { encodePath } = require('ungit-address-parser');\n\ncomponents.register('home', (args) => new HomeViewModel(args.app));\n\nclass HomeRepositoryViewModel {\n  constructor(home, path) {\n    this.home = home;\n    this.app = home.app;\n    this.server = this.app.server;\n    this.path = path;\n    this.title = path;\n    this.link = `${ungit.config.rootPath}/#/repository?path=${encodePath(path)}`;\n    this.pathRemoved = ko.observable(false);\n    this.remote = ko.observable('...');\n    this.updateState();\n    this.removeIcon = octicons.x.toSVG({ height: 18 });\n    this.arrowIcon = octicons['arrow-right'].toSVG({ height: 24 });\n  }\n\n  updateState() {\n    this.server\n      .getPromise(`/fs/exists?path=${encodePath(this.path)}`)\n      .then((exists) => {\n        this.pathRemoved(!exists);\n      })\n      .catch((e) => this.server.unhandledRejection(e));\n    this.server\n      .getPromise(`/remotes/origin?path=${encodePath(this.path)}`)\n      .then((remote) => {\n        this.remote(remote.address.replace(/\\/\\/.*?@/, '//***@'));\n      })\n      .catch(() => {\n        this.remote('');\n      });\n  }\n\n  remove() {\n    this.app.repoList.remove(this.path);\n    this.home.update();\n  }\n}\n\nclass HomeViewModel {\n  constructor(app) {\n    this.app = app;\n    this.repos = ko.observableArray();\n    this.showNux = ko.computed(() => this.repos().length == 0);\n    this.addIcon = octicons.plus.toSVG({ height: 18 });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('home', this, {}, parentElement);\n  }\n\n  shown() {\n    this.update();\n  }\n\n  update() {\n    const reposByPath = {};\n    this.repos().forEach((repo) => {\n      reposByPath[repo.path] = repo;\n    });\n    this.repos(\n      this.app\n        .repoList()\n        .sort()\n        .map((path) => {\n          if (!reposByPath[path]) reposByPath[path] = new HomeRepositoryViewModel(this, path);\n          return reposByPath[path];\n        })\n    );\n  }\n  get template() {\n    return 'home';\n  }\n}\n"
  },
  {
    "path": "components/home/home.less",
    "content": ".home {\n  .nux {\n    text-align: center;\n\n    .logo-large {\n      margin-top: 20px;\n      margin-bottom: 50px;\n    }\n  }\n\n  .repository {\n    position: relative;\n    min-height: 62px;\n\n    &.path-removed {\n      .list-group-item-heading {\n        color: #cf5353;\n      }\n    }\n\n    .arrow-icon {\n      color: #686868;\n    }\n\n    &:hover .arrow-icon {\n      color: #a5a5a5;\n    }\n  }\n}\n"
  },
  {
    "path": "components/home/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"home\": \"home.html\"\n    },\n    \"javascript\": \"home.bundle.js\",\n    \"css\": \"home.css\"\n  }\n}\n"
  },
  {
    "path": "components/imagediff/imagediff.html",
    "content": "<!-- ko if: isShowingDiffs -->\n<!-- ko if: state() == 'new' -->\n<div class=\"imageDiff img-added\">\n  <img data-bind=\"attr: { src: newImageSrc }\" class=\"img-responsive\" />\n</div>\n<!-- /ko -->\n<!-- ko if: state() == 'removed' -->\n<div class=\"imageDiff img-removed\">\n  <img data-bind=\"attr: { src: oldImageSrc }\" class=\"img-responsive\" />\n</div>\n<!-- /ko -->\n<!-- ko if: state() == 'changed' -->\n<div class=\"imageDiff\">\n  <div class=\"row\">\n    <div class=\"col-lg-5\">\n      <img\n        data-bind=\"event: {error: oldImageError }, attr: { src: oldImageSrc }\"\n        class=\"img-responsive\"\n      />\n    </div>\n    <div class=\"col-lg-2\">\n      <span class=\"visible-lg\" data-bind=\"html: rightArrowIcon\"></span>\n      <span class=\"hidden-lg\" data-bind=\"html: downArrowIcon\"></span>\n    </div>\n    <div class=\"col-lg-5\">\n      <img\n        data-bind=\"event: {error: newImageError }, attr: { src: newImageSrc }\"\n        class=\"img-responsive\"\n      />\n    </div>\n  </div>\n</div>\n<!-- /ko -->\n<!-- /ko -->\n"
  },
  {
    "path": "components/imagediff/imagediff.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst { encodePath } = require('ungit-address-parser');\n\ncomponents.register('imagediff', (args) => new ImageDiffViewModel(args));\n\nclass ImageDiffViewModel {\n  constructor(args) {\n    this.filename = args.filename;\n    this.oldFilename = args.oldFilename;\n    this.repoPath = args.repoPath;\n    this.isNew = ko.observable(false);\n    this.isRemoved = ko.observable(false);\n    this.sha1 = args.sha1;\n    this.state = ko.computed(() => {\n      if (this.isNew()) return 'new';\n      if (this.isRemoved()) return 'removed';\n      return 'changed';\n    });\n    const gitDiffURL = `${ungit.config.rootPath}/api/diff/image?path=${encodePath(\n      this.repoPath()\n    )}`;\n    this.oldImageSrc =\n      gitDiffURL + `&filename=${this.oldFilename}&version=${this.sha1 ? this.sha1 + '^' : 'HEAD'}`;\n    this.newImageSrc =\n      gitDiffURL + `&filename=${this.filename}&version=${this.sha1 ? this.sha1 : 'current'}`;\n    this.isShowingDiffs = args.isShowingDiffs;\n    this.rightArrowIcon = octicons['arrow-right'].toSVG({ height: 100 });\n    this.downArrowIcon = octicons['arrow-down'].toSVG({ height: 100 });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('imagediff', this, {}, parentElement);\n  }\n\n  invalidateDiff() {}\n\n  newImageError() {\n    this.isRemoved(true);\n  }\n\n  oldImageError() {\n    this.isNew(true);\n  }\n}\n"
  },
  {
    "path": "components/imagediff/imagediff.less",
    "content": ".imageDiff {\n  padding: 10px;\n  text-align: center;\n}\n\n.img-removed {\n  background-color: rgba(230, 70, 100, 0.2);\n}\n\n.img-added {\n  background-color: rgba(70, 230, 100, 0.2);\n}\n"
  },
  {
    "path": "components/imagediff/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"imagediff\": \"imagediff.html\"\n    },\n    \"javascript\": \"imagediff.bundle.js\",\n    \"css\": \"imagediff.css\"\n  }\n}\n"
  },
  {
    "path": "components/login/login.html",
    "content": "<div class=\"container\">\n  <div data-bind=\"visible: status() == 'loading'\"></div>\n\n  <div class=\"login col-lg-6\" data-bind=\"visible: status() == 'login'\">\n    <h1>Login</h1>\n    <!-- ko if: loginError -->\n    <div class=\"alert alert-danger loginError\" role=\"alert\" data-bind=\"text: loginError\"></div>\n    <!-- /ko -->\n    <form data-bind=\"submit: login\">\n      <div class=\"form-group\">\n        <label for=\"inputUsername\">Username</label>\n        <input\n          id=\"inputUsername\"\n          class=\"form-control\"\n          type=\"text\"\n          autocomplete=\"username\"\n          data-bind=\"value: username\"\n        />\n      </div>\n\n      <div class=\"form-group\">\n        <label for=\"inputPassword\">Password</label>\n        <input\n          id=\"inputPassword\"\n          class=\"form-control\"\n          type=\"password\"\n          autocomplete=\"current-password\"\n          data-bind=\"value: password\"\n        />\n      </div>\n\n      <button class=\"btn btn-primary\" type=\"submit\">Login</button>\n    </form>\n  </div>\n</div>\n"
  },
  {
    "path": "components/login/login.js",
    "content": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst signals = require('signals');\n\ncomponents.register('login', (args) => new LoginViewModel(args.server));\n\nclass LoginViewModel {\n  constructor(server) {\n    this.server = server;\n    this.loggedIn = new signals.Signal();\n    this.status = ko.observable('loading');\n    this.username = ko.observable();\n    this.password = ko.observable();\n    this.loginError = ko.observable();\n    this.server\n      .getPromise('/loggedin')\n      .then((status) => {\n        if (status.loggedIn) {\n          this.loggedIn.dispatch();\n          this.status('loggedIn');\n        } else {\n          this.status('login');\n        }\n      })\n      .catch(() => {});\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('login', this, {}, parentElement);\n  }\n\n  login() {\n    this.server\n      .postPromise('/login', { username: this.username(), password: this.password() })\n      .then(() => {\n        this.loggedIn.dispatch();\n        this.status('loggedIn');\n      })\n      .catch((err) => {\n        if (err.res.body.error) {\n          this.loginError(err.res.body.error);\n        } else {\n          this.server.unhandledRejection(err);\n        }\n      });\n  }\n}\n"
  },
  {
    "path": "components/login/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"login\": \"login.html\"\n    },\n    \"javascript\": \"login.bundle.js\"\n  }\n}\n"
  },
  {
    "path": "components/modals/formModal.html",
    "content": "<div class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <h4 class=\"modal-title\" data-bind=\"text: title\"></h4>\n      </div>\n      <!-- Autocomplete is off here because of https://github.com/FredrikNoren/ungit/issues/363 -->\n      <form class=\"form-horizontal\" data-bind=\"submit: submit\" autocomplete=\"off\">\n        <div class=\"modal-body\" data-bind=\"foreach: items\">\n          <div class=\"form-group\">\n            <label data-bind=\"text: name, attr: { for: name }\" class=\"col-lg-2 control-label\"></label>\n            <div class=\"col-lg-10\">\n              <input class=\"form-control\"\n                data-bind=\"value: value, attr: { id: name, type: type, autofocus: autoFocus }\" />\n            </div>\n          </div>\n        </div>\n        <div class=\"modal-footer\">\n          <button type=\"submit\" data-bind=\"click: submit, class: 'btn btn-primary'\">Submit</button>\n          <button type=\"button\" data-bind=\"click: close, visible: showCancel, class: 'btn btn-default'\">\n            Cancel\n          </button>\n        </div>\n      </form>\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "components/modals/forms.ts",
    "content": "import * as ko from 'knockout';\nimport { ModalViewModel, FormItems } from './modalBase';\ndeclare const ungit: any;\n\nungit.components.register(\n  'credentialsmodal',\n  (args: any) => new CredentialsModalViewModel(args.remote)\n);\nungit.components.register('addremotemodal', (arg: any) => new AddRemoteModalViewModel(arg.path));\nungit.components.register('addsubmodulemodal', (arg: any) => new AddSubmoduleModalViewModel(arg.path));\n\n/**\n * Form receives collection of user inputs, i.e. username, password and etc.\n */\nclass FormModalViewModel extends ModalViewModel {\n  items: Array<FormItems>\n  showCancel: boolean\n  template: string\n  constructor(title: string, taModalName: string, showCancel: boolean) {\n    super(title, taModalName);\n    this.items = [];\n    this.showCancel = showCancel;\n    this.template = 'formModal';\n  }\n\n  submit() {\n    this.close();\n  }\n}\n\nclass CredentialsModalViewModel extends FormModalViewModel {\n  constructor(remote: string) {\n    super(`Remote ${remote} requires authentication`, 'credentials-dialog', false);\n    this.items.push(new FormItems('Username', ko.observable(), 'text', true))\n    this.items.push(new FormItems('Password', ko.observable(), 'password', false))\n  }\n\n  submit() {\n    super.submit();\n    ungit.programEvents.dispatch({\n      event: 'request-credentials-response',\n      username: this.items[0].value(),\n      password: this.items[1].value(),\n    });\n  }\n}\n\nclass AddRemoteModalViewModel extends FormModalViewModel {\n  repoPath: string\n  constructor(path: string) {\n    super('Add new remote', 'add-remote', true);\n    this.repoPath = path;\n    this.items.push(new FormItems('Name', ko.observable(), 'text', true))\n    this.items.push(new FormItems('Url', ko.observable(), 'text', false))\n  }\n\n  async submit() {\n    super.submit();\n    try {\n      await ungit.server.postPromise(`/remotes/${encodeURIComponent(this.items[0].value())}`, {\n        path: this.repoPath,\n        url: this.items[1].value(),\n      });\n      ungit.programEvents.dispatch({ event: 'update-remote' });\n    } catch (e) {\n      ungit.server.unhandledRejection(e);\n    }\n  }\n}\n\nclass AddSubmoduleModalViewModel extends FormModalViewModel {\n  repoPath: string\n  constructor(path: string) {\n    super('Add new submodule', 'add-submodule', true);\n    this.repoPath = path;\n    this.items.push(new FormItems('Path', ko.observable(), 'text', true))\n    this.items.push(new FormItems('Url', ko.observable(), 'text', false))\n  }\n\n  async submit() {\n    super.submit();\n    try {\n      await ungit.server.postPromise('/submodules/add', {\n        path: this.repoPath,\n        submodulePath: this.items[0].value(),\n        submoduleUrl: this.items[1].value(),\n      });\n      ungit.programEvents.dispatch({ event: 'submodule-fetch' });\n    } catch (e) {\n      ungit.server.unhandledRejection(e);\n    }\n  }\n}\n"
  },
  {
    "path": "components/modals/modalBase.ts",
    "content": "declare const ungit: any;\n\nexport class ModalViewModel {\n  title: string\n  taModalName: string\n  timestamp = new Date().getTime()\n  constructor(title: string, taModalName: string) {\n    this.title = title;\n    this.taModalName = taModalName;\n  }\n\n  close() {\n    ungit.programEvents.dispatch({ event: 'modal-close-dialog', modal: this });\n  }\n}\n\nexport class FormItems {\n  name: string\n  value: ko.Observable\n  type: string\n  autoFocus: boolean\n  constructor(name: string, value: ko.Observable, type: string, autoFocus: boolean) {\n    this.name = name;\n    this.value = value;\n    this.type = type;\n    this.autoFocus = autoFocus;\n  }\n}\n\nexport class PromptOptions {\n  label: string\n  primary: boolean\n  taId: string\n  close: Function\n\n  constructor(label: string, primary: boolean, taId: string, close: Function) {\n    this.label = label;\n    this.primary = primary;\n    this.taId = taId;\n    this.close = close;\n  }\n}\n"
  },
  {
    "path": "components/modals/modals.ts",
    "content": "import './forms';\nimport './prompts';"
  },
  {
    "path": "components/modals/promptModal.html",
    "content": "<div class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" data-bind=\"attr: { 'data-ta-container': taModalName }\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\">\n      <div class=\"modal-header\">\n        <h4 class=\"modal-title\" data-bind=\"text: title\"></h4>\n      </div>\n      <div class=\"modal-body\">\n        <p data-bind=\"html: details\"></p>\n      </div>\n      <div class=\"modal-footer\">\n        <!-- ko foreach: promptOptions -->\n        <button type=\"button\"\n          data-bind=\"css: { 'btn-primary': primary }, attr: { 'data-ta-action': taId }, text: label, click: close, class: 'btn btn-default'\"></button>\n        <!-- /ko -->\n      </div>\n    </div>\n  </div>\n</div>"
  },
  {
    "path": "components/modals/prompts.ts",
    "content": "import * as ko from 'knockout';\nimport { ModalViewModel, PromptOptions } from './modalBase';\ndeclare const ungit: any;\n\nungit.components.register('yesnomodal', (args: any) => new YesNoModalViewModel(args.title, args.details, args.closeFunc));\nungit.components.register(\n  'yesnomutemodal',\n  (args: any) => new YesNoMuteModalViewModel(args.title, args.details, args.closeFunc)\n);\nungit.components.register(\n  'toomanyfilesmodal',\n  (args: any) => new TooManyFilesModalViewModel(args.title, args.details, args.closeFunc)\n);\nungit.components.register('texteditmodal', (args: any) => new TextEditModal(args.title, args.content, args.closeFunc));\n\n\n/**\n * Prompt's receives decisions from users, such as 'yes' or 'no', based on \n * button clicks.\n */\nclass PromptModalViewModel extends ModalViewModel {\n  promptOptions: Array<PromptOptions>\n  details: ko.Observable\n  template: string\n  closeFunc: Function\n  constructor(title: string, taModalName: string, details: string, closeFunc: Function) {\n    super(title, taModalName);\n    this.promptOptions = [];\n    this.details = ko.observable(details);\n    this.template = 'promptModal';\n    this.closeFunc = closeFunc;\n  }\n\n  close(isYes: boolean = false, isMute: boolean = false) {\n    this.closeFunc(isYes, isMute);\n    super.close();\n  }\n\n  closeYes() {\n    this.close(true);\n  }\n\n  closeYesMute() {\n    this.close(true, true);\n  }\n\n  closeNo() {\n    this.close();\n  }\n}\n\nclass YesNoModalViewModel extends PromptModalViewModel {\n  constructor(title: string, details: string, closeFunc: Function) {\n    super(title, 'yes-no-modal', details, closeFunc);\n    this.promptOptions.push(new PromptOptions('Yes', true, 'yes', this.closeYes.bind(this)));\n    this.promptOptions.push(new PromptOptions('No', false, 'no', this.closeNo.bind(this)));\n  }\n}\n\nclass YesNoMuteModalViewModel extends PromptModalViewModel {\n  constructor(title: string, details: string, closeFunc: Function) {\n    super(title, 'yes-no-mute-modal', details, closeFunc);\n    this.promptOptions.push(new PromptOptions('Yes', true, 'yes', this.closeYes.bind(this)));\n    this.promptOptions.push(new PromptOptions('Yes and mute for awhile', false, 'mute', this.closeYesMute.bind(this)));\n    this.promptOptions.push(new PromptOptions('No', false, 'no', this.closeNo.bind(this)));\n  }\n}\n\nclass TooManyFilesModalViewModel extends PromptModalViewModel {\n  constructor(title: string, details: string, closeFunc: Function) {\n    super(title, 'yes-no-modal', details, closeFunc);\n    this.promptOptions.push(new PromptOptions(`Don't load`, true, 'noLoad', this.closeYes.bind(this)));\n    this.promptOptions.push(new PromptOptions(`Load anyway`, false, 'loadAnyway', this.closeNo.bind(this)));\n  }\n}\n\nclass TextEditModal extends PromptModalViewModel {\n  constructor(title: string, details: string, closeFunc: Function) {\n    super(\n      title,\n      'text-edit-modal',\n      `<textarea class=\"text-area-content form-control\" spellcheck=\"false\" style=\"height: 250px; width: 100%; font-family: monospace; resize: vertical;\">${details}</textarea>`,\n      closeFunc\n    );\n    this.promptOptions.push(new PromptOptions('Save', true, 'save', this.closeYes.bind(this)));\n    this.promptOptions.push(new PromptOptions('Cancel', false, 'cancel', this.closeNo.bind(this)));\n  }\n}\n"
  },
  {
    "path": "components/modals/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"formModal\": \"formModal.html\",\n      \"promptModal\": \"promptModal.html\"\n    },\n    \"javascript\": \"modals.bundle.js\"\n  }\n}"
  },
  {
    "path": "components/path/path.html",
    "content": "<div class=\"path\" data-bind=\"shown: shown\">\n  <!-- ko if: status() == 'uninited' -->\n  <div class=\"uninited container\">\n    <div class=\"alert alert-info\" data-bind=\"visible: showDirectoryCreatedAlert\">\n      Directory \"<span data-bind=\"text: dirName\"></span>\" created.\n    </div>\n\n    <div class=\"row create-repo-container panel\">\n      <h1>\"<span data-bind=\"text: dirName\"></span>\" is not a git repository</h1>\n      <p>There is no git repository at the selected path.</p>\n      <div class=\"create-repo-toggle\" data-bind=\"click: toggleShowCreateRepo\">\n        <span class=\"expand-icon\" data-bind=\"html: expandIcon\"></span>\n      </div>\n\n      <div data-bind=\"visible: isShowCreateRepo\">\n        <div class=\"col-lg-6\">\n          <div class=\"panel panel-default\">\n            <div class=\"panel-heading\">\n              Create a new git repository in \"<span data-bind=\"text: dirName\"></span>\"\n            </div>\n            <div class=\"panel-body\">\n              <button class=\"btn btn-primary btn-lg\" data-bind=\"click: initRepository\">\n                Create Repository\n              </button>\n            </div>\n          </div>\n        </div>\n        <div class=\"col-lg-6\">\n          <div class=\"panel panel-default\">\n            <div class=\"panel-heading\">\n              Clone a git repository into a subfolder of \"<span data-bind=\"text: dirName\"></span>\"\n            </div>\n            <div class=\"panel-body\">\n              <form data-bind=\"submit: cloneRepository\">\n                <div class=\"form-group\">\n                  <label for=\"cloneFromInput\">Clone from</label>\n                  <input class=\"form-control\" id=\"cloneFromInput\" type=\"text\" placeholder=\"URL\"\n                    data-bind=\"value: cloneUrl, valueUpdate: 'afterkeydown'\" required />\n                </div>\n                <div class=\"form-group\">\n                  <label for=\"cloneToInput\">into</label>\n                  <input class=\"form-control\" id=\"cloneToInput\" type=\"text\"\n                    data-bind=\"value: cloneDestination, attr: { placeholder: cloneDestinationImplicit }\" />\n                </div>\n                <div class=\"form-group\">\n                  <label>\n                    <input type=\"checkbox\" data-bind=\"checked: isRecursiveSubmodule\" />\n                    Recurse submodules\n                  </label>\n                </div>\n                <button class=\"btn btn-primary btn-lg\" type=\"submit\">Clone Repository</button>\n              </form>\n            </div>\n          </div>\n        </div>\n      </div>\n    </div>\n\n    <div class=\"list-group\" data-bind=\"foreach: subRepos\">\n      <a class=\"list-group-item repository\" data-bind=\"attr: { href: link }\">\n        <span class=\"arrow-icon octicon-circled pull-left\" data-bind=\"html: arrowIcon\"></span>\n        <h4 class=\"list-group-item-heading\" data-bind=\"text: title\"></h4>\n        <p class=\"list-group-item-text\" data-bind=\"text: remote\"></p>\n      </a>\n    </div>\n  </div>\n  <!-- /ko -->\n\n  <!-- ko if: status() == 'no-such-path' -->\n  <div class=\"invalid-path container\">\n    <h1>Invalid path</h1>\n    <p>\"<span data-bind=\"text: repoPath\"></span>\" doesn't seem to be a valid path.</p>\n    <div class=\"create-dir\">\n      <button class=\"btn btn-primary btn-lg\" data-bind=\"click: createDir\">Create Directory</button>\n    </div>\n  </div>\n  <!-- /ko -->\n\n  <!-- ko if: repository -->\n  <!-- ko component: repository -->\n  <!-- /ko -->\n  <!-- /ko -->\n</div>"
  },
  {
    "path": "components/path/path.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst addressParser = require('ungit-address-parser');\nconst navigation = require('ungit-navigation');\nconst programEvents = require('ungit-program-events');\nconst { encodePath } = require('ungit-address-parser');\nconst storage = require('ungit-storage');\nconst { ComponentRoot } = require('../ComponentRoot');\nconst showCreateRepoKey = 'isShowCreateRepo';\n\ncomponents.register('path', (args) => {\n  return new PathViewModel(args.server, args.path);\n});\n\nclass SubRepositoryViewModel {\n  constructor(server, path) {\n    this.path = path;\n    this.title = path;\n    this.link = `${ungit.config.rootPath}/#/repository?path=${encodePath(path)}`;\n    this.arrowIcon = octicons['arrow-right'].toSVG({ height: 24 });\n    this.remote = ko.observable('...');\n\n    server\n      .getPromise(`/remotes/origin?path=${encodePath(this.path)}`)\n      .then((remote) => {\n        this.remote(remote.address.replace(/\\/\\/.*?@/, '//***@'));\n      })\n      .catch(() => {\n        this.remote('');\n      });\n  }\n}\n\nclass PathViewModel extends ComponentRoot {\n  constructor(server, path) {\n    super();\n    this.server = server;\n    this.repoPath = ko.observable(path);\n    this.dirName =\n      this.repoPath()\n        .replace(/\\\\/g, '/')\n        .split('/')\n        .filter((s) => s)\n        .slice(-1)[0] || '/';\n    this.status = ko.observable('loading');\n    this.cloneUrl = ko.observable();\n    this.showDirectoryCreatedAlert = ko.observable(false);\n    this.subRepos = ko.observableArray();\n    this.cloneDestinationImplicit = ko.computed(() => {\n      const defaultText = 'destination folder';\n      if (!this.cloneUrl()) return defaultText;\n\n      const parsedAddress = addressParser.parseAddress(this.cloneUrl());\n      return parsedAddress.shortProject || defaultText;\n    });\n    this.cloneDestination = ko.observable();\n    this.repository = ko.observable();\n    this.expandIcon = ko.observable();\n    this.isRecursiveSubmodule = ko.observable(true);\n    this.showCreateRepoKey = `${showCreateRepoKey}-${this.repoPath()}`;\n    const storageValue = storage.getItem(this.showCreateRepoKey);\n    this.isShowCreateRepo = ko.observable(storageValue && storageValue === 'false' ? false : true);\n    this.updateShowCreateRepoMetadata();\n  }\n\n  toggleShowCreateRepo() {\n    this.isShowCreateRepo(!this.isShowCreateRepo());\n    storage.setItem(this.showCreateRepoKey, this.isShowCreateRepo() ? 'true' : 'false');\n    this.updateShowCreateRepoMetadata();\n  }\n\n  updateShowCreateRepoMetadata() {\n    if (this.isShowCreateRepo()) {\n      this.expandIcon(octicons['chevron-right'].toSVG({ height: 28 }));\n    } else {\n      this.expandIcon(octicons['chevron-down'].toSVG({ height: 35 }));\n    }\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('path', this, {}, parentElement);\n  }\n  shown() {\n    this.updateStatus();\n  }\n  updateAnimationFrame(deltaT) {\n    if (this.repository()) this.repository().updateAnimationFrame(deltaT);\n  }\n  async updateStatus() {\n    ungit.logger.debug('path.updateStatus() triggered');\n    const status = await this.server.getPromise('/quickstatus', { path: this.repoPath() });\n    try {\n      if (this.isSamePayload(status)) {\n        return;\n      }\n\n      if (status.type == 'inited' || status.type == 'bare') {\n        if (this.repoPath() !== status.gitRootPath) {\n          this.repoPath(status.gitRootPath);\n          programEvents.dispatch({ event: 'navigated-to-path', path: this.repoPath() });\n          programEvents.dispatch({ event: 'working-tree-changed' });\n        }\n        this.status(status.type);\n        if (!this.repository()) {\n          this.repository(components.create('repository', { server: this.server, path: this }));\n        }\n      } else if (status.type == 'uninited' || status.type == 'no-such-path') {\n        if (status.subRepos && status.subRepos.length > 0) {\n          this.subRepos(\n            status.subRepos.map((subRepo) => new SubRepositoryViewModel(this.server, subRepo))\n          );\n        }\n        this.status(status.type);\n        this.repository(null);\n      }\n    } catch (err) {\n      ungit.logger.debug('path.updateStatus() errored', err);\n    } finally {\n      ungit.logger.debug('path.updateStatus() finished');\n    }\n  }\n  initRepository() {\n    return this.server\n      .postPromise('/init', { path: this.repoPath() })\n      .catch((e) => this.server.unhandledRejection(e))\n      .finally(() => this.updateStatus());\n  }\n  async onProgramEvent(event) {\n    const promises = [];\n    if (event.event == 'working-tree-changed' || event.event == 'request-app-content-refresh') {\n      promises.push(this.updateStatus());\n    }\n\n    if (this.repository()) {\n      promises.push(this.repository().onProgramEvent(event));\n    }\n\n    await Promise.all(promises);\n  }\n  cloneRepository() {\n    this.status('cloning');\n    const dest = this.cloneDestination() || this.cloneDestinationImplicit();\n\n    return this.server\n      .postPromise('/clone', {\n        path: this.repoPath(),\n        url: this.cloneUrl(),\n        destinationDir: dest,\n        isRecursiveSubmodule: this.isRecursiveSubmodule(),\n      })\n      .then((res) => navigation.browseTo('repository?path=' + addressParser.encodePath(res.path)))\n      .catch((e) => this.server.unhandledRejection(e))\n      .finally(() => {\n        programEvents.dispatch({ event: 'working-tree-changed' });\n      });\n  }\n  createDir() {\n    this.showDirectoryCreatedAlert(true);\n    return this.server\n      .postPromise('/createDir', { dir: this.repoPath() })\n      .catch((e) => this.server.unhandledRejection(e))\n      .then(() => this.updateStatus());\n  }\n}\n"
  },
  {
    "path": "components/path/path.less",
    "content": ".create-dir {\n  margin-top: 50px;\n}\n\n.create-repo-container {\n  background-color: #643a44;\n  box-shadow: 0 -1px 15px #252833;\n  padding: 5px 10px 5px 10px;\n}\n\n.create-repo-toggle {\n  float: right;\n  margin-top: -75px;\n  margin-right: 10px;\n}\n"
  },
  {
    "path": "components/path/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"path\": \"path.html\"\n    },\n    \"javascript\": \"path.bundle.js\",\n    \"css\": \"path.css\"\n  }\n}\n"
  },
  {
    "path": "components/refreshbutton/refreshbutton.html",
    "content": "<button\n  class=\"btn btn-default refresh-button\"\n  data-bind=\"html: refreshIcon, css: { 'btn-lg': isLarge }, click: refresh\"\n  data-toggle=\"tooltip\"\n  data-placement=\"bottom\"\n  title=\"Refresh changes\"\n></button>\n"
  },
  {
    "path": "components/refreshbutton/refreshbutton.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\n\ncomponents.register('refreshbutton', (args) => new RefreshButton(args.isLarge));\n\nclass RefreshButton {\n  constructor(isLarge) {\n    this.isLarge = isLarge;\n    this.refreshIcon = octicons.sync.toSVG({ height: isLarge ? 26 : 18 });\n  }\n\n  refresh() {\n    programEvents.dispatch({ event: 'request-app-content-refresh' });\n    return true;\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('refreshbutton', this, {}, parentElement);\n  }\n}\n"
  },
  {
    "path": "components/refreshbutton/refreshbutton.less",
    "content": ".toolbar {\n  .refresh-button {\n    background: rgba(0, 0, 0, 0.1);\n    border: 0;\n  }\n}\n"
  },
  {
    "path": "components/refreshbutton/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"refreshbutton\": \"refreshbutton.html\"\n    },\n    \"javascript\": \"refreshbutton.bundle.js\",\n    \"css\": \"refreshbutton.css\"\n  }\n}\n"
  },
  {
    "path": "components/remotes/remotes.html",
    "content": "<div class=\"btn-group fetchButton\">\n  <button\n    type=\"button\"\n    class=\"btn btn-default btn-main\"\n    data-bind=\"click: clickFetch, enable: fetchEnabled\"\n  >\n    <span data-bind=\"html: remotesIcon\"></span>\n    <span data-bind=\"text: fetchLabel\"></span>\n  </button>\n  <button\n    type=\"button\"\n    class=\"btn btn-default dropdown-toggle\"\n    data-toggle=\"dropdown\"\n    aria-haspopup=\"true\"\n    aria-expanded=\"false\"\n  >\n    <span class=\"caret\"></span>\n    <span class=\"sr-only\">Toggle Remote List</span>\n  </button>\n  <ul class=\"dropdown-menu dropdown-menu-right\" role=\"menu\">\n    <!-- ko foreach: remotes -->\n    <li>\n      <a\n        class=\"linked-remove\"\n        href=\"#\"\n        data-bind=\"text: name, click: changeRemote, attr: { 'data-ta-clickable': name, title: title }\"\n      ></a>\n      <a\n        class=\"list-link list-remove\"\n        href=\"#\"\n        data-bind=\"html: $parent.closeIcon, click: $parent.remoteRemove.bind($parent), attr: { 'data-ta-clickable': name + '-remove' }\"\n      ></a>\n    </li>\n    <!-- /ko -->\n    <!-- ko if: remotes().length -->\n    <li class=\"divider\" role=\"separator\"></li>\n    <!-- /ko -->\n    <li>\n      <a href=\"#\" class=\"add-new-remote\" data-bind=\"click: showAddRemoteDialog\">Add a new remote</a>\n    </li>\n  </ul>\n</div>\n"
  },
  {
    "path": "components/remotes/remotes.js",
    "content": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\n\ncomponents.register('remotes', (args) => new RemotesViewModel(args.server, args.repoPath));\n\nclass RemotesViewModel {\n  constructor(server, repoPath) {\n    this.repoPath = repoPath;\n    this.server = server;\n    this.remotes = ko.observable([]);\n    this.currentRemote = ko.observable(null);\n    this.currentRemote.subscribe((value) => {\n      programEvents.dispatch({ event: 'current-remote-changed', newRemote: value });\n    });\n    this.fetchLabel = ko.computed(() => {\n      if (this.currentRemote()) return `Fetch from ${this.currentRemote()}`;\n      else return 'No remotes specified';\n    });\n    this.remotesIcon = octicons.download.toSVG({ height: 18 });\n    this.closeIcon = octicons.x.toSVG({ height: 18 });\n\n    this.fetchEnabled = ko.computed(() => this.remotes().length > 0);\n\n    this.shouldAutoFetch = ungit.config.autoFetch;\n    this.updateRemotes();\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('remotes', this, {}, parentElement);\n  }\n\n  clickFetch() {\n    this.fetch({ nodes: true, tags: true });\n  }\n\n  async onProgramEvent(event) {\n    if (event.event === 'request-app-content-refresh' || event.event === 'request-fetch-tags') {\n      await this.fetch({ tags: true });\n    } else if (event.event === 'git-directory-changed' && this.shouldAutoFetch) {\n      await this.fetch({ tags: true });\n    } else if (event.event === 'update-remote') {\n      await this.updateRemotes();\n    }\n  }\n\n  async fetch(options) {\n    if (!this.currentRemote()) return;\n    ungit.logger.debug('remotes.fetch() triggered');\n\n    try {\n      const tagPromise = options.tags\n        ? this.server.getPromise('/remote/tags', {\n            path: this.repoPath(),\n            remote: this.currentRemote(),\n          })\n        : null;\n      const fetchPromise = options.nodes\n        ? this.server.getPromise('/fetch', { path: this.repoPath(), remote: this.currentRemote() })\n        : null;\n\n      if (tagPromise) {\n        programEvents.dispatch({ event: 'remote-tags-update', tags: await tagPromise });\n      }\n      if (fetchPromise) {\n        await fetchPromise;\n      }\n      if (!this.server.isInternetConnected) {\n        this.server.isInternetConnected = true;\n      }\n    } catch (err) {\n      let errorMessage;\n      let stdout;\n      let stderr;\n      try {\n        errorMessage = `Ungit has failed to fetch a remote.  ${err.res.body.error}`;\n        stdout = err.res.body.stdout;\n        stderr = err.res.body.stderr;\n      } catch {\n        errorMessage = '';\n      }\n\n      if (errorMessage.includes('Could not resolve host')) {\n        if (this.server.isInternetConnected) {\n          this.server.isInternetConnected = false;\n          stdout = '';\n          stderr = '';\n        } else {\n          // Message is already seen, just return\n          return;\n        }\n      }\n\n      programEvents.dispatch({\n        event: 'git-error',\n        data: {\n          isWarning: true,\n          command: err.res.body.command,\n          error: err.res.body.error,\n          stdout,\n          stderr,\n          repoPath: err.res.body.workingDirectory,\n        },\n      });\n    } finally {\n      ungit.logger.debug('remotes.fetch() finished');\n    }\n  }\n\n  updateRemotes() {\n    return this.server\n      .getPromise('/remotes', { path: this.repoPath() })\n      .then((remotes) => {\n        remotes = remotes.map((remote) => ({\n          name: remote.name,\n          title:\n            remote.fetchUrl == remote.pushUrl\n              ? `Fetch/Push ${remote.fetchUrl || remote.pushUrl || remote.url}`\n              : `Fetch ${remote.fetchUrl || remote.url}\\nPush ${remote.pushUrl || remote.url}`,\n          changeRemote: () => {\n            this.currentRemote(remote.name);\n          },\n        }));\n        this.remotes(remotes);\n        if (!this.currentRemote() && remotes.length > 0) {\n          if (_.find(remotes, { name: 'origin' })) {\n            // default to origin if it exists\n            this.currentRemote('origin');\n          } else {\n            // otherwise take the first one\n            this.currentRemote(remotes[0].name);\n          }\n\n          if (this.shouldAutoFetch) {\n            this.shouldAutoFetch = false;\n            return this.fetch({ nodes: true, tags: true });\n          }\n        }\n      })\n      .catch((err) => {\n        if (err.errorCode != 'not-a-repository') {\n          this.server.unhandledRejection(err);\n        } else {\n          ungit.logger.warn('updateRemotes failed', err);\n        }\n      });\n  }\n\n  showAddRemoteDialog() {\n    components.showModal('addremotemodal', { path: this.repoPath() });\n  }\n\n  remoteRemove(remote) {\n    components.showModal('yesnomodal', {\n      title: 'Are you sure?',\n      details: `Deleting ${remote.name} remote cannot be undone with ungit.`,\n      closeFunc: (isYes) => {\n        if (isYes) {\n          this.server\n            .delPromise(`/remotes/${remote.name}`, { path: this.repoPath() })\n            .then(() => {\n              this.updateRemotes();\n            })\n            .catch((e) => this.server.unhandledRejection(e));\n        }\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "components/remotes/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"remotes\": \"remotes.html\"\n    },\n    \"javascript\": \"remotes.bundle.js\"\n  }\n}\n"
  },
  {
    "path": "components/repository/repository.html",
    "content": "<div class=\"repository-view animated fadeInLeft\" data-bind=\"attr: { style: 'tab-size: ' + ungit.config.tabSize }\">\n  <!-- ko component: gitErrors --><!-- /ko -->\n\n  <!-- ko if: isSubmodule -->\n  <div class=\"submodule alert alert-warning\">\n    <h4>This is a submodule</h4>\n    Base repository: <a data-bind=\"text: parentModulePath, attr: { href: parentModuleLink}\"></a>\n  </div>\n  <!-- /ko -->\n\n  <!-- ko component: stash --><!-- /ko -->\n\n  <!-- ko component: staging --><!-- /ko -->\n\n  <!-- ko if: staging.conflictText -->\n  <h2 class=\"text-muted\">\n    <span data-bind=\"text: staging.conflictText\" /> in progress\n    <small>resolve conflicts to continue</small>\n  </h2>\n  <!-- /ko -->\n\n  <!-- ko if: showLog -->\n\n  <div class=\"repository-actions\">\n    <!-- ko if: refreshButton -->\n    <!-- ko component: refreshButton --><!-- /ko -->\n    <!-- /ko -->\n    <!-- ko component: remotes --><!-- /ko -->\n    <!-- ko component: submodules --><!-- /ko -->\n    <!-- ko component: branches --><!-- /ko -->\n\n    <div class=\"btn-group branch\">\n      <button type=\"button\" class=\"btn btn-default btn-main\" data-bind=\"click: editGitignore\">\n        <span data-bind=\"html: ignoreIcon\"></span>\n        <span>.gitignore</span>\n      </button>\n    </div>\n  </div>\n\n  <!-- ko component: graph --><!-- /ko -->\n\n  <!-- /ko -->\n</div>\n"
  },
  {
    "path": "components/repository/repository.js",
    "content": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\nconst { encodePath } = require('ungit-address-parser');\n\ncomponents.register('repository', (args) => new RepositoryViewModel(args.server, args.path));\n\nclass RepositoryViewModel {\n  constructor(server, path) {\n    this.server = server;\n    this.isBareDir = path.status() === 'bare';\n    this.repoPath = path.repoPath;\n    this.gitErrors = components.create('gitErrors', { server, repoPath: this.repoPath });\n    this.graph = components.create('graph', { server, repoPath: this.repoPath });\n    this.remotes = components.create('remotes', { server, repoPath: this.repoPath });\n    this.submodules = components.create('submodules', { server, repoPath: this.repoPath });\n    this.stash = this.isBareDir\n      ? {}\n      : components.create('stash', { server, repoPath: this.repoPath });\n    this.staging = this.isBareDir\n      ? {}\n      : components.create('staging', { server, repoPath: this.repoPath, graph: this.graph });\n    this.branches = components.create('branches', {\n      server,\n      graph: this.graph,\n      repoPath: this.repoPath,\n    });\n    this.repoPath.subscribe((value) => {\n      this.server.watchRepository(value);\n    });\n    this.server.watchRepository(this.repoPath());\n    this.showLog = this.isBareDir ? ko.observable(true) : this.staging.isStageValid;\n    this.parentModulePath = ko.observable();\n    this.parentModuleLink = ko.observable();\n    this.isSubmodule = ko.computed(() => this.parentModulePath() && this.parentModuleLink());\n    this.refreshSubmoduleStatus();\n    if (window.location.search.includes('noheader=true')) {\n      this.refreshButton = components.create('refreshbutton', { isLarge: false });\n    } else {\n      this.refreshButton = false;\n    }\n    this.ignoreIcon = octicons.file.toSVG({ height: 18 });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('repository', this, {}, parentElement);\n  }\n\n  onProgramEvent(event) {\n    if (this.gitErrors.onProgramEvent) this.gitErrors.onProgramEvent(event);\n    if (this.graph.onProgramEvent) this.graph.onProgramEvent(event);\n    if (this.staging.onProgramEvent) this.staging.onProgramEvent(event);\n    if (this.stash.onProgramEvent) this.stash.onProgramEvent(event);\n    if (this.remotes.onProgramEvent) this.remotes.onProgramEvent(event);\n    if (this.submodules.onProgramEvent) this.submodules.onProgramEvent(event);\n    if (this.branches.onProgramEvent) this.branches.onProgramEvent(event);\n    if (event.event == 'connected') this.server.watchRepository(this.repoPath());\n\n    // If we get a reconnect event it's usually because the server crashed and then restarted\n    // or something like that, so we need to tell it to start watching the path again\n  }\n\n  updateAnimationFrame(deltaT) {\n    if (this.graph.updateAnimationFrame) this.graph.updateAnimationFrame(deltaT);\n  }\n\n  refreshSubmoduleStatus() {\n    return this.server\n      .getPromise('/baserepopath', { path: this.repoPath() })\n      .then((baseRepoPath) => {\n        if (baseRepoPath.path) {\n          return this.server\n            .getPromise('/submodules', { path: baseRepoPath.path })\n            .then((submodules) => {\n              const baseName = this.repoPath().substring(baseRepoPath.path.length + 1);\n              for (let n = 0; n < submodules.length; n++) {\n                if (submodules[n].path === baseName) {\n                  this.parentModulePath(baseRepoPath.path);\n                  this.parentModuleLink(`/#/repository?path=${encodePath(baseRepoPath.path)}`);\n                  return;\n                }\n              }\n            });\n        }\n      })\n      .catch(() => {\n        this.parentModuleLink(undefined);\n        this.parentModulePath(undefined);\n      });\n  }\n\n  editGitignore() {\n    return this.server\n      .getPromise('/gitignore', { path: this.repoPath() })\n      .then((res) => {\n        return components.showModal('texteditmodal', {\n          title: `${this.repoPath()}${ungit.config.fileSeparator}.gitignore`,\n          content: res.content,\n          closeFunc: (isYes) => {\n            if (isYes) {\n              this.server.putPromise('/gitignore', {\n                path: this.repoPath(),\n                data: document.querySelector('.modal-body .text-area-content').value,\n              });\n            }\n          },\n        });\n      })\n      .catch((e) => {\n        // Not a git error but we are going to treat like one\n        programEvents.dispatch({\n          event: 'git-error',\n          data: {\n            command: `fs.write \"${this.repoPath()}${ungit.config.fileSeparator}.gitignore\"`,\n            error: e.message || e.errorSummary,\n            stdout: '',\n            stderr: e.stack,\n            repoPath: this.repoPath(),\n          },\n        });\n      });\n  }\n}\n"
  },
  {
    "path": "components/repository/repository.less",
    "content": ".repository-view {\n  position: relative;\n  height: auto;\n  margin-bottom: 1px;\n  padding-bottom: 1px;\n\n  .repository-actions {\n    position: absolute;\n    margin-top: 20px;\n    right: 0;\n    z-index: 30;\n  }\n}\n"
  },
  {
    "path": "components/repository/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"repository\": \"repository.html\"\n    },\n    \"javascript\": \"repository.bundle.js\",\n    \"css\": \"repository.css\"\n  }\n}\n"
  },
  {
    "path": "components/staging/staging.html",
    "content": "<div\n  class=\"staging panel panel-default\"\n  data-bind=\"css: { commitValidationError: commitValidationError }\"\n>\n  <div class=\"panel-body\">\n    <div class=\"arrow\"></div>\n    <div data-bind=\"visible: showNux\" class=\"nux\">\n      Nothing to commit.\n      <a class=\"amend-link\" href=\"#\" data-bind=\"click: toggleAmend, visible: canAmend\"\n        >Amend previous commit?</a\n      >\n      <a\n        class=\"empty-commit-link\"\n        href=\"#\"\n        data-bind=\"click: toggleEmptyCommit, visible: canEmptyCommit\"\n        >Create an empty commit?</a\n      >\n    </div>\n    <div class=\"row\" data-bind=\"visible: !showNux()\">\n      <div class=\"col-lg-3\">\n        <input\n          class=\"form-control\"\n          type=\"text\"\n          placeholder=\"Title (required)\"\n          aria-label=\"Commit message title\"\n          data-bind=\"value: commitMessageTitle, valueUpdate: 'afterkeydown', enable: !inRebase(), event: {keypress: onEnter}\"\n        />\n        <textarea\n          class=\"form-control commit-body\"\n          rows=\"2\"\n          placeholder=\"Body\"\n          aria-label=\"Commit message body\"\n          data-bind=\"value: commitMessageBody, valueUpdate: 'afterkeydown', enable: !inRebase(), event: {keypress: onAltEnter}\"\n        ></textarea>\n        <div>\n          <button\n            class=\"amend-button btn btn-link\"\n            data-bind=\"visible: canAmend, click: toggleAmend\"\n          >\n            <div class=\"checkmark\" data-bind=\"css: { checked: amend }\">\n              <span\n                class=\"glyphicon\"\n                data-bind=\"css: { 'glyphicon-check': amend, 'glyphicon-unchecked': !amend() }\"\n              ></span>\n            </div>\n            <span>Amend last commit</span>\n          </button>\n          <span class=\"commit-message-title-counter\" data-bind=\"text: commitMessageTitleCount\" />\n        </div>\n        <div class=\"btn-group commit-grp\" data-bind=\"visible: isStageValid\">\n          <button\n            class=\"btn btn-primary btn-lg commit-btn\"\n            data-bind=\"click: commit, enable: !commitValidationError()\"\n          >\n            Commit\n          </button>\n          <button\n            type=\"button\"\n            class=\"btn btn-primary btn-lg dropdown-toggle\"\n            data-toggle=\"dropdown\"\n            data-bind=\"enable: !commitValidationError()\"\n            aria-haspopup=\"true\"\n            aria-expanded=\"false\"\n          >\n            <span class=\"caret\"></span>\n            <span class=\"sr-only\">Toggle Commit Options</span>\n          </button>\n          <ul class=\"dropdown-menu\" role=\"menu\">\n            <li>\n              <a\n                href=\"#\"\n                data-bind=\"click: commitnpush, css: { disabled: !canPush() }\"\n                class=\"commitnpush\"\n                >Commit & Push</a\n              >\n            </li>\n          </ul>\n        </div>\n        <button\n          class=\"btn btn-warning btn-lg btn-stg-cancel\"\n          data-bind=\"click: cancelAmendEmpty, visible: showCancelButton\"\n        >\n          Cancel\n        </button>\n        <button\n          class=\"btn btn-primary\"\n          data-bind=\"click: conflictContinue, visible: conflictText, enable: !commitValidationError()\"\n        >\n          Continue <span data-bind=\"text: conflictText\" />\n        </button>\n        <button\n          class=\"btn btn-warning btn-stg-abort\"\n          data-bind=\"click: conflictAbort, visible: conflictText\"\n        >\n          Abort <span data-bind=\"text: conflictText\" />\n        </button>\n        <span\n          class=\"validationError\"\n          data-bind=\"text: commitValidationError, visible: commitValidationError\"\n        ></span>\n      </div>\n      <div class=\"col-lg-9 file-area\">\n        <div class=\"btn-toolbar\">\n          <div class=\"commands btn-group btn-group-sm\">\n            <button class=\"btn btn-default\" disabled data-bind=\"text: stats\"></button>\n            <button\n              class=\"btn btn-default\"\n              data-bind=\"click: toggleAllStages\"\n              data-toggle=\"tooltip\"\n              data-placement=\"bottom\"\n              data-container=\"body\"\n              title=\"Toggle all uncommitted files for commit\"\n            >\n              <span class=\"glyphicon\" data-bind=\"css: toggleSelectAllGlyphClass\"></span>\n              Toggle all\n            </button>\n            <button\n              class=\"btn btn-default\"\n              data-bind=\"click: discardAllChanges\"\n              data-toggle=\"tooltip\"\n              data-placement=\"bottom\"\n              data-container=\"body\"\n              title=\"Discard all uncommitted file changes, including not showing files\"\n            >\n              <span data-bind=\"html: discardAllIcon\"></span>\n              Discard all\n            </button>\n            <button\n              class=\"btn btn-default stash-all\"\n              data-bind=\"click: stashAll, css: { disabled: !canStashAll() }\"\n              data-toggle=\"tooltip\"\n              data-placement=\"bottom\"\n              data-container=\"body\"\n              title=\"Stash all uncommitted file changes, including not showing files\"\n            >\n              <span data-bind=\"html: stashIcon\"></span>\n              Stash all\n            </button>\n          </div>\n          <div class=\"btn-group btn-group-sm pull-right\">\n            <button\n              class=\"btn btn-default\"\n              data-bind=\"click: wordWrap.toggle, css: {active: wordWrap.isActive}\"\n              data-toggle=\"tooltip\"\n              data-placement=\"bottom\"\n              data-container=\"body\"\n              title=\"Wrap words per line\"\n            >\n              <span data-bind=\"text: wordWrap.text\"></span>\n            </button>\n          </div>\n          <div class=\"btn-group btn-group-sm pull-right\">\n            <button\n              class=\"btn btn-default\"\n              data-bind=\"click: textDiffType.toggle, css: {active: textDiffType.isActive}\"\n              data-toggle=\"tooltip\"\n              data-placement=\"bottom\"\n              data-container=\"body\"\n              title=\"Show side by side diff view\"\n            >\n              <span data-bind=\"text: textDiffType.text\"></span>\n            </button>\n          </div>\n          <div class=\"btn-group btn-group-sm pull-right\">\n            <button\n              class=\"btn btn-default\"\n              data-bind=\"click: whiteSpace.toggle, css: {active: whiteSpace.isActive}\"\n              data-toggle=\"tooltip\"\n              data-placement=\"bottom\"\n              data-container=\"body\"\n              title=\"Hide whitespace changes in diff\"\n            >\n              <span data-bind=\"text: whiteSpace.text\"></span>\n            </button>\n          </div>\n        </div>\n\n        <div class=\"files\" data-bind=\"foreach: files\">\n          <div class=\"file\" data-bind=\"css: { showingDiffs: isShowingDiffs }\">\n            <div\n              class=\"checkmark\"\n              data-bind=\"click: toggleStaged, css: { checked: editState() !== 'none' }\"\n            >\n              <span\n                class=\"glyphicon\"\n                data-bind=\"css: { 'glyphicon-check': editState() === 'staged', 'glyphicon-unchecked': editState() === 'none', 'glyphicon-list-alt': editState() === 'patched'}\"\n              ></span>\n            </div>\n            <button class=\"name btn btn-default\" data-bind=\"click: toggleDiffs\">\n              <span data-bind=\"text: displayName\"></span>\n            </button>\n            <span class=\"new\" data-bind=\"visible: isNew\">New</span>\n            <span class=\"deleted\" data-bind=\"visible: removed\">Removed</span>\n            <span class=\"additions\" data-bind=\"text: additions\"></span>\n            <span class=\"deletions\" data-bind=\"text: deletions\"></span>\n            <span class=\"modified\" data-bind=\"visible: modified\">Modified</span>\n            <span class=\"conflict\" data-bind=\"visible: conflict\"\n              ><span class=\"temporary\">Conflicts</span\n              ><span\n                class=\"launchmergetool explanation\"\n                data-bind=\"visible: mergeTool, click: launchMergeTool\"\n                >Launch Merge Tool</span\n              ><span class=\"markresolved explanation\" data-bind=\"click: resolveConflict\"\n                >Mark as Resolved</span\n              ></span\n            >\n            <button\n              class=\"patch btn\"\n              data-bind=\"visible: isShowPatch(), click: patchClick\"\n              data-toggle=\"tooltip\"\n              title=\"Patch changes\"\n            >\n              Patch\n            </button>\n            <button\n              class=\"ignore btn\"\n              data-bind=\"html: $parent.ignoreIcon, click: ignoreFile\"\n              data-toggle=\"tooltip\"\n              title=\"Add to .gitignore\"\n            ></button>\n            <button\n              class=\"discard btn\"\n              data-bind=\"html: $parent.discardIcon, click: discardChanges\"\n              data-toggle=\"tooltip\"\n              title=\"Discard changes\"\n            ></button>\n            <!-- ko if: isShowingDiffs -->\n            <div class=\"diffContainer\" data-bind=\"component: diff\"></div>\n            <!-- /ko -->\n          </div>\n        </div>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "components/staging/staging.js",
    "content": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\nconst filesToDisplayIncrmentBy = 50;\nconst filesToDisplayLimit = filesToDisplayIncrmentBy;\nconst mergeTool = ungit.config.mergeTool;\nconst { ComponentRoot } = require('../ComponentRoot');\n\ncomponents.register(\n  'staging',\n  (args) => new StagingViewModel(args.server, args.repoPath, args.graph)\n);\n\nclass StagingViewModel extends ComponentRoot {\n  constructor(server, repoPath, graph) {\n    super();\n    this.server = server;\n    this.repoPath = repoPath;\n    this.refreshContent = _.debounce(this._refreshContent, 250, this.defaultDebounceOption);\n    this.graph = graph;\n    this.filesByPath = {};\n    this.files = ko.observableArray();\n    this.commitMessageTitleCount = ko.observable(0);\n    this.commitMessageTitle = ko.observable();\n    this.commitMessageTitle.subscribe((value) => {\n      this.commitMessageTitleCount(value.length);\n    });\n    this.commitMessageBody = ko.observable();\n    this.wordWrap = components.create('textdiff.wordwrap');\n    this.textDiffType = components.create('textdiff.type');\n    this.whiteSpace = components.create('textdiff.whitespace');\n    this.inRebase = ko.observable(false);\n    this.inMerge = ko.observable(false);\n    this.inCherry = ko.observable(false);\n    this.conflictText = ko.computed(() => {\n      if (this.inMerge()) {\n        this.conflictContinue = this.conflictResolution.bind(this, '/merge/continue');\n        this.conflictAbort = this.conflictResolution.bind(this, '/merge/abort');\n        return 'Merge';\n      } else if (this.inRebase()) {\n        this.conflictContinue = this.conflictResolution.bind(this, '/rebase/continue');\n        this.conflictAbort = this.conflictResolution.bind(this, '/rebase/abort');\n        return 'Rebase';\n      } else if (this.inCherry()) {\n        this.conflictContinue = this.commit;\n        this.conflictAbort = this.discardAllChanges;\n        return 'Cherry-pick';\n      } else {\n        this.conflictContinue = undefined;\n        this.conflictAbort = undefined;\n        return undefined;\n      }\n    });\n    this.HEAD = ko.observable();\n    this.isStageValid = ko.computed(() => !this.inRebase() && !this.inMerge() && !this.inCherry());\n    this.nFiles = ko.computed(() => this.files().length);\n    this.nStagedFiles = ko.computed(\n      () => this.files().filter((f) => f.editState() === 'staged').length\n    );\n    this.allStageFlag = ko.computed(() => this.nFiles() !== this.nStagedFiles());\n    this.stats = ko.computed(() => `${this.nFiles()} files, ${this.nStagedFiles()} to be commited`);\n    this.amend = ko.observable(false);\n    this.canAmend = ko.computed(\n      () => this.HEAD() && !this.inRebase() && !this.inMerge() && !this.emptyCommit()\n    );\n    this.emptyCommit = ko.observable(false);\n    this.canEmptyCommit = ko.computed(() => this.HEAD() && !this.inRebase() && !this.inMerge());\n    this.canStashAll = ko.computed(() => !this.amend());\n    this.canPush = ko.computed(() => !!this.graph.currentRemote());\n    this.showNux = ko.computed(\n      () => this.files().length == 0 && !this.amend() && !this.inRebase() && !this.emptyCommit()\n    );\n    this.showCancelButton = ko.computed(() => this.amend() || this.emptyCommit());\n    this.commitValidationError = ko.computed(() => {\n      if (this.conflictText()) {\n        if (this.files().some((file) => file.conflict())) return 'Files in conflict';\n      } else {\n        if (\n          !this.emptyCommit() &&\n          !this.amend() &&\n          !this.files().some(\n            (file) => file.editState() === 'staged' || file.editState() === 'patched'\n          )\n        ) {\n          return 'No files to commit';\n        }\n        if (!this.commitMessageTitle()) {\n          return 'Provide a title';\n        }\n\n        if (this.textDiffType.value() === 'sidebysidediff') {\n          const patchFiles = this.files().filter((file) => file.editState() === 'patched');\n          if (patchFiles.length > 0) return 'Cannot patch with side by side view.';\n        }\n      }\n      return '';\n    });\n    this.toggleSelectAllGlyphClass = ko.computed(() => {\n      if (this.allStageFlag()) return 'glyphicon-unchecked';\n      else return 'glyphicon-check';\n    });\n\n    this.refreshContentThrottled = _.throttle(this.refreshContent.bind(this), 500, {\n      leading: false,\n      trailing: true,\n    });\n    this.invalidateFilesDiffsThrottled = _.throttle(this.invalidateFilesDiffs.bind(this), 500, {\n      leading: false,\n      trailing: true,\n    });\n    this.refreshContentThrottled();\n    this.loadAnyway = false;\n    this.isDiagOpen = false;\n    this.mutedTime = null;\n    this.discardAllIcon = octicons.trash.toSVG({ height: 15 });\n    this.stashIcon = octicons.pin.toSVG({ height: 15 });\n    this.discardIcon = octicons.x.toSVG({ height: 18 });\n    this.ignoreIcon = octicons.skip.toSVG({ height: 18 });\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('staging', this, {}, parentElement);\n  }\n\n  onProgramEvent(event) {\n    if (\n      event.event == 'request-app-content-refresh' ||\n      event.event === 'working-tree-changed' ||\n      event.event === 'git-directory-changed'\n    ) {\n      this.refreshContent();\n      this.invalidateFilesDiffs();\n    }\n  }\n\n  async _refreshContent() {\n    ungit.logger.debug('staging.refreshContent() triggered');\n\n    try {\n      const headPromise = this.server.getPromise('/head', { path: this.repoPath(), limit: 1 });\n      const statusPromise = this.server.getPromise('/status', {\n        path: this.repoPath(),\n        fileLimit: filesToDisplayLimit,\n      });\n      const log = await headPromise;\n      if (log.length > 0) {\n        const array = log[0].message.split('\\n');\n        this.HEAD({ title: array[0], body: array.slice(2).join('\\n') });\n      } else {\n        this.HEAD(null);\n      }\n\n      const status = await statusPromise;\n      if (this.isSamePayload(status)) {\n        return;\n      }\n\n      if (Object.keys(status.files).length > filesToDisplayLimit && !this.loadAnyway) {\n        if (this.isDiagOpen) {\n          return;\n        }\n        this.isDiagOpen = true;\n        components.showModal('toomanyfilesmodal', {\n          title: 'Too many unstaged files',\n          details: 'It is recommended to use command line as ungit may be too slow.',\n          closeFunc: (isYes) => {\n            this.isDiagOpen = false;\n            if (isYes) {\n              window.location.href = '/#/';\n            } else {\n              this.loadAnyway = true;\n              this.loadStatus(status);\n            }\n          },\n        });\n      } else {\n        this.loadStatus(status);\n      }\n    } catch (err) {\n      if (err.errorCode != 'must-be-in-working-tree' && err.errorCode != 'no-such-path') {\n        this.server.unhandledRejection(err);\n      } else {\n        ungit.logger.error('error during staging refresh: ', err);\n      }\n    } finally {\n      ungit.logger.debug('staging.refreshContent() finished');\n    }\n  }\n\n  loadStatus(status) {\n    this.setFiles(status.files);\n    this.inRebase(!!status.inRebase);\n    this.inMerge(!!status.inMerge);\n    // There are time where '.git/CHERRY_PICK_HEAD' file is created and no files are in conflicts.\n    // in such cases we should ignore exception as no good way to resolve it.\n    this.inCherry(!!status.inCherry && !!status.inConflict);\n\n    if (this.inRebase()) {\n      this.commitMessageTitle('Rebase conflict');\n      this.commitMessageBody('Commit messages are not applicable!\\n(╯°□°）╯︵ ┻━┻');\n    } else if (this.inMerge() || this.inCherry()) {\n      const lines = status.commitMessage.split('\\n');\n      if (!this.commitMessageTitle()) {\n        this.commitMessageTitle(lines[0]);\n        this.commitMessageBody(lines.slice(1).join('\\n'));\n      }\n    }\n  }\n\n  setFiles(files) {\n    const newFiles = [];\n    for (const fileStatus of Object.values(files)) {\n      let fileViewModel = this.filesByPath[fileStatus.fileName];\n      if (!fileViewModel) {\n        this.filesByPath[fileStatus.fileName] = fileViewModel = new FileViewModel(\n          this,\n          fileStatus.fileName,\n          fileStatus.oldFileName,\n          fileStatus.displayName\n        );\n      } else {\n        // this is mainly for patching and it may not fire due to the fact that\n        // '/commit' triggers working-tree-changed which triggers throttled refresh\n        fileViewModel.diff().invalidateDiff();\n      }\n      fileViewModel.setState(fileStatus);\n      newFiles.push(fileViewModel);\n    }\n    this.files(newFiles);\n  }\n\n  toggleAmend() {\n    if (!this.amend() && !this.commitMessageTitle()) {\n      this.commitMessageTitle(this.HEAD().title);\n      this.commitMessageBody(this.HEAD().body);\n    } else if (this.amend()) {\n      const isPrevDefaultMsg =\n        this.commitMessageTitle() == this.HEAD().title &&\n        this.commitMessageBody() == this.HEAD().body;\n      if (isPrevDefaultMsg) {\n        this.commitMessageTitle('');\n        this.commitMessageBody('');\n      }\n    }\n    this.amend(!this.amend());\n  }\n\n  toggleEmptyCommit() {\n    this.commitMessageTitle('Empty commit');\n    this.commitMessageBody();\n    this.emptyCommit(true);\n  }\n\n  resetMessages() {\n    this.commitMessageTitle('');\n    this.commitMessageBody('');\n    for (const key in this.filesByPath) {\n      const element = this.filesByPath[key];\n      element.diff().invalidateDiff();\n      element.patchLineList.removeAll();\n      element.isShowingDiffs(false);\n      element.editState(element.editState() === 'patched' ? 'none' : element.editState());\n    }\n    this.amend(false);\n    this.emptyCommit(false);\n  }\n\n  commit() {\n    const files = this.files()\n      .filter((file) => file.editState() !== 'none')\n      .map((file) => ({\n        name: file.name(),\n        patchLineList: file.editState() === 'patched' ? file.patchLineList() : null,\n      }));\n    let commitMessage = this.commitMessageTitle();\n    if (this.commitMessageBody()) commitMessage += `\\n\\n${this.commitMessageBody()}`;\n\n    this.server\n      .postPromise('/commit', {\n        path: this.repoPath(),\n        message: commitMessage,\n        files,\n        amend: this.amend(),\n        emptyCommit: this.emptyCommit(),\n      })\n      .then(() => {\n        this.resetMessages();\n        programEvents.dispatch({ event: 'branch-updated' });\n      })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  commitnpush() {\n    const files = this.files()\n      .filter((file) => file.editState() !== 'none')\n      .map((file) => ({\n        name: file.name(),\n        patchLineList: file.editState() === 'patched' ? file.patchLineList() : null,\n      }));\n    let commitMessage = this.commitMessageTitle();\n    if (this.commitMessageBody()) commitMessage += `\\n\\n${this.commitMessageBody()}`;\n\n    this.server\n      .postPromise('/commit', {\n        path: this.repoPath(),\n        message: commitMessage,\n        files,\n        amend: this.amend(),\n        emptyCommit: this.emptyCommit(),\n      })\n      .then(() => {\n        this.resetMessages();\n        return this.server.postPromise('/push', {\n          path: this.repoPath(),\n          remote: this.graph.currentRemote(),\n        });\n      })\n      .catch((err) => {\n        if (err.errorCode == 'non-fast-forward') {\n          components.showModal('yesnomodal', {\n            title: 'Force push?',\n            details: \"The remote branch can't be fast-forwarded.\",\n            closeFunc: (isYes) => {\n              if (!isYes) return;\n              this.server.postPromise('/push', {\n                path: this.repoPath(),\n                remote: this.graph.currentRemote(),\n                force: true,\n              });\n            },\n          });\n        } else {\n          this.server.unhandledRejection(err);\n        }\n      });\n  }\n\n  conflictResolution(apiPath) {\n    let commitMessage = this.commitMessageTitle();\n    if (this.commitMessageBody()) commitMessage += `\\n\\n${this.commitMessageBody()}`;\n    this.server\n      .postPromise(apiPath, { path: this.repoPath(), message: commitMessage })\n      .catch((e) => this.server.unhandledRejection(e))\n      .finally(() => {\n        this.resetMessages();\n      });\n  }\n\n  invalidateFilesDiffs() {\n    this.files().forEach((file) => {\n      file.diff().invalidateDiff();\n    });\n  }\n\n  cancelAmendEmpty() {\n    this.resetMessages();\n  }\n\n  discardAllChanges() {\n    components.showModal('yesnomodal', {\n      title: 'Are you sure you want to discard all changes?',\n      details: 'This operation cannot be undone.',\n      closeFunc: (isYes) => {\n        if (!isYes) return;\n        this.server\n          .postPromise('/discardchanges', { path: this.repoPath(), all: true })\n          .catch((e) => this.server.unhandledRejection(e));\n      },\n    });\n  }\n\n  stashAll() {\n    this.server\n      .postPromise('/stashes', { path: this.repoPath(), message: this.commitMessageTitle() })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  toggleAllStages() {\n    const allStageFlag = this.allStageFlag();\n    for (const n in this.files()) {\n      this.files()[n].editState(allStageFlag ? 'staged' : 'none');\n    }\n  }\n\n  onEnter(d, e) {\n    if (e.keyCode === 13 && !this.commitValidationError()) {\n      this.commit();\n    }\n    return true;\n  }\n\n  onAltEnter(d, e) {\n    if (e.keyCode === 13 && e.altKey && !this.commitValidationError()) {\n      this.commit();\n    }\n    return true;\n  }\n}\n\nclass FileViewModel {\n  constructor(staging, name, oldName, displayName) {\n    this.staging = staging;\n    this.server = staging.server;\n    this.editState = ko.observable('staged'); // staged, patched and none\n    this.name = ko.observable(name);\n    this.oldName = ko.observable(oldName);\n    this.displayName = ko.observable(displayName);\n    this.isNew = ko.observable(false);\n    this.removed = ko.observable(false);\n    this.conflict = ko.observable(false);\n    this.renamed = ko.observable(false);\n    this.isShowingDiffs = ko.observable(false);\n    this.additions = ko.observable('');\n    this.deletions = ko.observable('');\n    this.modified = ko.computed(() => {\n      // only show modfied whe not removed, not conflicted, not new, not renamed\n      // and length of additions and deletions is 0.\n      return (\n        !this.removed() &&\n        !this.conflict() &&\n        !this.isNew() &&\n        this.additions().length === 0 &&\n        this.deletions().length === 0\n      );\n    });\n    this.fileType = ko.observable('text');\n    this.patchLineList = ko.observableArray();\n    this.diff = ko.observable();\n    this.isShowPatch = ko.computed(\n      () =>\n        // if not new file\n        // and if not merging\n        // and if not rebasing\n        // and if text file\n        // and if diff is showing, display patch button\n        !this.isNew() &&\n        !staging.inMerge() &&\n        !staging.inRebase() &&\n        this.fileType() === 'text' &&\n        this.isShowingDiffs()\n    );\n    this.mergeTool = ko.computed(() => this.conflict() && mergeTool !== false);\n\n    this.editState.subscribe((value) => {\n      if (value === 'none') {\n        this.patchLineList.removeAll();\n      } else if (value === 'patched') {\n        if (this.diff().render) this.diff().render();\n      }\n    });\n  }\n\n  getSpecificDiff() {\n    return components.create(!this.name() || `${this.fileType()}diff`, {\n      filename: this.name(),\n      oldFilename: this.oldName(),\n      displayFilename: this.displayName(),\n      repoPath: this.staging.repoPath,\n      server: this.server,\n      textDiffType: this.staging.textDiffType,\n      whiteSpace: this.staging.whiteSpace,\n      isShowingDiffs: this.isShowingDiffs,\n      patchLineList: this.patchLineList,\n      editState: this.editState,\n      wordWrap: this.staging.wordWrap,\n    });\n  }\n\n  setState(state) {\n    this.displayName(state.displayName);\n    this.isNew(state.isNew);\n    this.removed(state.removed);\n    this.conflict(state.conflict);\n    this.renamed(state.renamed);\n    this.fileType(state.type);\n    this.additions(state.additions != '-' ? `+${state.additions}` : '');\n    this.deletions(state.deletions != '-' ? `-${state.deletions}` : '');\n    if (this.diff()) {\n      this.diff().invalidateDiff();\n    } else {\n      this.diff(this.getSpecificDiff());\n    }\n    if (this.diff().isNew) this.diff().isNew(state.isNew);\n    if (this.diff().isRemoved) this.diff().isRemoved(state.removed);\n  }\n\n  toggleStaged() {\n    if (this.editState() === 'none') {\n      this.editState('staged');\n    } else {\n      this.editState('none');\n    }\n    this.patchLineList([]);\n  }\n\n  discardChanges() {\n    const timeSinceLastMute = new Date().getTime() - this.staging.mutedTime;\n    const isMuteWarning = timeSinceLastMute < ungit.config.disableDiscardMuteTime;\n    ungit.logger.debug(\n      `discard time since mute: ${timeSinceLastMute}, isMuteWarning: ${isMuteWarning}`\n    );\n    if (ungit.config.disableDiscardWarning || isMuteWarning) {\n      this.server\n        .postPromise('/discardchanges', { path: this.staging.repoPath(), file: this.name() })\n        .catch((e) => this.server.unhandledRejection(e));\n    } else {\n      components.showModal('yesnomutemodal', {\n        title: 'Are you sure you want to discard these changes?',\n        details: 'This operation cannot be undone.',\n        closeFunc: (isYes, isMute) => {\n          if (isYes) {\n            this.server\n              .postPromise('/discardchanges', { path: this.staging.repoPath(), file: this.name() })\n              .catch((e) => this.server.unhandledRejection(e));\n          }\n          if (isMute) {\n            this.staging.mutedTime = new Date().getTime();\n          }\n        },\n      });\n    }\n  }\n\n  ignoreFile() {\n    this.server\n      .postPromise('/ignorefile', { path: this.staging.repoPath(), file: this.name() })\n      .catch((err) => {\n        if (err.errorCode == 'file-already-git-ignored') {\n          // The file was already in the .gitignore, so force an update of the staging area (to hopefully clear away this file)\n          programEvents.dispatch({ event: 'working-tree-changed' });\n        } else {\n          this.server.unhandledRejection(err);\n        }\n      });\n  }\n\n  resolveConflict() {\n    this.server\n      .postPromise('/resolveconflicts', { path: this.staging.repoPath(), files: [this.name()] })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  launchMergeTool() {\n    this.server\n      .postPromise('/launchmergetool', {\n        path: this.staging.repoPath(),\n        file: this.name(),\n        tool: mergeTool,\n      })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  toggleDiffs() {\n    this.isShowingDiffs(!this.isShowingDiffs());\n  }\n\n  patchClick() {\n    if (!this.isShowingDiffs()) return;\n\n    if (this.editState() === 'patched') {\n      this.editState('staged');\n    } else {\n      this.editState('patched');\n    }\n  }\n}\n"
  },
  {
    "path": "components/staging/staging.less",
    "content": "@import 'public/less/variables.less';\n\n.staging {\n  background: #643a44;\n  box-shadow: 0 -1px 15px #252833;\n  color: #b8a5a5;\n  z-index: 5;\n  position: relative;\n\n  .form-control {\n    &:disabled {\n      background-color: rgba(64, 36, 43, 0.75);\n    }\n  }\n\n  textarea.commit-body {\n    resize: vertical;\n  }\n\n  .arrow {\n    border-top-color: #643a44;\n    left: (@log-width-small + 45px);\n    bottom: -30px;\n  }\n\n  .commitnpush.disabled {\n    pointer-events: none;\n    opacity: 0.5;\n  }\n\n  .file-area {\n    position: relative;\n  }\n\n  .validationError {\n    display: none;\n    color: #d6542d;\n    padding: 0.25em;\n  }\n\n  &:hover .validationError {\n    display: inline-block;\n  }\n\n  .diffContainer {\n    margin-top: 0;\n    border-radius: 3px;\n    background: rgba(255, 255, 255, 0.1);\n  }\n\n  .discard {\n    background: transparent;\n    color: rgba(255, 255, 255, 0.3);\n    border-radius: 3px;\n    padding: 3px;\n    padding-left: 5px;\n    padding-right: 5px;\n    cursor: pointer;\n\n    &:focus,\n    &:hover {\n      background: #000000;\n      color: rgba(255, 255, 255, 0.9);\n    }\n  }\n\n  .ignore {\n    background: transparent;\n    color: rgba(255, 255, 255, 0.3);\n    border-radius: 3px;\n    padding: 3px;\n    padding-left: 5px;\n    padding-right: 5px;\n    cursor: pointer;\n    font-weight: bold;\n\n    &:focus,\n    &:hover {\n      background: #5555ff;\n      color: rgba(255, 255, 255, 0.9);\n    }\n  }\n\n  .patch {\n    background: #279124;\n    color: rgba(255, 255, 255, 0.9);\n    border-radius: 3px;\n    padding: 3px;\n    padding-left: 5px;\n    padding-right: 5px;\n    cursor: pointer;\n    font-weight: bold;\n\n    &:focus,\n    &:hover {\n      background: #279124;\n      color: rgba(255, 255, 255, 0.9);\n    }\n  }\n\n  .d2h-code-line-prefix input[type='checkbox'] {\n    margin: 0;\n    margin-right: -5px;\n    vertical-align: sub;\n  }\n\n  .files {\n    position: relative;\n\n    .file {\n      padding: 0.3em;\n\n      &.showingDiffs {\n        .name {\n          background: rgba(255, 255, 255, 0.1);\n          color: #000000;\n          border-bottom-left-radius: 0;\n          border-bottom-right-radius: 0;\n        }\n      }\n\n      .checkmark {\n        span {\n          top: 5px;\n        }\n      }\n\n      .name {\n        background: transparent;\n        font-size: 1.3em;\n        cursor: pointer;\n        padding: 3px;\n        border: 0;\n        border-radius: 3px;\n        color: rgba(255, 255, 255, 0.8);\n      }\n\n      .new,\n      .deleted,\n      .conflict,\n      .markresolved,\n      .launchmergetool {\n        padding: 3px;\n        padding-left: 5px;\n        padding-right: 5px;\n      }\n\n      .new,\n      .additions {\n        color: #949494;\n        vertical-align: middle;\n      }\n\n      .deleted,\n      .deletions {\n        color: #7b7b7b;\n        vertical-align: middle;\n      }\n\n      .conflict {\n        color: #db12c0;\n\n        .explanation {\n          display: none;\n        }\n\n        &:hover {\n          .explanation {\n            display: inline;\n          }\n\n          .temporary {\n            display: none;\n          }\n        }\n      }\n\n      .markresolved {\n        color: #db12c0;\n        cursor: pointer;\n\n        .explanation {\n          display: none;\n        }\n\n        &:hover {\n          background: #a445ed;\n          color: #000000;\n          border-radius: 3px;\n\n          .explanation {\n            display: inline;\n          }\n        }\n      }\n\n      .launchmergetool {\n        color: #db55ff;\n        cursor: pointer;\n\n        .explanation {\n          display: none;\n        }\n\n        &:hover {\n          background: #a477ff;\n          color: #000000;\n          border-radius: 3px;\n\n          .explanation {\n            display: inline;\n          }\n        }\n      }\n    }\n  }\n}\n\n@media (min-width: @screen-md-min) {\n  .staging {\n    .arrow {\n      left: (@log-width-large + 45px);\n    }\n  }\n}\n\n.commit-message-title-counter {\n  right: 20px;\n  position: absolute;\n}\n\n.amend-button {\n  padding: 0;\n\n  &:active,\n  &:focus,\n  &:hover {\n    text-decoration: none;\n  }\n}\n\n.checkmark {\n  color: #ffffff;\n  display: inline-block;\n  opacity: 0.3;\n  cursor: pointer;\n\n  &.checked {\n    opacity: 0.8;\n  }\n}\n"
  },
  {
    "path": "components/staging/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"staging\": \"staging.html\"\n    },\n    \"javascript\": \"staging.bundle.js\",\n    \"css\": \"staging.css\"\n  }\n}\n"
  },
  {
    "path": "components/stash/stash.html",
    "content": "<div class=\"stash-toggle stash-toggle-text\" data-bind=\"click: toggleShowStash, visible: !isShow()\">\n  <span class=\"expand-icon\" data-bind=\"html: expandIcon\"></span>\n  Stash (<span data-bind=\"text: stashedChanges().length\"></span>)\n</div>\n<div class=\"panel panel-default stash\" data-bind=\"visible: visible\">\n  <div class=\"panel-body\">\n    <h4 class=\"stash-toggle-text\" data-bind=\"click: toggleShowStash\">\n      <span class=\"expand-icon\" data-bind=\"html: expandedIcon\"></span>\n      Stashed changes (<span data-bind=\"text: stashedChanges().length\"></span>)\n    </h4>\n    <div class=\"list-group\" data-bind=\"foreach: stashedChanges\">\n      <div class=\"list-group-item\">\n        <a\n          href=\"#\"\n          class=\"stash-apply octicon-circled\"\n          data-bind=\"html: applyIcon, click: apply\"\n          data-toggle=\"tooltip\"\n          title=\"Apply this stash\"\n        ></a>\n        <a\n          href=\"#\"\n          class=\"toggle-show-commit-diffs\"\n          data-bind=\"click: toggleShowCommitDiffs\"\n          data-toggle=\"tooltip\"\n          title=\"Show stash diff\"\n        >\n          <h4 class=\"list-group-item-heading\" data-bind=\"text: title\"></h4>\n          <p class=\"list-group-item-text\" data-bind=\"text: message\"></p>\n        </a>\n        <!-- ko if: showCommitDiff() -->\n        <div class=\"diff-wrapper\">\n          <div class=\"diff-inner\" data-bind=\"component: commitDiff\"></div>\n        </div>\n        <!-- /ko -->\n        <button\n          type=\"button\"\n          class=\"btn btn-default list-item-remove\"\n          data-bind=\"html: dropIcon, click: drop\"\n          data-toggle=\"tooltip\"\n          title=\"Drop this stash\"\n        ></button>\n      </div>\n    </div>\n  </div>\n</div>\n"
  },
  {
    "path": "components/stash/stash.js",
    "content": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst moment = require('moment');\nconst components = require('ungit-components');\nconst storage = require('ungit-storage');\nconst { ComponentRoot } = require('../ComponentRoot');\n\ncomponents.register('stash', (args) => new StashViewModel(args.server, args.repoPath));\n\nclass StashItemViewModel {\n  constructor(stash, data) {\n    this.stash = stash;\n    this.server = stash.server;\n    this.id = data.reflogId;\n    this.sha1 = data.sha1;\n    this.title = `${data.reflogName} ${moment(new Date(data.commitDate)).fromNow()}`;\n    this.message = data.message;\n    this.showCommitDiff = ko.observable(false);\n\n    this.commitDiff = ko.observable(\n      components.create('commitDiff', {\n        fileLineDiffs: data.fileLineDiffs.slice(),\n        sha1: this.sha1,\n        repoPath: stash.repoPath,\n        server: stash.server,\n        showDiffButtons: ko.observable(true),\n      })\n    );\n    this.dropIcon = octicons.x.toSVG({ height: 18 });\n    this.applyIcon = octicons.pencil.toSVG({ height: 20 });\n  }\n\n  apply() {\n    this.server\n      .delPromise(`/stashes/${this.id}`, { path: this.stash.repoPath(), apply: true })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  drop() {\n    components.showModal('yesnomodal', {\n      title: 'Are you sure you want to drop the stash?',\n      details: 'This operation cannot be undone.',\n      closeFunc: (isYes) => {\n        if (!isYes) return;\n        this.server\n          .delPromise(`/stashes/${this.id}`, { path: this.stash.repoPath() })\n          .catch((e) => this.server.unhandledRejection(e));\n      },\n    });\n  }\n\n  toggleShowCommitDiffs() {\n    this.showCommitDiff(!this.showCommitDiff());\n  }\n}\n\nclass StashViewModel extends ComponentRoot {\n  constructor(server, repoPath) {\n    super();\n    this.server = server;\n    this.repoPath = repoPath;\n    this.refresh = _.debounce(this._refresh, 250, this.defaultDebounceOption);\n    this.stashedChanges = ko.observable([]);\n    this.isShow = ko.observable(storage.getItem('showStash') === 'true');\n    this.visible = ko.computed(() => this.stashedChanges().length > 0 && this.isShow());\n    this.expandIcon = octicons['chevron-right'].toSVG({ height: 18 });\n    this.expandedIcon = octicons['chevron-down'].toSVG({ height: 22 });\n    this.refresh();\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('stash', this, {}, parentElement);\n  }\n\n  onProgramEvent(event) {\n    if (event.event == 'request-app-content-refresh' || event.event == 'git-directory-changed') {\n      this.refresh();\n    }\n  }\n\n  async _refresh() {\n    ungit.logger.debug('stash.refresh() triggered');\n\n    try {\n      const stashes = await this.server.getPromise('/stashes', { path: this.repoPath() });\n      if (this.isSamePayload(stashes)) {\n        return;\n      }\n\n      let changed = this.stashedChanges().length != stashes.length;\n      if (!changed) {\n        changed = !this.stashedChanges().every((item1) =>\n          stashes.some((item2) => item1.sha1 == item2.sha1)\n        );\n      }\n\n      if (changed) {\n        this.stashedChanges(stashes.map((item) => new StashItemViewModel(this, item)));\n      }\n    } catch (err) {\n      if (err.errorCode != 'no-such-path') {\n        this.server.unhandledRejection(err);\n      } else {\n        ungit.logger.warn('refresh failed: ', err);\n      }\n    } finally {\n      ungit.logger.debug('stash.refresh() finished');\n    }\n  }\n\n  toggleShowStash() {\n    this.isShow(!this.isShow());\n    storage.setItem('showStash', this.isShow());\n  }\n}\n"
  },
  {
    "path": "components/stash/stash.less",
    "content": ".stash {\n  z-index: 4;\n  margin-left: 20px;\n  margin-right: 20px;\n  margin-bottom: -15px;\n  background: #55323c;\n\n  h4 {\n    margin-top: 0;\n  }\n\n  .toggle-show-commit-diffs {\n    display: inline-block;\n  }\n\n  .diff-wrapper {\n    margin-top: 5px;\n  }\n}\n\n.stash-toggle {\n  width: 120px;\n  height: 30px;\n  position: relative;\n  background: #55323c;\n  left: 20px;\n  border-radius: 5px 5px 0 0;\n  padding-top: 5px;\n  text-align: center;\n}\n\n.stash-toggle-text {\n  cursor: pointer;\n}\n\n.expand-icon {\n  opacity: 0.5;\n}\n\n.stash-apply .octicon {\n  vertical-align: middle;\n}\n"
  },
  {
    "path": "components/stash/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"stash\": \"stash.html\"\n    },\n    \"javascript\": \"stash.bundle.js\",\n    \"css\": \"stash.css\"\n  }\n}\n"
  },
  {
    "path": "components/submodules/submodules.html",
    "content": "<div class=\"btn-group fetchButton submodule\">\n  <button type=\"button\" class=\"btn btn-default btn-main\" data-bind=\"click: function() {}\">\n    <span data-bind=\"html: submodulesIcon\"></span>\n    <span data-bind=\"text: 'Submodules'\"></span>\n  </button>\n  <button\n    type=\"button\"\n    class=\"btn btn-default dropdown-toggle\"\n    data-toggle=\"dropdown\"\n    aria-haspopup=\"true\"\n    aria-expanded=\"false\"\n  >\n    <span class=\"caret\"></span>\n    <span class=\"sr-only\">Toggle Submodule List</span>\n  </button>\n  <ul class=\"dropdown-menu dropdown-menu-right\" role=\"menu\">\n    <!-- ko foreach: submodules -->\n    <li>\n      <a\n        class=\"linked-url\"\n        href=\"#\"\n        data-bind=\"text: name, click: $parent.submodulePathClick, attr: { 'data-ta-clickable': name }\"\n      ></a>\n      <a\n        class=\"list-link list-url\"\n        href=\"#\"\n        data-bind=\"html: $parent.linkIcon, click: $parent.submoduleLinkClick, attr: { 'data-ta-clickable': name + '-weblink' }\"\n      ></a>\n      <a\n        class=\"list-link list-remove\"\n        href=\"#\"\n        data-bind=\"html: $parent.closeIcon, click: $parent.submoduleRemove.bind($parent), attr: { 'data-ta-clickable': name + '-remove' }\"\n      ></a>\n    </li>\n    <!-- /ko -->\n    <!-- ko if: submodules().length-->\n    <li class=\"divider\" role=\"separator\"></li>\n    <li>\n      <a href=\"#\" class=\"update-submodule\" data-bind=\"click: updateSubmodules\">Update Submodules</a>\n    </li>\n    <li class=\"divider\" role=\"separator\"></li>\n    <!-- /ko -->\n    <li>\n      <a href=\"#\" class=\"add-submodule\" data-bind=\"click: showAddSubmoduleDialog\">Add Submodules</a>\n    </li>\n  </ul>\n</div>\n"
  },
  {
    "path": "components/submodules/submodules.js",
    "content": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-program-events');\nconst { ComponentRoot } = require('../ComponentRoot');\n\ncomponents.register('submodules', (args) => new SubmodulesViewModel(args.server, args.repoPath));\n\nclass SubmodulesViewModel extends ComponentRoot {\n  constructor(server, repoPath) {\n    super();\n    this.repoPath = repoPath;\n    this.server = server;\n    this.fetchSubmodules = _.debounce(this._fetchSubmodules, 250, this.defaultDebounceOption);\n    this.submodules = ko.observableArray();\n    this.submodulesIcon = octicons['file-submodule'].toSVG({ height: 18 });\n    this.closeIcon = octicons.x.toSVG({ height: 18 });\n    this.linkIcon = octicons['link-external'].toSVG({ height: 18 });\n  }\n\n  onProgramEvent(event) {\n    if (event.event == 'submodule-fetch') {\n      this.fetchSubmodules();\n    }\n  }\n\n  updateNode(parentElement) {\n    this.fetchSubmodules();\n    this.fetchSubmodules.flush().then((submoduleViewModel) => {\n      ko.renderTemplate('submodules', submoduleViewModel, {}, parentElement);\n    });\n  }\n\n  async _fetchSubmodules() {\n    try {\n      const submodules = await this.server.getPromise('/submodules', { path: this.repoPath() });\n      this.submodules(submodules);\n      return this;\n    } catch (e) {\n      ungit.logger.error('error during fetchSubmodules', e);\n    }\n  }\n\n  updateSubmodules() {\n    return this.server\n      .postPromise('/submodules/update', { path: this.repoPath() })\n      .catch((e) => this.server.unhandledRejection(e));\n  }\n\n  showAddSubmoduleDialog() {\n    components.showModal('addsubmodulemodal', { path: this.repoPath() });\n  }\n\n  submoduleLinkClick(submodule) {\n    window.location.href = submodule.url;\n  }\n\n  submodulePathClick(submodule) {\n    window.location.href = document.URL + ungit.config.fileSeparator + submodule.path;\n  }\n\n  submoduleRemove(submodule) {\n    components.showModal('yesnomodal', {\n      title: 'Are you sure?',\n      details: `Deleting ${submodule.name} submodule cannot be undone with ungit.`,\n      closeFunc: (isYes) => {\n        if (!isYes) return;\n        this.server\n          .delPromise('/submodules', {\n            path: this.repoPath(),\n            submodulePath: submodule.path,\n            submoduleName: submodule.name,\n          })\n          .then(() => {\n            programEvents.dispatch({ event: 'submodule-fetch' });\n          })\n          .catch((e) => this.server.unhandledRejection(e));\n      },\n    });\n  }\n}\n"
  },
  {
    "path": "components/submodules/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"submodules\": \"submodules.html\"\n    },\n    \"javascript\": \"submodules.bundle.js\"\n  }\n}\n"
  },
  {
    "path": "components/textdiff/textdiff.html",
    "content": "<!-- ko if: isShowingDiffs -->\n<div class=\"textdiff\">\n  <!-- ko if: isParsed -->\n  <div\n    data-bind=\"template: {nodes: ko.utils.parseHtmlFragment(htmlSrc)}, css: {'word-wrap': wordWrap.value}\"\n  ></div>\n  <!-- /ko -->\n  <div class=\"load-more\" data-bind=\"visible: hasMore\">\n    <button class=\"btn btn-default\" data-bind=\"click: loadMore\">Load more</button>\n  </div>\n</div>\n<!-- /ko -->\n"
  },
  {
    "path": "components/textdiff/textdiff.js",
    "content": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst diff2html = require('diff2html');\nconst sideBySideDiff = 'sidebysidediff';\nconst textDiff = 'textdiff';\n\ncomponents.register('textdiff', (args) => new TextDiffViewModel(args));\ncomponents.register('textdiff.type', () => new Type());\ncomponents.register('textdiff.wordwrap', () => new WordWrap());\ncomponents.register('textdiff.whitespace', () => new WhiteSpace());\n\nconst loadLimit = 100;\n\nclass WordWrap {\n  constructor() {\n    this.value = ko.observable(false);\n\n    this.toggle = () => {\n      this.value(!this.value());\n    };\n    this.text = ko.computed(() => (this.value() ? 'Wrap Lines' : 'No Wrap'));\n    this.isActive = ko.computed(() => this.value());\n  }\n}\n\nclass Type {\n  constructor() {\n    if (\n      !!ungit.config.diffType &&\n      ungit.config.diffType !== textDiff &&\n      ungit.config.diffType !== sideBySideDiff\n    ) {\n      ungit.config.diffType = textDiff;\n      console.log('Config \"diffType\" must be either \"textdiff\" or \"sidebysidediff\".');\n    }\n\n    this.value = ko.observable(ungit.config.diffType || textDiff);\n\n    this.toggle = () => {\n      this.value(this.value() === textDiff ? sideBySideDiff : textDiff);\n    };\n    this.text = ko.computed(() => (this.value() === textDiff ? 'Inline' : 'Side By Side'));\n    this.isActive = ko.computed(() => this.value() === sideBySideDiff);\n  }\n}\n\nclass WhiteSpace {\n  constructor() {\n    this.value = ko.observable(ungit.config.ignoreWhiteSpaceDiff);\n\n    this.toggle = () => {\n      this.value(!this.value());\n    };\n    this.text = ko.computed(() => (this.value() ? 'Show Whitespace' : 'Hide Whitespace'));\n    this.isActive = ko.computed(() => this.value());\n  }\n}\n\nclass TextDiffViewModel {\n  constructor(args) {\n    this.filename = args.filename;\n    this.oldFilename = args.oldFilename;\n    this.repoPath = args.repoPath;\n    this.server = args.server;\n    this.sha1 = args.sha1;\n    this.hasMore = ko.observable(false);\n    this.diffJson = null;\n    this.loadCount = loadLimit;\n    this.textDiffType = args.textDiffType;\n    this.whiteSpace = args.whiteSpace;\n    this.isShowingDiffs = args.isShowingDiffs;\n    this.editState = args.editState;\n    this.wordWrap = args.wordWrap;\n    this.patchLineList = args.patchLineList;\n    this.numberOfSelectedPatchLines = 0;\n    this.htmlSrc = undefined;\n    this.isParsed = ko.observable(false);\n\n    this.isShowingDiffs.subscribe((newValue) => {\n      if (newValue) this.render();\n    });\n    this.textDiffType.value.subscribe(() => {\n      if (this.isShowingDiffs()) this.render();\n    });\n    this.whiteSpace.value.subscribe(() => {\n      if (this.isShowingDiffs()) this.invalidateDiff();\n    });\n\n    if (this.isShowingDiffs()) {\n      this.render();\n    }\n  }\n\n  updateNode(parentElement) {\n    ko.renderTemplate('textdiff', this, {}, parentElement);\n  }\n\n  getDiffArguments() {\n    return {\n      file: this.filename,\n      oldFile: this.oldFilename,\n      path: this.repoPath(),\n      sha1: this.sha1 ? this.sha1 : '',\n      whiteSpace: this.whiteSpace.value(),\n    };\n  }\n\n  invalidateDiff() {\n    this.diffJson = null;\n    if (this.isShowingDiffs()) this.render();\n  }\n\n  getDiffJson() {\n    return this.server\n      .getPromise('/diff', this.getDiffArguments())\n      .then((diffs) => {\n        if (typeof diffs !== 'string') {\n          // Invalid value means there is no changes, show dummy diff without any changes\n          diffs = `diff --git a/${this.filename} b/${this.filename}\n                  index aaaaaaaa..bbbbbbbb 111111\n                  --- a/${this.filename}\n                  +++ b/${this.filename}`;\n        }\n        this.diffJson = diff2html.parse(diffs);\n      })\n      .catch((err) => {\n        // The file existed before but has been removed, but we're trying to get a diff for it\n        // Most likely it will just disappear with the next refresh of the staging area\n        // so we just ignore the error here\n        if (err.errorCode != 'no-such-file') {\n          this.server.unhandledRejection(err);\n        } else {\n          ungit.logger.warn('diff, no such file', err);\n        }\n      });\n  }\n\n  render() {\n    return (!this.diffJson ? this.getDiffJson() : Promise.resolve()).then(() => {\n      if (!this.diffJson || this.diffJson.length == 0) return; // check if diffs are available (binary files do not support them)\n\n      if (!this.diffJson[0].allBlocks) {\n        this.diffJson[0].allBlocks = this.diffJson[0].blocks;\n      }\n\n      const currentLoadCount = Math.max(this.loadCount, loadLimit);\n      let lineCount = 0;\n      let loadCount = 0;\n      this.diffJson[0].blocks = this.diffJson[0].allBlocks.reduce((blocks, block) => {\n        const length = block.lines.length;\n        const remaining = currentLoadCount - lineCount;\n        if (remaining > 0) {\n          loadCount += length;\n          blocks.push(block);\n        }\n        lineCount += length;\n        return blocks;\n      }, []);\n\n      this.loadCount = loadCount;\n      this.hasMore(lineCount > loadCount);\n\n      let html = diff2html.html(this.diffJson, {\n        outputFormat:\n          this.textDiffType.value() === sideBySideDiff ? 'side-by-side' : 'line-by-line',\n        drawFileList: false,\n      });\n\n      this.numberOfSelectedPatchLines = 0;\n      let index = 0;\n\n      // ko's binding resolution is not recursive, which means below ko.bind refresh method doesn't work for\n      // data bind at getPatchCheckBox that is rendered with \"html\" binding.\n      // which is reason why manually updating the html content and refreshing kobinding to have it render...\n      if (this.patchLineList) {\n        html = html.replace(/<span class=\"d2h-code-line-prefix\">(\\+|-)/g, (match, capture) => {\n          if (this.patchLineList()[index] === undefined) {\n            this.patchLineList()[index] = true;\n          }\n\n          return this.getPatchCheckBox(capture, index, this.patchLineList()[index++]);\n        });\n      }\n\n      if (html !== this.htmlSrc) {\n        // diff has changed since last we displayed and need refresh\n        this.htmlSrc = html;\n        this.isParsed(false);\n        this.isParsed(true);\n      }\n    });\n  }\n\n  loadMore() {\n    this.loadCount += loadLimit;\n    this.render();\n  }\n\n  getPatchCheckBox(symbol, index, isActive) {\n    if (isActive) {\n      this.numberOfSelectedPatchLines++;\n    }\n    return `<span class=\"d2h-code-line-prefix\"><span data-bind=\"visible: editState() !== 'patched'\">${symbol}</span><input ${\n      isActive ? 'checked' : ''\n    } type=\"checkbox\" data-bind=\"visible: editState() === 'patched', click: togglePatchLine.bind($data, ${index})\">`;\n  }\n\n  togglePatchLine(index) {\n    this.patchLineList()[index] = !this.patchLineList()[index];\n\n    if (this.patchLineList()[index]) {\n      this.numberOfSelectedPatchLines++;\n    } else {\n      this.numberOfSelectedPatchLines--;\n    }\n\n    if (this.numberOfSelectedPatchLines === 0) {\n      this.editState('none');\n    }\n\n    return true;\n  }\n}\n"
  },
  {
    "path": "components/textdiff/textdiff.less",
    "content": ".textdiff {\n  .load-more {\n    padding: 10px 0;\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "components/textdiff/ungit-plugin.json",
    "content": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"textdiff\": \"textdiff.html\"\n    },\n    \"javascript\": \"textdiff.bundle.js\",\n    \"css\": \"textdiff.css\"\n  }\n}\n"
  },
  {
    "path": "eslint.config.mjs",
    "content": "import js from '@eslint/js';\nimport globals from 'globals';\nimport mochaPlugin from 'eslint-plugin-mocha';\nimport nodePlugin from 'eslint-plugin-n';\nimport prettierPlugin from 'eslint-plugin-prettier/recommended';\n\nexport default [\n  {\n    ignores: ['public/js', '**/*.bundle.js'],\n  },\n\n  js.configs.recommended,\n  mochaPlugin.configs.recommended,\n  nodePlugin.configs['flat/recommended'],\n  prettierPlugin,\n\n  // components\n  {\n    files: ['components/**'],\n\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ungit: 'readonly',\n      },\n    },\n\n    rules: {\n      'n/no-missing-require': 'off',\n      'n/no-unsupported-features/node-builtins': 'off',\n    },\n  },\n\n  // public/source\n  {\n    files: ['public/source/**'],\n\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        io: 'readonly',\n        jQuery: 'writable',\n        Raven: 'readonly',\n        ungit: 'readonly',\n      },\n    },\n\n    rules: {\n      'n/no-missing-require': 'off',\n      'n/no-unsupported-features/node-builtins': 'off',\n    },\n  },\n\n  // public/main.js\n  {\n    files: ['public/main.js'],\n\n    rules: {\n      'n/no-unpublished-require': [\n        'error',\n        {\n          allowModules: ['electron'],\n        },\n      ],\n    },\n  },\n\n  // source\n  {\n    files: ['source/**'],\n\n    rules: {\n      'no-control-regex': 'off',\n      'n/no-process-exit': 'off',\n    },\n  },\n\n  // test\n  {\n    files: ['test/**'],\n\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n      },\n    },\n\n    rules: {\n      'mocha/no-mocha-arrows': 'off',\n    },\n  },\n\n  // clicktests\n  {\n    files: ['clicktests/**'],\n\n    languageOptions: {\n      globals: {\n        ...globals.browser,\n        ungit: 'readonly',\n      },\n    },\n\n    rules: {\n      'mocha/no-mocha-arrows': 'off',\n      'mocha/no-setup-in-describe': 'off',\n    },\n  },\n\n  // eslint.config.mjs\n  {\n    files: ['eslint.config.mjs'],\n\n    languageOptions: {\n      sourceType: 'module',\n    },\n  },\n];\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"ungit\",\n  \"productName\": \"ungit\",\n  \"author\": \"Fredrik Norén <fredrik.jw.noren@gmail.com>\",\n  \"description\": \"Git made easy\",\n  \"version\": \"1.5.30\",\n  \"ungitPluginApiVersion\": \"0.2.0\",\n  \"scripts\": {\n    \"start\": \"node ./bin/ungit\",\n    \"inspect\": \"node --inspect ./source/server.js\",\n    \"test\": \"npm run unittest && npm run clicktest\",\n    \"unittest\": \"mocha --config .mochatest.json\",\n    \"clicktest\": \"mocha --config .mochaclicktest.json\",\n    \"coverage\": \"nyc npm run unittest\",\n    \"lint\": \"eslint .\",\n    \"format\": \"npm run lint -- --fix\",\n    \"build\": \"node --trace-deprecation ./scripts/build.js\",\n    \"watch\": \"nodemon -C --exec \\\"npm run build\\\" -e js,less -w public/source -w public/less -w components/ -i \\\"*.bundle.js\\\"\",\n    \"bumpdependencies\": \"ncu --upgrade --reject jquery --reject bootstrap\",\n    \"electronpackage\": \"node ./scripts/electronpackage.js\",\n    \"electronzip\": \"node ./scripts/electronzip.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/FredrikNoren/ungit.git\"\n  },\n  \"bin\": {\n    \"ungit\": \"./bin/ungit\",\n    \"0ungit-credentials-helper\": \"./bin/credentials-helper\"\n  },\n  \"dependencies\": {\n    \"@primer/octicons\": \"~19.22.0\",\n    \"blueimp-md5\": \"~2.19.0\",\n    \"body-parser\": \"~2.2.2\",\n    \"bootstrap\": \"~3.4.1\",\n    \"chokidar\": \"~5.0.0\",\n    \"cookie-parser\": \"~1.4.7\",\n    \"crossroads\": \"~0.12.2\",\n    \"diff2html\": \"~3.4.56\",\n    \"dnd-page-scroll\": \"0.0.4\",\n    \"express\": \"~5.2.1\",\n    \"express-session\": \"~1.19.0\",\n    \"getmac\": \"~6.6.0\",\n    \"hasher\": \"~1.2.0\",\n    \"ignore\": \"~7.0.5\",\n    \"jquery\": \"~3.7.1\",\n    \"jquery-ui\": \"~1.14.2\",\n    \"just-detect-adblock\": \"~1.1.0\",\n    \"knockout\": \"~3.5.1\",\n    \"latest-version\": \"~9.0.0\",\n    \"lodash\": \"~4.17.23\",\n    \"memorystore\": \"~1.6.7\",\n    \"mkdirp\": \"~3.0.1\",\n    \"moment\": \"~2.30.1\",\n    \"node-cache\": \"~5.1.2\",\n    \"nprogress\": \"~0.2.0\",\n    \"open\": \"~11.0.0\",\n    \"p-limit\": \"~7.3.0\",\n    \"passport\": \"~0.7.0\",\n    \"passport-local\": \"~1.0.0\",\n    \"raven-js\": \"~3.27.2\",\n    \"rc\": \"~1.2.8\",\n    \"rimraf\": \"~6.1.3\",\n    \"semver\": \"~7.7.4\",\n    \"serve-static\": \"~2.2.1\",\n    \"signals\": \"~1.0.0\",\n    \"snapsvg\": \"~0.5.1\",\n    \"socket.io\": \"~4.8.3\",\n    \"temp\": \"~0.9.4\",\n    \"tsify\": \"~5.0.4\",\n    \"typescript\": \"~5.9.3\",\n    \"winston\": \"~3.19.0\",\n    \"yargs\": \"~18.0.0\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"~10.0.1\",\n    \"@homer0/prettier-plugin-jsdoc\": \"~11.0.2\",\n    \"archiver\": \"~7.0.1\",\n    \"browserify\": \"~17.0.1\",\n    \"dedent\": \"~1.7.1\",\n    \"electron\": \"~40.6.1\",\n    \"electron-packager\": \"~17.1.2\",\n    \"eslint\": \"~10.0.2\",\n    \"eslint-config-prettier\": \"~10.1.8\",\n    \"eslint-plugin-mocha\": \"~11.2.0\",\n    \"eslint-plugin-n\": \"~17.24.0\",\n    \"eslint-plugin-prettier\": \"~5.5.5\",\n    \"exorcist\": \"~2.0.0\",\n    \"expect.js\": \"~0.3.1\",\n    \"globals\": \"~17.3.0\",\n    \"less\": \"~4.5.1\",\n    \"mocha\": \"~11.7.5\",\n    \"nodemon\": \"~3.1.14\",\n    \"npm-check-updates\": \"~19.6.3\",\n    \"nyc\": \"~18.0.0\",\n    \"portfinder\": \"~1.0.38\",\n    \"prettier\": \"~3.8.1\",\n    \"puppeteer\": \"~24.37.5\",\n    \"superagent\": \"~10.3.0\",\n    \"supertest\": \"~7.2.2\"\n  },\n  \"engines\": {\n    \"node\": \"^20.19.0 || ^22.12.0 || >=23\"\n  },\n  \"license\": \"MIT\",\n  \"main\": \"public/main.js\"\n}\n"
  },
  {
    "path": "public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n    <link rel=\"stylesheet\" type=\"text/css\" href=\"__ROOT_PATH__/css/styles.css\" />\n    <link rel=\"shortcut icon\" href=\"__ROOT_PATH__/favicon.ico\" />\n    <link rel=\"shortcut icon\" href=\"/images/icon.png\" />\n    <title>ungit</title>\n  </head>\n  <body>\n    <!-- ko component: content --><!-- /ko -->\n\n    <script type=\"text/javascript\">\n      ungit = {};\n    </script>\n    <script type=\"text/javascript\" src=\"__ROOT_PATH__/serverdata.js\"></script>\n    <script type=\"text/javascript\" src=\"__ROOT_PATH__/js/raven.min.js\"></script>\n    <script type=\"text/javascript\">\n      console.log('App version: ' + ungit.version);\n      if (ungit.config.bugtracking) {\n        console.log('Initing raven');\n\n        try {\n          Raven.config('https://58f16d6f010d4c77900bb1de9c02185f@app.getsentry.com/12434', {\n            logger: 'root',\n            tags: {\n              version: ungit.version,\n              subsystem: 'ui',\n              deployment: ungit.config.desktopMode ? 'desktop' : 'web',\n            },\n          }).install();\n          Raven.setUser({ id: ungit.userHash });\n        } catch (e) {\n          console.warn('Failed to initialize Raven.', e);\n          Raven = {\n            TraceKit: {\n              report: { subscribe: function () {} },\n            },\n          };\n        }\n      }\n    </script>\n    <script type=\"text/javascript\" src=\"__ROOT_PATH__/socket.io/socket.io.js\"></script>\n    <script type=\"text/javascript\" src=\"__ROOT_PATH__/js/ungit.js\"></script>\n\n    <!-- ungit-plugins-placeholder -->\n\n    <script type=\"text/javascript\">\n      require('ungit-main').start();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "public/less/bootstrap.less",
    "content": "// Core variables and mixins\n@import '../../node_modules/bootstrap/less/mixins.less';\n\n// Reset\n@import '../../node_modules/bootstrap/less/normalize.less';\n\n// Core CSS\n@import '../../node_modules/bootstrap/less/scaffolding.less';\n@import '../../node_modules/bootstrap/less/type.less';\n@import '../../node_modules/bootstrap/less/code.less';\n@import '../../node_modules/bootstrap/less/grid.less';\n@import '../../node_modules/bootstrap/less/forms.less';\n@import '../../node_modules/bootstrap/less/buttons.less';\n\n// Components\n@import '../../node_modules/bootstrap/less/component-animations.less';\n@import '../../node_modules/bootstrap/less/glyphicons.less';\n@import '../../node_modules/bootstrap/less/dropdowns.less';\n@import '../../node_modules/bootstrap/less/button-groups.less';\n@import '../../node_modules/bootstrap/less/navbar.less';\n@import '../../node_modules/bootstrap/less/labels.less';\n@import '../../node_modules/bootstrap/less/alerts.less';\n@import '../../node_modules/bootstrap/less/list-group.less';\n@import '../../node_modules/bootstrap/less/panels.less';\n@import '../../node_modules/bootstrap/less/close.less';\n\n// Components w/ JavaScript\n@import '../../node_modules/bootstrap/less/modals.less';\n@import '../../node_modules/bootstrap/less/tooltip.less';\n\n// Utility classes\n@import '../../node_modules/bootstrap/less/utilities.less';\n@import '../../node_modules/bootstrap/less/responsive-utilities.less';\n\n// Bootstrap styling\n.form-control {\n  border: none;\n}\n\n.dropdown-menu {\n  max-height: 500px;\n  overflow-y: auto;\n}\n\n.list-group-item {\n  border: none;\n  margin-bottom: 3px;\n}\n\n.panel {\n  border: none;\n}\n"
  },
  {
    "path": "public/less/d2h.less",
    "content": "// diff2html style overrides\n\n.d2h-file-wrapper {\n  border: none;\n  margin-bottom: 0;\n}\n\n.d2h-file-header {\n  display: none;\n}\n\n.d2h-diff-table {\n  font-family: 'Source Code Pro', monospace;\n  font-size: 12px;\n\n  tbody > tr > td {\n    padding: 0;\n\n    &.d2h-code-side-linenumber {\n      padding: 0 0.5em;\n    }\n  }\n}\n\n.d2h-cntx {\n  color: rgba(255, 255, 255, 0.3);\n}\n\n.d2h-info {\n  background-color: transparent;\n  color: rgba(255, 255, 255, 0.3);\n}\n\n.d2h-code-side-emptyplaceholder,\n.d2h-emptyplaceholder {\n  background-color: transparent;\n}\n\n.d2h-code-linenumber,\n.d2h-code-side-linenumber {\n  background-color: #727a83;\n  border-left: 1px solid rgba(0, 0, 0, 0.34);\n  border-right: 1px solid rgba(0, 0, 0, 0.34);\n}\n\n.d2h-file-diff .d2h-del.d2h-change,\n.d2h-del {\n  background-color: #e86756;\n  color: #ffffff;\n}\n\n.d2h-file-diff .d2h-ins.d2h-change,\n.d2h-ins {\n  background-color: #66f27b;\n  color: #000000;\n}\n\n.d2h-code-line del,\n.d2h-code-side-line del {\n  background-color: rgba(255, 255, 255, 0.2);\n}\n\n.d2h-code-line ins,\n.d2h-code-side-line ins {\n  background-color: rgba(255, 255, 255, 0.6);\n}\n\n.d2h-code-line-ctn {\n  vertical-align: initial;\n}\n\n.word-wrap .d2h-code-line-ctn {\n  white-space: pre-wrap;\n}\n"
  },
  {
    "path": "public/less/styles.less",
    "content": "@import 'variables.less';\n@import 'bootstrap.less';\n@import (less) '../../node_modules/nprogress/nprogress.css';\n@import (less) '../../node_modules/jquery-ui/themes/base/core.css';\n@import (less) '../../node_modules/diff2html/bundles/css/diff2html.min.css';\n@import (less) '../../node_modules/@primer/octicons/build/build.css';\n@import (less) '../vendor/css/animate.css';\n@import 'd2h.less';\n\n// Font face declarations\n@font-face {\n  font-family: 'Open Sans';\n  font-style: normal;\n  font-weight: 400;\n  src: local('Open Sans'), local('OpenSans'), url('../fonts/OpenSans.woff') format('woff');\n}\n\n@font-face {\n  font-family: 'Source Code Pro';\n  font-style: normal;\n  font-weight: 500;\n  src: local('Source Code Pro Medium'), local('SourceCodePro-Medium'),\n    url('../fonts/SourceCodePro-Medium.woff') format('woff');\n}\n\n// Custom Scrollbar for Webkit\n::-webkit-scrollbar {\n  width: 15px;\n}\n\n::-webkit-scrollbar-track {\n  background-color: #2b3844;\n}\n\n::-webkit-scrollbar-thumb {\n  background-color: rgba(255, 255, 255, 0.2);\n}\n\n::-webkit-scrollbar-button {\n  background-color: rgba(0, 0, 0, 0.2);\n}\n\n::-webkit-scrollbar-corner {\n  background-color: black;\n}\n\n// Matching jQuery-UI autocomplete style to bootstrap dropdown\n.ui-autocomplete {\n  .ui-menu-item-wrapper {\n    cursor: pointer;\n\n    &.ui-state-active {\n      color: @dropdown-link-hover-color;\n      background-color: @dropdown-link-hover-bg;\n      border: none;\n      margin: 0;\n    }\n  }\n}\n\n//New shared styles\n.list-group-item .list-item-remove {\n  position: absolute;\n  right: 0;\n  top: 0;\n  background: transparent;\n  border: none;\n  opacity: 0.3;\n\n  &:hover {\n    opacity: 1;\n  }\n}\n\ndiv.list-group-item {\n  &:hover {\n    background: @list-group-hover-bg;\n  }\n\n  a:hover {\n    text-decoration: none;\n  }\n}\n\n.arrow {\n  position: absolute;\n  height: 0;\n  width: 0;\n  border: 15px solid transparent;\n}\n\n.octicon-circled {\n  margin-right: 15px;\n  border: 5px solid #686868;\n  border-radius: 100%;\n  display: inline-block;\n  height: 36px;\n  width: 36px;\n  text-align: center;\n  vertical-align: top;\n}\n\n.dropdown-menu {\n  .linked-remove {\n    padding-right: 40px;\n  }\n\n  .linked-url {\n    padding-right: 60px;\n  }\n\n  .list-link {\n    float: right;\n    margin-top: -23px;\n    padding: 0 4px;\n  }\n\n  .list-url {\n    margin-right: 30px;\n  }\n\n  .list-remove {\n    margin-right: 10px;\n  }\n}\n"
  },
  {
    "path": "public/less/variables.less",
    "content": "@import '../../node_modules/bootstrap/less/variables.less';\n\n// Application variables\n// --------------------------------------------------\n@log-width-small: 400px;\n@log-width-large: 550px;\n\n// Bootstrap variables\n// --------------------------------------------------\n\n// Scaffolding\n@body-bg: #252833;\n@text-color: #d8d8d8;\n\n// Links\n@link-hover-color: lighten(@link-color, 15%);\n\n// Typography\n@font-family-sans-serif: 'Open Sans', sans-serif;\n@font-family-monospace: 'Source Code Pro', monospace;\n\n// Buttons\n@btn-default-color: @text-color;\n@btn-default-bg: #556666;\n@btn-default-border: rgba(0, 0, 0, 0.1);\n\n// Forms\n@input-bg: rgba(255, 255, 255, 0.1);\n@input-color: rgba(255, 255, 255, 0.8);\n@input-color-placeholder: rgba(255, 255, 255, 0.3);\n\n// Navbar\n@navbar-default-bg: #2b3844;\n\n// Tooltips\n@tooltip-color: @text-color;\n@tooltip-bg: #3c4653;\n@tooltip-arrow-width: 8px;\n\n// Modals\n@modal-content-bg: @panel-bg;\n@modal-header-border-color: @list-group-border;\n\n// List group\n@list-group-bg: rgba(0, 0, 0, 0.09);\n@list-group-border: #435158;\n@list-group-hover-bg: rgba(0, 0, 0, 0.2);\n@list-group-link-color: rgba(255, 255, 255, 0.5);\n@list-group-link-heading-color: rgba(255, 255, 255, 0.8);\n\n// Panels\n@panel-bg: #3c4653;\n@panel-default-text: @text-color;\n@panel-default-border: #362c36;\n@panel-default-heading-bg: #2c3541;\n\n// Type\n@text-muted: #707a85;\n"
  },
  {
    "path": "public/main.js",
    "content": "var startLaunchTime = Date.now();\n\nvar child_process = require('child_process');\nvar path = require('path');\nconst { encodePath } = require('../source/address-parser');\nvar config = require('../source/config');\nvar BugTracker = require('../source/bugtracker');\nvar bugtracker = new BugTracker('electron');\n\nvar { app, dialog, shell, BrowserWindow, Menu } = require('electron');\n\nprocess.on('uncaughtException', function (err) {\n  console.error(err.stack.toString());\n  bugtracker.notify(err, 'ungit-launcher');\n  app.quit();\n});\n\nfunction openUngitBrowser(pathToNavigateTo) {\n  console.log(`Navigate to ${pathToNavigateTo}`);\n  mainWindow.loadURL(pathToNavigateTo);\n}\n\nfunction launch(callback) {\n  var url = config.urlBase + ':' + config.port;\n  if (config.forcedLaunchPath === undefined) {\n    url += '/#/repository?path=' + encodePath(process.cwd());\n  } else if (config.forcedLaunchPath !== null && config.forcedLaunchPath !== '') {\n    url += '/#/repository?path=' + encodePath(config.forcedLaunchPath);\n  }\n\n  if (config.launchCommand) {\n    var command = config.launchCommand.replace(/%U/g, url);\n    console.log('Running custom launch command: ' + command);\n    child_process.exec(command, function (err) {\n      if (err) {\n        callback(err);\n        return;\n      }\n      if (config.launchBrowser) {\n        openUngitBrowser(url);\n      }\n    });\n  } else if (config.launchBrowser) {\n    openUngitBrowser(url);\n  }\n}\n\nfunction checkIfUngitIsRunning(callback) {\n  // Fastest way to find out if a port is used or not/i.e. if ungit is running\n  var net = require('net');\n  var server = net.createServer();\n  server.on('error', function (e) {\n    if (e.code == 'EADDRINUSE') {\n      callback(true);\n    }\n  });\n  server.listen({ port: config.port, host: config.ungitBindIp }, function () {\n    server.close(function () {\n      callback(false);\n    });\n  });\n}\n\nvar mainWindow = null;\n\nvar appPath = app.getAppPath();\nif (!appPath.endsWith('.asar')) {\n  appPath = path.resolve(appPath, '..');\n}\n\nvar menuTemplate = [\n  {\n    label: 'File',\n    submenu: [{ role: 'quit' }],\n  },\n  {\n    label: 'Edit',\n    submenu: [\n      { role: 'undo' },\n      { role: 'redo' },\n      { type: 'separator' },\n      { role: 'cut' },\n      { role: 'copy' },\n      { role: 'paste' },\n      { role: 'delete' },\n      { type: 'separator' },\n      { role: 'selectAll' },\n    ],\n  },\n  {\n    label: 'View',\n    submenu: [\n      { role: 'reload' },\n      { role: 'forcereload' },\n      { role: 'toggledevtools' },\n      { type: 'separator' },\n      { role: 'resetzoom' },\n      { role: 'zoomin' },\n      { role: 'zoomout' },\n      { type: 'separator' },\n      { role: 'togglefullscreen' },\n    ],\n  },\n  {\n    role: 'help',\n    submenu: [\n      {\n        label: 'Learn More',\n        click: async () => {\n          await shell.openExternal('https://github.com/FredrikNoren/ungit');\n        },\n      },\n    ],\n  },\n];\n\napp.on('window-all-closed', function () {\n  app.quit();\n});\n\napp.on('ready', function () {\n  checkIfUngitIsRunning(function (ungitRunning) {\n    if (ungitRunning) {\n      dialog.showMessageBoxSync({\n        type: 'error',\n        title: 'Ungit',\n        message: 'Ungit instance is already running',\n      });\n      app.quit();\n    } else {\n      var server = require('../source/server');\n      server.started.add(function () {\n        launch(function (err) {\n          if (err) console.log(err);\n        });\n\n        var launchTime = Date.now() - startLaunchTime;\n        console.log('Took ' + launchTime + 'ms to start server.');\n      });\n\n      Menu.setApplicationMenu(Menu.buildFromTemplate(menuTemplate));\n\n      mainWindow = new BrowserWindow({\n        width: 1366,\n        height: 768,\n        icon: path.join(appPath, 'public/images/icon.png'),\n      });\n\n      mainWindow.on('closed', function () {\n        mainWindow = null;\n      });\n    }\n  });\n});\n"
  },
  {
    "path": "public/source/bootstrap.js",
    "content": "/*\n * Import the Bootstrap components individually.\n */\n\nrequire('bootstrap/js/dropdown');\nrequire('bootstrap/js/modal');\nrequire('bootstrap/js/tooltip');\n"
  },
  {
    "path": "public/source/components.js",
    "content": "const components = {};\nmodule.exports = components;\nungit.components = components;\n\ncomponents.registered = {};\n\ncomponents.register = function (name, creator) {\n  components.registered[name] = creator;\n};\n\ncomponents.create = function (name, args) {\n  var componentConstructor = components.registered[name];\n  if (!componentConstructor) throw new Error('No component found: ' + name);\n  return componentConstructor(args);\n};\n\ncomponents.showModal = (name, args) => {\n  const modal = components.create(name, args);\n  ungit.programEvents.dispatch({ event: 'modal-show-dialog', modal: modal });\n  return modal;\n};\n"
  },
  {
    "path": "public/source/jquery-ui.js",
    "content": "/*\n * Import the autocomplete widget and its dependencies.\n * The current order of the imports is required.\n */\n\n// All files require version, has to go first\nrequire('jquery-ui/ui/version');\n\n// Shared files, used by menu and autocomplete, in alphabetical order\nrequire('jquery-ui/ui/keycode');\nrequire('jquery-ui/ui/position');\nrequire('jquery-ui/ui/unique-id');\nrequire('jquery-ui/ui/widget');\n\n// Required by autocomplete, so has to go before\nrequire('jquery-ui/ui/widgets/menu');\n\n// The autocomplete widget we use\nrequire('jquery-ui/ui/widgets/autocomplete');\n"
  },
  {
    "path": "public/source/knockout-bindings.js",
    "content": "/* eslint no-unused-vars: \"off\" */\n\nvar _ = require('lodash');\nvar ko = require('knockout');\nvar $ = require('jquery');\nvar { encodePath } = require('ungit-address-parser');\nvar navigation = require('ungit-navigation');\nvar storage = require('ungit-storage');\n\nko.bindingHandlers.debug = {\n  init: function (element, valueAccessor) {\n    var value = ko.utils.unwrapObservable(valueAccessor());\n    console.log('DEBUG INIT', value);\n  },\n  update: function (element, valueAccessor, allBindingsAccessor, viewModel) {\n    var value = ko.utils.unwrapObservable(valueAccessor());\n    console.log('DEBUG UPDATE', value);\n  },\n};\n\nko.bindingHandlers.component = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel) {\n    ko.virtualElements.emptyNode(element);\n    return { controlsDescendantBindings: true };\n  },\n  update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {\n    var component = ko.utils.unwrapObservable(valueAccessor());\n    if (!component || !component.updateNode) {\n      ko.virtualElements.emptyNode(element);\n      return;\n    }\n    var node = component.updateNode(element);\n    if (node) ko.virtualElements.setDomNodeChildren(element, [node]);\n  },\n};\nko.virtualElements.allowedBindings.component = true;\n\nko.bindingHandlers.editableText = {\n  init: function (element, valueAccessor) {\n    $(element).on('blur', function () {\n      var observable = valueAccessor();\n      observable($(this).text());\n    });\n  },\n  update: function (element, valueAccessor) {\n    var value = ko.utils.unwrapObservable(valueAccessor());\n    $(element).text(value);\n  },\n};\n\nvar currentlyDraggingViewModel = null;\n\nko.bindingHandlers.dragStart = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    var value = valueAccessor();\n    element.addEventListener('dragstart', function (e) {\n      e.dataTransfer.setData('Text', 'ungit');\n      currentlyDraggingViewModel = viewModel;\n      var valueUnwrapped = ko.utils.unwrapObservable(value);\n      valueUnwrapped.call(viewModel, true);\n    });\n  },\n};\nko.bindingHandlers.dragEnd = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    var value = valueAccessor();\n    element.addEventListener('dragend', function () {\n      currentlyDraggingViewModel = null;\n      var valueUnwrapped = ko.utils.unwrapObservable(value);\n      valueUnwrapped.call(viewModel, false);\n    });\n  },\n};\n\nko.bindingHandlers.dropOver = {\n  init: function (element, valueAccessor) {\n    element.addEventListener('dragover', function (e) {\n      var value = valueAccessor();\n      var valueUnwrapped = ko.utils.unwrapObservable(value);\n      if (!valueUnwrapped) return;\n      if (e.preventDefault) e.preventDefault();\n      e.dataTransfer.dropEffect = 'move';\n      return false;\n    });\n  },\n};\n\nko.bindingHandlers.dragEnter = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    element.addEventListener('dragenter', function (e) {\n      var value = valueAccessor();\n      var valueUnwrapped = ko.utils.unwrapObservable(value);\n      valueUnwrapped.call(viewModel, currentlyDraggingViewModel);\n    });\n  },\n};\n\nko.bindingHandlers.dragLeave = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    element.addEventListener('dragleave', function (e) {\n      var value = valueAccessor();\n      var valueUnwrapped = ko.utils.unwrapObservable(value);\n      valueUnwrapped.call(viewModel, currentlyDraggingViewModel);\n    });\n  },\n};\n\nko.bindingHandlers.drop = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    var value = valueAccessor();\n    element.addEventListener('drop', function (e) {\n      if (e.preventDefault) e.preventDefault();\n      var valueUnwrapped = ko.utils.unwrapObservable(value);\n      valueUnwrapped.call(viewModel, currentlyDraggingViewModel);\n    });\n  },\n};\n\nko.bindingHandlers.shown = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    var value = valueAccessor();\n    var valueUnwrapped = ko.utils.unwrapObservable(value);\n    valueUnwrapped.call(viewModel);\n  },\n};\n\nko.bindingHandlers.element = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    var observable = valueAccessor();\n    observable(element);\n  },\n};\n\n(function scrollToEndBinding() {\n  ko.bindingHandlers.scrolledToEnd = {\n    init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n      element.valueAccessor = valueAccessor;\n      element.viewModel = viewModel;\n      element.setAttribute('data-scroll-to-end-listener', true);\n    },\n  };\n\n  var checkAtEnd = function (element) {\n    var elementEndY = $(element).offset().top + $(element).height();\n    var windowEndY = $(document).scrollTop() + document.documentElement.clientHeight;\n    if (windowEndY > elementEndY - document.documentElement.clientHeight / 2) {\n      var value = element.valueAccessor();\n      var valueUnwrapped = ko.utils.unwrapObservable(value);\n      valueUnwrapped.call(element.viewModel);\n    }\n  };\n  function scrollToEndCheck() {\n    var elems = document.querySelectorAll('[data-scroll-to-end-listener]');\n    for (var i = 0; i < elems.length; i++) checkAtEnd(elems[i]);\n  }\n\n  $(window).scroll(scrollToEndCheck);\n  $(window).resize(scrollToEndCheck);\n})();\n\n// handle focus for this element and all children. only when this element or all of its chlidren have lost focus set the value to false.\nko.bindingHandlers.hasfocus2 = {\n  init: function (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) {\n    var hasFocus = false;\n    var timeout;\n\n    ko.utils.registerEventHandler(element, 'focusin', handleElementFocusIn);\n    ko.utils.registerEventHandler(element, 'focusout', handleElementFocusOut);\n\n    function handleElementFocusIn() {\n      hasFocus = true;\n      valueAccessor()(true);\n    }\n    function handleElementFocusOut() {\n      hasFocus = false;\n\n      clearTimeout(timeout);\n      timeout = setTimeout(function () {\n        if (!hasFocus) {\n          valueAccessor()(false);\n        }\n      }, 50);\n    }\n  },\n};\n\nko.bindingHandlers.autocomplete = {\n  init: (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) => {\n    const setAutoCompleteOptions = (sources) => {\n      $(element)\n        .autocomplete({\n          classes: {\n            'ui-autocomplete': 'dropdown-menu',\n          },\n          source: sources,\n          minLength: 0,\n          messages: {\n            noResults: '',\n            results: () => {},\n          },\n        })\n        .data('ui-autocomplete')._renderItem = (ul, item) => {\n        return $('<li></li>').append($('<a>').text(item.label)).appendTo(ul);\n      };\n    };\n\n    const handleKeyEvent = (event) => {\n      const value = $(element).val();\n      const lastChar = value.slice(-1);\n      if (lastChar == ungit.config.fileSeparator) {\n        // When file separator is entered, list what is in given path, and rest auto complete options\n        ungit.server\n          .getPromise('/fs/listDirectories', { term: value })\n          .then((directoryList) => {\n            const currentDir = directoryList.shift();\n            $(element).val(\n              currentDir.endsWith(ungit.config.fileSeparator)\n                ? currentDir\n                : currentDir + ungit.config.fileSeparator\n            );\n            setAutoCompleteOptions(directoryList);\n            $(element).autocomplete('search', value);\n          })\n          .catch((err) => {\n            if (\n              !err.errorSummary.startsWith('ENOENT: no such file or directory') &&\n              err.errorCode !== 'read-dir-failed'\n            ) {\n              throw err;\n            }\n          });\n      } else if (event.keyCode === 13) {\n        // enter key is struck, navigate to the path\n        event.preventDefault();\n        navigation.browseTo(`repository?path=${encodePath(value)}`);\n      } else if (value === '' && storage.getItem('repositories')) {\n        // if path is emptied out, show save path options\n        const folderNames = JSON.parse(storage.getItem('repositories')).map((value) => {\n          return {\n            value: value,\n            label: value.substring(value.lastIndexOf(ungit.config.fileSeparator) + 1),\n          };\n        });\n        setAutoCompleteOptions(folderNames);\n        $(element).autocomplete('search', '');\n      }\n\n      return true;\n    };\n    ko.utils.registerEventHandler(element, 'keyup', _.debounce(handleKeyEvent, 100));\n  },\n};\n"
  },
  {
    "path": "public/source/main.js",
    "content": "var $ = require('jquery');\njQuery = $; // this is for old backward compatability of bootrap modules\nvar ko = require('knockout');\nvar dndPageScroll = require('dnd-page-scroll');\nrequire('./bootstrap');\nrequire('./jquery-ui');\nrequire('./knockout-bindings');\nconst winston = require('winston');\nungit.logger = winston.createLogger({\n  level: ungit.config.logLevel || 'error',\n  format: winston.format.combine(\n    winston.format.timestamp(),\n    winston.format.colorize(),\n    winston.format.printf((info) => {\n      const splat = info[Symbol.for('splat')];\n      if (splat) {\n        const splatStr = splat.map((arg) => JSON.stringify(arg)).join('\\n');\n        return `${info.timestamp} - ${info.level}: ${info.message} ${splatStr}`;\n      }\n      return `${info.timestamp} - ${info.level}: ${info.message}`;\n    })\n  ),\n  transports: [new winston.transports.Console()],\n});\nvar components = require('ungit-components');\nvar Server = require('./server');\nvar programEvents = require('ungit-program-events');\nvar navigation = require('ungit-navigation');\nvar adBlocker = require('just-detect-adblock');\n\n// Request animation frame polyfill and init tooltips\n(function () {\n  var lastTime = 0;\n  var vendors = ['ms', 'moz', 'webkit', 'o'];\n  for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {\n    window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];\n    window.cancelAnimationFrame =\n      window[vendors[x] + 'CancelAnimationFrame'] ||\n      window[vendors[x] + 'CancelRequestAnimationFrame'];\n  }\n\n  if (!window.requestAnimationFrame)\n    window.requestAnimationFrame = function (callback) {\n      var currTime = new Date().getTime();\n      var timeToCall = Math.max(0, 16 - (currTime - lastTime));\n      var id = window.setTimeout(function () {\n        callback(currTime + timeToCall);\n      }, timeToCall);\n      lastTime = currTime + timeToCall;\n      return id;\n    };\n\n  if (!window.cancelAnimationFrame)\n    window.cancelAnimationFrame = function (id) {\n      clearTimeout(id);\n    };\n\n  $(document).tooltip({\n    selector: '[data-toggle=\"tooltip\"]',\n  });\n})();\n\nfunction WindowTitle() {\n  this.path = 'ungit';\n  this.crash = false;\n}\nWindowTitle.prototype.update = function () {\n  var title = this.path\n    .replace(/\\\\/g, '/')\n    .split('/')\n    .filter(function (x) {\n      return x;\n    })\n    .reverse()\n    .join(' < ');\n  if (this.crash) title = ':( ungit crash ' + title;\n  document.title = title;\n};\n\nvar windowTitle = new WindowTitle();\nwindowTitle.update();\n\nvar AppContainerViewModel = function () {\n  this.content = ko.observable();\n};\nexports.AppContainerViewModel = AppContainerViewModel;\nAppContainerViewModel.prototype.templateChooser = function (data) {\n  if (!data) return '';\n  return data.template;\n};\n\nvar app, appContainer, server;\n\nexports.start = function () {\n  server = new Server();\n  appContainer = new AppContainerViewModel();\n  ungit.server = server;\n  app = components.create('app', { appContainer: appContainer, server: server });\n  ungit.__app = app;\n  programEvents.add(async (event) => {\n    ungit.logger.info(`received event: ${event.event}`);\n    if (event.event == 'disconnected' || event.event == 'git-crash-error') {\n      console.error(`ungit crash: ${event.event}`, event.error, event.stacktrace);\n      const err =\n        event.event == 'disconnected' && (await adBlocker.detectAnyAdblocker())\n          ? 'adblocker'\n          : event.event;\n      appContainer.content(components.create('crash', err));\n      windowTitle.crash = true;\n      windowTitle.update();\n    } else if (event.event == 'connected') {\n      appContainer.content(app);\n      windowTitle.crash = false;\n      windowTitle.update();\n    }\n\n    app.onProgramEvent(event);\n  });\n  if (ungit.config.authentication) {\n    var authenticationScreen = components.create('login', { server: server });\n    appContainer.content(authenticationScreen);\n    authenticationScreen.loggedIn.add(function () {\n      server.initSocket();\n    });\n  } else {\n    server.initSocket();\n  }\n\n  Raven.TraceKit.report.subscribe(function (event, err) {\n    programEvents.dispatch({ event: 'raven-crash', error: err || event.event });\n  });\n\n  var prevTimestamp = 0;\n  var updateAnimationFrame = function (timestamp) {\n    var delta = timestamp - prevTimestamp;\n    prevTimestamp = timestamp;\n    if (app.updateAnimationFrame) app.updateAnimationFrame(delta);\n    window.requestAnimationFrame(updateAnimationFrame);\n  };\n  window.requestAnimationFrame(updateAnimationFrame);\n\n  ko.applyBindings(appContainer);\n\n  // routing\n  navigation.crossroads.addRoute('/', function () {\n    app.content(components.create('home', { app: app }));\n    windowTitle.path = 'ungit';\n    windowTitle.update();\n  });\n\n  navigation.crossroads.addRoute('/repository{?query}', function (query) {\n    programEvents.dispatch({ event: 'navigated-to-path', path: query.path });\n    app.content(components.create('path', { server: server, path: query.path }));\n    windowTitle.path = query.path;\n    windowTitle.update();\n  });\n\n  navigation.init();\n};\n\n$(document).ready(function () {\n  dndPageScroll.default(); // Automatic page scrolling on drag-n-drop: http://www.planbox.com/blog/news/updates/html5-drag-and-drop-scrolling-the-page.html\n});\n"
  },
  {
    "path": "public/source/navigation.js",
    "content": "var programEvents = require('ungit-program-events');\n\nvar navigation = {};\nmodule.exports = navigation;\n\nvar hasher = (navigation.hasher = require('hasher'));\nvar crossroads = (navigation.crossroads = require('crossroads'));\n\nnavigation.browseTo = function (path) {\n  hasher.setHash(path);\n};\n\nnavigation.init = function () {\n  //setup hasher\n  function parseHash(newHash, oldHash) {\n    crossroads.parse(newHash);\n    programEvents.dispatch({ event: 'navigation-changed', path: newHash, oldPath: oldHash });\n  }\n  hasher.initialized.add(parseHash); //parse initial hash\n  hasher.changed.add(parseHash); //parse hash changes\n  hasher.raw = true;\n\n  hasher.init();\n};\n"
  },
  {
    "path": "public/source/program-events.js",
    "content": "const signals = require('signals');\n\nconst programEvents = new signals.Signal();\nmodule.exports = programEvents;\nungit.programEvents = programEvents;\n\nprogramEvents.add(function (event) {\n  console.log('Event:', event.event);\n});\n"
  },
  {
    "path": "public/source/server.js",
    "content": "var programEvents = require('ungit-program-events');\n\nvar rootPath = (ungit.config && ungit.config.rootPath) || '';\nvar nprogress;\nif (ungit.config.isDisableProgressBar) {\n  nprogress = {\n    start: () => {},\n    done: () => {},\n  };\n} else {\n  nprogress = require('nprogress');\n  nprogress.configure({\n    trickleRate: 0.06,\n    trickleSpeed: 200,\n    showSpinner: false,\n  });\n}\n\nfunction Server() {\n  this.isInternetConnected = true;\n  this.isUnloading = false;\n  window.addEventListener('beforeunload', () => (this.isUnloading = true));\n}\nmodule.exports = Server;\n\nServer.prototype.initSocket = function () {\n  var self = this;\n  this.socket = io('', {\n    path: rootPath + '/socket.io',\n  });\n  this.socket.on('connect_error', function (err) {\n    self._isConnected(function (connected) {\n      if (connected) throw err;\n      else self._onDisconnect(err);\n    });\n  });\n  this.socket.on('disconnect', function () {\n    self._onDisconnect();\n  });\n  this.socket.on('connected', function (data) {\n    self.socketId = data.socketId;\n    programEvents.dispatch({ event: 'connected' });\n  });\n  this.socket.on('working-tree-changed', function () {\n    programEvents.dispatch({ event: 'working-tree-changed' });\n  });\n  this.socket.on('git-directory-changed', function () {\n    programEvents.dispatch({ event: 'git-directory-changed' });\n  });\n  this.socket.on('request-credentials', function (args) {\n    self._getCredentials(function (credentials) {\n      self.socket.emit('credentials', credentials);\n    }, args);\n  });\n};\nServer.prototype._queryToString = function (query) {\n  var str = [];\n  for (var p in query)\n    if (Object.prototype.hasOwnProperty.call(query, p)) {\n      str.push(encodeURIComponent(p) + '=' + encodeURIComponent(query[p]));\n    }\n  return str.join('&');\n};\nServer.prototype._httpJsonRequest = function (request, callback) {\n  var httpRequest = new XMLHttpRequest();\n  httpRequest.onreadystatechange = function () {\n    // It seems like you can get both readyState == 0, and readyState == 4 && status == 0 when you lose connection to the server\n    if (httpRequest.readyState === 0) {\n      callback({ error: 'connection-lost' });\n    } else if (httpRequest.readyState === 4) {\n      var body;\n      try {\n        body = JSON.parse(httpRequest.responseText);\n      } catch {\n        body = null;\n      }\n      if (httpRequest.status == 0) callback({ error: 'connection-lost' });\n      else if (httpRequest.status != 200)\n        callback({ status: httpRequest.status, body: body, httpRequest: httpRequest });\n      else callback(null, body);\n    }\n  };\n  var url = request.url;\n  if (request.query) {\n    url += '?' + this._queryToString(request.query);\n  }\n  httpRequest.open(request.method, url, true);\n  httpRequest.setRequestHeader('Accept', 'application/json');\n  if (request.body) {\n    httpRequest.setRequestHeader('Content-Type', 'application/json');\n    httpRequest.send(JSON.stringify(request.body));\n  } else {\n    httpRequest.send(null);\n  }\n};\n// Check if the server is still alive\nServer.prototype._isConnected = function (callback) {\n  this._httpJsonRequest({ method: 'GET', url: rootPath + '/api/ping' }, function (err, res) {\n    callback(!err && res);\n  });\n};\nServer.prototype._onDisconnect = function (err) {\n  if (!this.isUnloading) {\n    const stacktrace = Error().stack;\n    console.warn('disconnecting...', err, stacktrace);\n    programEvents.dispatch({ event: 'disconnected', stacktrace: stacktrace, error: err });\n  }\n};\nServer.prototype._getCredentials = function (callback, args) {\n  // Push out a program event, hoping someone will respond! (Which the app component will)\n  programEvents.dispatch({ event: 'request-credentials', remote: args.remote });\n  var credentialsBinding = programEvents.add(function (event) {\n    if (event.event != 'request-credentials-response') return;\n    credentialsBinding.detach();\n    callback({ username: event.username, password: event.password });\n  });\n};\nServer.prototype.watchRepository = function (repositoryPath, callback) {\n  this.socket.emit('watch', { path: repositoryPath }, callback);\n};\nServer.prototype.queryPromise = function (method, path, body) {\n  var self = this;\n  if (body) body.socketId = this.socketId;\n  var request = {\n    method: method,\n    url: rootPath + '/api' + path,\n  };\n  if (method == 'GET' || method == 'DELETE') request.query = body;\n  else request.body = body;\n\n  nprogress.start();\n  return new Promise(function (resolve, reject) {\n    self._httpJsonRequest(request, function (error, res) {\n      if (error) {\n        if (error.error == 'connection-lost') {\n          return self._isConnected(function (connected) {\n            if (connected) {\n              reject({ errorCode: 'cross-domain-error', error: error });\n            } else {\n              self._onDisconnect(error);\n              resolve();\n            }\n          });\n        }\n        var errorSummary;\n        if (error.body) {\n          if (error.body.errorCode && error.body.errorCode != 'unknown')\n            errorSummary = error.body.errorCode;\n          else if (typeof error.body.error == 'string')\n            errorSummary = error.body.error.split('\\n')[0];\n          else if (typeof error.body.message == 'string') errorSummary = error.body.message;\n          else errorSummary = JSON.stringify(error.body.error);\n        } else {\n          errorSummary = error.httpRequest.statusText + ' ' + error.status;\n        }\n        reject({\n          errorSummary: errorSummary,\n          error: error,\n          path: path,\n          res: error,\n          errorCode: error && error.body ? error.body.errorCode : 'unknown',\n        });\n      } else {\n        resolve(res);\n      }\n    });\n  }).finally(() => nprogress.done(true));\n};\nServer.prototype.getPromise = function (url, arg) {\n  return this.queryPromise('GET', url, arg);\n};\nServer.prototype.postPromise = function (url, arg) {\n  return this.queryPromise('POST', url, arg);\n};\nServer.prototype.delPromise = function (url, arg) {\n  return this.queryPromise('DELETE', url, arg);\n};\nServer.prototype.putPromise = function (url, arg) {\n  return this.queryPromise('PUT', url, arg);\n};\n\nServer.prototype.unhandledRejection = function (err) {\n  // Show a error screen for git errors (so that people have a chance to debug them)\n  if (err.res && err.res.body && err.res.body.isGitError) {\n    programEvents.dispatch({\n      event: 'git-error',\n      data: {\n        command: err.res.body.command,\n        error: err.res.body.error,\n        stdout: err.res.body.stdout,\n        stderr: err.res.body.stderr,\n        repoPath: err.res.body.workingDirectory,\n      },\n    });\n  } else {\n    // Everything else is handled as a pure error, using the precreated error (to get a better stacktrace)\n    console.trace('Unhandled Promise ERROR: ', err, JSON.stringify(err));\n    programEvents.dispatch({ event: 'git-crash-error', error: err });\n    Raven.captureException(err);\n  }\n};\n"
  },
  {
    "path": "public/source/storage.js",
    "content": "/**\n * A wrapper around LocalStorage to support environments where LocalStorage is not available.\n * Stores and retrieves items from LocalStorage if available and uses a non-persistent cache otherwise.\n */\nvar storage;\ntry {\n  storage = {\n    getItem: localStorage.getItem.bind(localStorage),\n    setItem: localStorage.setItem.bind(localStorage),\n  };\n} catch {\n  /* Ignore Exception, use fallback implementation. */\n}\n\nif (!storage) {\n  var cache = Object.create(null);\n  storage = {\n    getItem: function (key) {\n      return cache[key] || null;\n    },\n    setItem: function (key, value) {\n      cache[key] = value;\n    },\n  };\n}\n\nmodule.exports = storage;\n"
  },
  {
    "path": "public/vendor/css/animate.css",
    "content": "@charset \"UTF-8\";\n/*\nAnimate.css - http://daneden.me/animate\nLicensed under the MIT license\n\nCopyright (c) 2013 Daniel Eden\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n*/\nbody { /* Addresses a small issue in webkit: http://bit.ly/NEdoDq */\n\t-webkit-backface-visibility: hidden;\n}\n.animated {\n\t-webkit-animation-duration: 1s;\n\t   -moz-animation-duration: 1s;\n\t     -o-animation-duration: 1s;\n\t        animation-duration: 1s;\n\t-webkit-animation-fill-mode: both;\n\t   -moz-animation-fill-mode: both;\n\t     -o-animation-fill-mode: both;\n\t        animation-fill-mode: both;\n}\n\n.animated.hinge {\n\t-webkit-animation-duration: 2s;\n\t   -moz-animation-duration: 2s;\n\t     -o-animation-duration: 2s;\n\t        animation-duration: 2s;\n}\n\n@-webkit-keyframes flash {\n\t0%, 50%, 100% {opacity: 1;}\t\n\t25%, 75% {opacity: 0;}\n}\n\n@-moz-keyframes flash {\n\t0%, 50%, 100% {opacity: 1;}\t\n\t25%, 75% {opacity: 0;}\n}\n\n@-o-keyframes flash {\n\t0%, 50%, 100% {opacity: 1;}\t\n\t25%, 75% {opacity: 0;}\n}\n\n@keyframes flash {\n\t0%, 50%, 100% {opacity: 1;}\t\n\t25%, 75% {opacity: 0;}\n}\n\n.flash {\n\t-webkit-animation-name: flash;\n\t-moz-animation-name: flash;\n\t-o-animation-name: flash;\n\tanimation-name: flash;\n}\n@-webkit-keyframes shake {\n\t0%, 100% {-webkit-transform: translateX(0);}\n\t10%, 30%, 50%, 70%, 90% {-webkit-transform: translateX(-10px);}\n\t20%, 40%, 60%, 80% {-webkit-transform: translateX(10px);}\n}\n\n@-moz-keyframes shake {\n\t0%, 100% {-moz-transform: translateX(0);}\n\t10%, 30%, 50%, 70%, 90% {-moz-transform: translateX(-10px);}\n\t20%, 40%, 60%, 80% {-moz-transform: translateX(10px);}\n}\n\n@-o-keyframes shake {\n\t0%, 100% {-o-transform: translateX(0);}\n\t10%, 30%, 50%, 70%, 90% {-o-transform: translateX(-10px);}\n\t20%, 40%, 60%, 80% {-o-transform: translateX(10px);}\n}\n\n@keyframes shake {\n\t0%, 100% {transform: translateX(0);}\n\t10%, 30%, 50%, 70%, 90% {transform: translateX(-10px);}\n\t20%, 40%, 60%, 80% {transform: translateX(10px);}\n}\n\n.shake {\n\t-webkit-animation-name: shake;\n\t-moz-animation-name: shake;\n\t-o-animation-name: shake;\n\tanimation-name: shake;\n}\n@-webkit-keyframes bounce {\n\t0%, 20%, 50%, 80%, 100% {-webkit-transform: translateY(0);}\n\t40% {-webkit-transform: translateY(-30px);}\n\t60% {-webkit-transform: translateY(-15px);}\n}\n\n@-moz-keyframes bounce {\n\t0%, 20%, 50%, 80%, 100% {-moz-transform: translateY(0);}\n\t40% {-moz-transform: translateY(-30px);}\n\t60% {-moz-transform: translateY(-15px);}\n}\n\n@-o-keyframes bounce {\n\t0%, 20%, 50%, 80%, 100% {-o-transform: translateY(0);}\n\t40% {-o-transform: translateY(-30px);}\n\t60% {-o-transform: translateY(-15px);}\n}\n@keyframes bounce {\n\t0%, 20%, 50%, 80%, 100% {transform: translateY(0);}\n\t40% {transform: translateY(-30px);}\n\t60% {transform: translateY(-15px);}\n}\n\n.bounce {\n\t-webkit-animation-name: bounce;\n\t-moz-animation-name: bounce;\n\t-o-animation-name: bounce;\n\tanimation-name: bounce;\n}\n@-webkit-keyframes tada {\n\t0% {-webkit-transform: scale(1);}\t\n\t10%, 20% {-webkit-transform: scale(0.9) rotate(-3deg);}\n\t30%, 50%, 70%, 90% {-webkit-transform: scale(1.1) rotate(3deg);}\n\t40%, 60%, 80% {-webkit-transform: scale(1.1) rotate(-3deg);}\n\t100% {-webkit-transform: scale(1) rotate(0);}\n}\n\n@-moz-keyframes tada {\n\t0% {-moz-transform: scale(1);}\t\n\t10%, 20% {-moz-transform: scale(0.9) rotate(-3deg);}\n\t30%, 50%, 70%, 90% {-moz-transform: scale(1.1) rotate(3deg);}\n\t40%, 60%, 80% {-moz-transform: scale(1.1) rotate(-3deg);}\n\t100% {-moz-transform: scale(1) rotate(0);}\n}\n\n@-o-keyframes tada {\n\t0% {-o-transform: scale(1);}\t\n\t10%, 20% {-o-transform: scale(0.9) rotate(-3deg);}\n\t30%, 50%, 70%, 90% {-o-transform: scale(1.1) rotate(3deg);}\n\t40%, 60%, 80% {-o-transform: scale(1.1) rotate(-3deg);}\n\t100% {-o-transform: scale(1) rotate(0);}\n}\n\n@keyframes tada {\n\t0% {transform: scale(1);}\t\n\t10%, 20% {transform: scale(0.9) rotate(-3deg);}\n\t30%, 50%, 70%, 90% {transform: scale(1.1) rotate(3deg);}\n\t40%, 60%, 80% {transform: scale(1.1) rotate(-3deg);}\n\t100% {transform: scale(1) rotate(0);}\n}\n\n.tada {\n\t-webkit-animation-name: tada;\n\t-moz-animation-name: tada;\n\t-o-animation-name: tada;\n\tanimation-name: tada;\n}\n@-webkit-keyframes swing {\n\t20%, 40%, 60%, 80%, 100% { -webkit-transform-origin: top center; }\n\t20% { -webkit-transform: rotate(15deg); }\t\n\t40% { -webkit-transform: rotate(-10deg); }\n\t60% { -webkit-transform: rotate(5deg); }\t\n\t80% { -webkit-transform: rotate(-5deg); }\t\n\t100% { -webkit-transform: rotate(0deg); }\n}\n\n@-moz-keyframes swing {\n\t20% { -moz-transform: rotate(15deg); }\t\n\t40% { -moz-transform: rotate(-10deg); }\n\t60% { -moz-transform: rotate(5deg); }\t\n\t80% { -moz-transform: rotate(-5deg); }\t\n\t100% { -moz-transform: rotate(0deg); }\n}\n\n@-o-keyframes swing {\n\t20% { -o-transform: rotate(15deg); }\t\n\t40% { -o-transform: rotate(-10deg); }\n\t60% { -o-transform: rotate(5deg); }\t\n\t80% { -o-transform: rotate(-5deg); }\t\n\t100% { -o-transform: rotate(0deg); }\n}\n\n@keyframes swing {\n\t20% { transform: rotate(15deg); }\t\n\t40% { transform: rotate(-10deg); }\n\t60% { transform: rotate(5deg); }\t\n\t80% { transform: rotate(-5deg); }\t\n\t100% { transform: rotate(0deg); }\n}\n\n.swing {\n\t-webkit-transform-origin: top center;\n\t-moz-transform-origin: top center;\n\t-o-transform-origin: top center;\n\ttransform-origin: top center;\n\t-webkit-animation-name: swing;\n\t-moz-animation-name: swing;\n\t-o-animation-name: swing;\n\tanimation-name: swing;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes wobble {\n  0% { -webkit-transform: translateX(0%); }\n  15% { -webkit-transform: translateX(-25%) rotate(-5deg); }\n  30% { -webkit-transform: translateX(20%) rotate(3deg); }\n  45% { -webkit-transform: translateX(-15%) rotate(-3deg); }\n  60% { -webkit-transform: translateX(10%) rotate(2deg); }\n  75% { -webkit-transform: translateX(-5%) rotate(-1deg); }\n  100% { -webkit-transform: translateX(0%); }\n}\n\n@-moz-keyframes wobble {\n  0% { -moz-transform: translateX(0%); }\n  15% { -moz-transform: translateX(-25%) rotate(-5deg); }\n  30% { -moz-transform: translateX(20%) rotate(3deg); }\n  45% { -moz-transform: translateX(-15%) rotate(-3deg); }\n  60% { -moz-transform: translateX(10%) rotate(2deg); }\n  75% { -moz-transform: translateX(-5%) rotate(-1deg); }\n  100% { -moz-transform: translateX(0%); }\n}\n\n@-o-keyframes wobble {\n  0% { -o-transform: translateX(0%); }\n  15% { -o-transform: translateX(-25%) rotate(-5deg); }\n  30% { -o-transform: translateX(20%) rotate(3deg); }\n  45% { -o-transform: translateX(-15%) rotate(-3deg); }\n  60% { -o-transform: translateX(10%) rotate(2deg); }\n  75% { -o-transform: translateX(-5%) rotate(-1deg); }\n  100% { -o-transform: translateX(0%); }\n}\n\n@keyframes wobble {\n  0% { transform: translateX(0%); }\n  15% { transform: translateX(-25%) rotate(-5deg); }\n  30% { transform: translateX(20%) rotate(3deg); }\n  45% { transform: translateX(-15%) rotate(-3deg); }\n  60% { transform: translateX(10%) rotate(2deg); }\n  75% { transform: translateX(-5%) rotate(-1deg); }\n  100% { transform: translateX(0%); }\n}\n\n.wobble {\n\t-webkit-animation-name: wobble;\n\t-moz-animation-name: wobble;\n\t-o-animation-name: wobble;\n\tanimation-name: wobble;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes pulse {\n    0% { -webkit-transform: scale(1); }\t\n\t50% { -webkit-transform: scale(1.1); }\n    100% { -webkit-transform: scale(1); }\n}\n@-moz-keyframes pulse {\n    0% { -moz-transform: scale(1); }\t\n\t50% { -moz-transform: scale(1.1); }\n    100% { -moz-transform: scale(1); }\n}\n@-o-keyframes pulse {\n    0% { -o-transform: scale(1); }\t\n\t50% { -o-transform: scale(1.1); }\n    100% { -o-transform: scale(1); }\n}\n@keyframes pulse {\n    0% { transform: scale(1); }\t\n\t50% { transform: scale(1.1); }\n    100% { transform: scale(1); }\n}\n\n.pulse {\n\t-webkit-animation-name: pulse;\n\t-moz-animation-name: pulse;\n\t-o-animation-name: pulse;\n\tanimation-name: pulse;\n}\n@-webkit-keyframes flip {\n\t0% {\n\t\t-webkit-transform: perspective(400px) rotateY(0);\n\t\t-webkit-animation-timing-function: ease-out;\n\t}\n\t40% {\n\t\t-webkit-transform: perspective(400px) translateZ(150px) rotateY(170deg);\n\t\t-webkit-animation-timing-function: ease-out;\n\t}\n\t50% {\n\t\t-webkit-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n\t\t-webkit-animation-timing-function: ease-in;\n\t}\n\t80% {\n\t\t-webkit-transform: perspective(400px) rotateY(360deg) scale(.95);\n\t\t-webkit-animation-timing-function: ease-in;\n\t}\n\t100% {\n\t\t-webkit-transform: perspective(400px) scale(1);\n\t\t-webkit-animation-timing-function: ease-in;\n\t}\n}\n@-moz-keyframes flip {\n\t0% {\n\t\t-moz-transform: perspective(400px) rotateY(0);\n\t\t-moz-animation-timing-function: ease-out;\n\t}\n\t40% {\n\t\t-moz-transform: perspective(400px) translateZ(150px) rotateY(170deg);\n\t\t-moz-animation-timing-function: ease-out;\n\t}\n\t50% {\n\t\t-moz-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n\t\t-moz-animation-timing-function: ease-in;\n\t}\n\t80% {\n\t\t-moz-transform: perspective(400px) rotateY(360deg) scale(.95);\n\t\t-moz-animation-timing-function: ease-in;\n\t}\n\t100% {\n\t\t-moz-transform: perspective(400px) scale(1);\n\t\t-moz-animation-timing-function: ease-in;\n\t}\n}\n@-o-keyframes flip {\n\t0% {\n\t\t-o-transform: perspective(400px) rotateY(0);\n\t\t-o-animation-timing-function: ease-out;\n\t}\n\t40% {\n\t\t-o-transform: perspective(400px) translateZ(150px) rotateY(170deg);\n\t\t-o-animation-timing-function: ease-out;\n\t}\n\t50% {\n\t\t-o-transform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n\t\t-o-animation-timing-function: ease-in;\n\t}\n\t80% {\n\t\t-o-transform: perspective(400px) rotateY(360deg) scale(.95);\n\t\t-o-animation-timing-function: ease-in;\n\t}\n\t100% {\n\t\t-o-transform: perspective(400px) scale(1);\n\t\t-o-animation-timing-function: ease-in;\n\t}\n}\n@keyframes flip {\n\t0% {\n\t\ttransform: perspective(400px) rotateY(0);\n\t\tanimation-timing-function: ease-out;\n\t}\n\t40% {\n\t\ttransform: perspective(400px) translateZ(150px) rotateY(170deg);\n\t\tanimation-timing-function: ease-out;\n\t}\n\t50% {\n\t\ttransform: perspective(400px) translateZ(150px) rotateY(190deg) scale(1);\n\t\tanimation-timing-function: ease-in;\n\t}\n\t80% {\n\t\ttransform: perspective(400px) rotateY(360deg) scale(.95);\n\t\tanimation-timing-function: ease-in;\n\t}\n\t100% {\n\t\ttransform: perspective(400px) scale(1);\n\t\tanimation-timing-function: ease-in;\n\t}\n}\n\n.flip {\n\t-webkit-backface-visibility: visible !important;\n\t-webkit-animation-name: flip;\n\t-moz-backface-visibility: visible !important;\n\t-moz-animation-name: flip;\n\t-o-backface-visibility: visible !important;\n\t-o-animation-name: flip;\n\tbackface-visibility: visible !important;\n\tanimation-name: flip;\n}\n@-webkit-keyframes flipInX {\n    0% {\n        -webkit-transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        -webkit-transform: perspective(400px) rotateX(-10deg);\n    }\n    \n    70% {\n        -webkit-transform: perspective(400px) rotateX(10deg);\n    }\n    \n    100% {\n        -webkit-transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n}\n@-moz-keyframes flipInX {\n    0% {\n        -moz-transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        -moz-transform: perspective(400px) rotateX(-10deg);\n    }\n    \n    70% {\n        -moz-transform: perspective(400px) rotateX(10deg);\n    }\n    \n    100% {\n        -moz-transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n}\n@-o-keyframes flipInX {\n    0% {\n        -o-transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        -o-transform: perspective(400px) rotateX(-10deg);\n    }\n    \n    70% {\n        -o-transform: perspective(400px) rotateX(10deg);\n    }\n    \n    100% {\n        -o-transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n}\n@keyframes flipInX {\n    0% {\n        transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        transform: perspective(400px) rotateX(-10deg);\n    }\n    \n    70% {\n        transform: perspective(400px) rotateX(10deg);\n    }\n    \n    100% {\n        transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n}\n\n.flipInX {\n\t-webkit-backface-visibility: visible !important;\n\t-webkit-animation-name: flipInX;\n\t-moz-backface-visibility: visible !important;\n\t-moz-animation-name: flipInX;\n\t-o-backface-visibility: visible !important;\n\t-o-animation-name: flipInX;\n\tbackface-visibility: visible !important;\n\tanimation-name: flipInX;\n}\n@-webkit-keyframes flipOutX {\n    0% {\n        -webkit-transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n\t100% {\n        -webkit-transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n}\n\n@-moz-keyframes flipOutX {\n    0% {\n        -moz-transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n\t100% {\n        -moz-transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n}\n\n@-o-keyframes flipOutX {\n    0% {\n        -o-transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n\t100% {\n        -o-transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n}\n\n@keyframes flipOutX {\n    0% {\n        transform: perspective(400px) rotateX(0deg);\n        opacity: 1;\n    }\n\t100% {\n        transform: perspective(400px) rotateX(90deg);\n        opacity: 0;\n    }\n}\n\n.flipOutX {\n\t-webkit-animation-name: flipOutX;\n\t-webkit-backface-visibility: visible !important;\n\t-moz-animation-name: flipOutX;\n\t-moz-backface-visibility: visible !important;\n\t-o-animation-name: flipOutX;\n\t-o-backface-visibility: visible !important;\n\tanimation-name: flipOutX;\n\tbackface-visibility: visible !important;\n}\n@-webkit-keyframes flipInY {\n    0% {\n        -webkit-transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        -webkit-transform: perspective(400px) rotateY(-10deg);\n    }\n    \n    70% {\n        -webkit-transform: perspective(400px) rotateY(10deg);\n    }\n    \n    100% {\n        -webkit-transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n}\n@-moz-keyframes flipInY {\n    0% {\n        -moz-transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        -moz-transform: perspective(400px) rotateY(-10deg);\n    }\n    \n    70% {\n        -moz-transform: perspective(400px) rotateY(10deg);\n    }\n    \n    100% {\n        -moz-transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n}\n@-o-keyframes flipInY {\n    0% {\n        -o-transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        -o-transform: perspective(400px) rotateY(-10deg);\n    }\n    \n    70% {\n        -o-transform: perspective(400px) rotateY(10deg);\n    }\n    \n    100% {\n        -o-transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n}\n@keyframes flipInY {\n    0% {\n        transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n    \n    40% {\n        transform: perspective(400px) rotateY(-10deg);\n    }\n    \n    70% {\n        transform: perspective(400px) rotateY(10deg);\n    }\n    \n    100% {\n        transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n}\n\n.flipInY {\n\t-webkit-backface-visibility: visible !important;\n\t-webkit-animation-name: flipInY;\n\t-moz-backface-visibility: visible !important;\n\t-moz-animation-name: flipInY;\n\t-o-backface-visibility: visible !important;\n\t-o-animation-name: flipInY;\n\tbackface-visibility: visible !important;\n\tanimation-name: flipInY;\n}\n@-webkit-keyframes flipOutY {\n    0% {\n        -webkit-transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n\t100% {\n        -webkit-transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n}\n@-moz-keyframes flipOutY {\n    0% {\n        -moz-transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n\t100% {\n        -moz-transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n}\n@-o-keyframes flipOutY {\n    0% {\n        -o-transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n\t100% {\n        -o-transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n}\n@keyframes flipOutY {\n    0% {\n        transform: perspective(400px) rotateY(0deg);\n        opacity: 1;\n    }\n\t100% {\n        transform: perspective(400px) rotateY(90deg);\n        opacity: 0;\n    }\n}\n\n.flipOutY {\n\t-webkit-backface-visibility: visible !important;\n\t-webkit-animation-name: flipOutY;\n\t-moz-backface-visibility: visible !important;\n\t-moz-animation-name: flipOutY;\n\t-o-backface-visibility: visible !important;\n\t-o-animation-name: flipOutY;\n\tbackface-visibility: visible !important;\n\tanimation-name: flipOutY;\n}\n@-webkit-keyframes fadeIn {\n\t0% {opacity: 0;}\t\n\t100% {opacity: 1;}\n}\n\n@-moz-keyframes fadeIn {\n\t0% {opacity: 0;}\t\n\t100% {opacity: 1;}\n}\n\n@-o-keyframes fadeIn {\n\t0% {opacity: 0;}\t\n\t100% {opacity: 1;}\n}\n\n@keyframes fadeIn {\n\t0% {opacity: 0;}\t\n\t100% {opacity: 1;}\n}\n\n.fadeIn {\n\t-webkit-animation-name: fadeIn;\n\t-moz-animation-name: fadeIn;\n\t-o-animation-name: fadeIn;\n\tanimation-name: fadeIn;\n}\n@-webkit-keyframes fadeInUp {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n}\n\n@-moz-keyframes fadeInUp {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n}\n\n@-o-keyframes fadeInUp {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n}\n\n@keyframes fadeInUp {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n}\n\n.fadeInUp {\n\t-webkit-animation-name: fadeInUp;\n\t-moz-animation-name: fadeInUp;\n\t-o-animation-name: fadeInUp;\n\tanimation-name: fadeInUp;\n}\n@-webkit-keyframes fadeInDown {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n}\n\n@-moz-keyframes fadeInDown {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n}\n\n@-o-keyframes fadeInDown {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n}\n\n@keyframes fadeInDown {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n}\n\n.fadeInDown {\n\t-webkit-animation-name: fadeInDown;\n\t-moz-animation-name: fadeInDown;\n\t-o-animation-name: fadeInDown;\n\tanimation-name: fadeInDown;\n}\n@-webkit-keyframes fadeInLeft {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n}\n\n@-moz-keyframes fadeInLeft {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n}\n\n@-o-keyframes fadeInLeft {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n}\n\n@keyframes fadeInLeft {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n}\n\n.fadeInLeft {\n\t-webkit-animation-name: fadeInLeft;\n\t-moz-animation-name: fadeInLeft;\n\t-o-animation-name: fadeInLeft;\n\tanimation-name: fadeInLeft;\n}\n@-webkit-keyframes fadeInRight {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n}\n\n@-moz-keyframes fadeInRight {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n}\n\n@-o-keyframes fadeInRight {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n}\n\n@keyframes fadeInRight {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n}\n\n.fadeInRight {\n\t-webkit-animation-name: fadeInRight;\n\t-moz-animation-name: fadeInRight;\n\t-o-animation-name: fadeInRight;\n\tanimation-name: fadeInRight;\n}\n@-webkit-keyframes fadeInUpBig {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n}\n\n@-moz-keyframes fadeInUpBig {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n}\n\n@-o-keyframes fadeInUpBig {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n}\n\n@keyframes fadeInUpBig {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n}\n\n.fadeInUpBig {\n\t-webkit-animation-name: fadeInUpBig;\n\t-moz-animation-name: fadeInUpBig;\n\t-o-animation-name: fadeInUpBig;\n\tanimation-name: fadeInUpBig;\n}\n@-webkit-keyframes fadeInDownBig {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n}\n\n@-moz-keyframes fadeInDownBig {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n}\n\n@-o-keyframes fadeInDownBig {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n}\n\n@keyframes fadeInDownBig {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n}\n\n.fadeInDownBig {\n\t-webkit-animation-name: fadeInDownBig;\n\t-moz-animation-name: fadeInDownBig;\n\t-o-animation-name: fadeInDownBig;\n\tanimation-name: fadeInDownBig;\n}\n@-webkit-keyframes fadeInLeftBig {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n}\n@-moz-keyframes fadeInLeftBig {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n}\n@-o-keyframes fadeInLeftBig {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n}\n@keyframes fadeInLeftBig {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateX(-2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n}\n\n.fadeInLeftBig {\n\t-webkit-animation-name: fadeInLeftBig;\n\t-moz-animation-name: fadeInLeftBig;\n\t-o-animation-name: fadeInLeftBig;\n\tanimation-name: fadeInLeftBig;\n}\n@-webkit-keyframes fadeInRightBig {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n}\n\n@-moz-keyframes fadeInRightBig {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n}\n\n@-o-keyframes fadeInRightBig {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n}\n\n@keyframes fadeInRightBig {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateX(2000px);\n\t}\n\t\n\t100% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n}\n\n.fadeInRightBig {\n\t-webkit-animation-name: fadeInRightBig;\n\t-moz-animation-name: fadeInRightBig;\n\t-o-animation-name: fadeInRightBig;\n\tanimation-name: fadeInRightBig;\n}\n@-webkit-keyframes fadeOut {\n\t0% {opacity: 1;}\n\t100% {opacity: 0;}\n}\n\n@-moz-keyframes fadeOut {\n\t0% {opacity: 1;}\n\t100% {opacity: 0;}\n}\n\n@-o-keyframes fadeOut {\n\t0% {opacity: 1;}\n\t100% {opacity: 0;}\n}\n\n@keyframes fadeOut {\n\t0% {opacity: 1;}\n\t100% {opacity: 0;}\n}\n\n.fadeOut {\n\t-webkit-animation-name: fadeOut;\n\t-moz-animation-name: fadeOut;\n\t-o-animation-name: fadeOut;\n\tanimation-name: fadeOut;\n}\n@-webkit-keyframes fadeOutUp {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(-20px);\n\t}\n}\n@-moz-keyframes fadeOutUp {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(-20px);\n\t}\n}\n@-o-keyframes fadeOutUp {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(-20px);\n\t}\n}\n@keyframes fadeOutUp {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateY(-20px);\n\t}\n}\n\n.fadeOutUp {\n\t-webkit-animation-name: fadeOutUp;\n\t-moz-animation-name: fadeOutUp;\n\t-o-animation-name: fadeOutUp;\n\tanimation-name: fadeOutUp;\n}\n@-webkit-keyframes fadeOutDown {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(20px);\n\t}\n}\n\n@-moz-keyframes fadeOutDown {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(20px);\n\t}\n}\n\n@-o-keyframes fadeOutDown {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(20px);\n\t}\n}\n\n@keyframes fadeOutDown {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateY(20px);\n\t}\n}\n\n.fadeOutDown {\n\t-webkit-animation-name: fadeOutDown;\n\t-moz-animation-name: fadeOutDown;\n\t-o-animation-name: fadeOutDown;\n\tanimation-name: fadeOutDown;\n}\n@-webkit-keyframes fadeOutLeft {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(-20px);\n\t}\n}\n\n@-moz-keyframes fadeOutLeft {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(-20px);\n\t}\n}\n\n@-o-keyframes fadeOutLeft {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(-20px);\n\t}\n}\n\n@keyframes fadeOutLeft {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateX(-20px);\n\t}\n}\n\n.fadeOutLeft {\n\t-webkit-animation-name: fadeOutLeft;\n\t-moz-animation-name: fadeOutLeft;\n\t-o-animation-name: fadeOutLeft;\n\tanimation-name: fadeOutLeft;\n}\n@-webkit-keyframes fadeOutRight {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(20px);\n\t}\n}\n\n@-moz-keyframes fadeOutRight {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(20px);\n\t}\n}\n\n@-o-keyframes fadeOutRight {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(20px);\n\t}\n}\n\n@keyframes fadeOutRight {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateX(20px);\n\t}\n}\n\n.fadeOutRight {\n\t-webkit-animation-name: fadeOutRight;\n\t-moz-animation-name: fadeOutRight;\n\t-o-animation-name: fadeOutRight;\n\tanimation-name: fadeOutRight;\n}\n@-webkit-keyframes fadeOutUpBig {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(-2000px);\n\t}\n}\n\n@-moz-keyframes fadeOutUpBig {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(-2000px);\n\t}\n}\n\n@-o-keyframes fadeOutUpBig {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(-2000px);\n\t}\n}\n\n@keyframes fadeOutUpBig {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateY(-2000px);\n\t}\n}\n\n.fadeOutUpBig {\n\t-webkit-animation-name: fadeOutUpBig;\n\t-moz-animation-name: fadeOutUpBig;\n\t-o-animation-name: fadeOutUpBig;\n\tanimation-name: fadeOutUpBig;\n}\n@-webkit-keyframes fadeOutDownBig {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(2000px);\n\t}\n}\n\n@-moz-keyframes fadeOutDownBig {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(2000px);\n\t}\n}\n\n@-o-keyframes fadeOutDownBig {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(2000px);\n\t}\n}\n\n@keyframes fadeOutDownBig {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateY(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateY(2000px);\n\t}\n}\n\n.fadeOutDownBig {\n\t-webkit-animation-name: fadeOutDownBig;\n\t-moz-animation-name: fadeOutDownBig;\n\t-o-animation-name: fadeOutDownBig;\n\tanimation-name: fadeOutDownBig;\n}\n@-webkit-keyframes fadeOutLeftBig {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(-2000px);\n\t}\n}\n\n@-moz-keyframes fadeOutLeftBig {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(-2000px);\n\t}\n}\n\n@-o-keyframes fadeOutLeftBig {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(-2000px);\n\t}\n}\n\n@keyframes fadeOutLeftBig {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateX(-2000px);\n\t}\n}\n\n.fadeOutLeftBig {\n\t-webkit-animation-name: fadeOutLeftBig;\n\t-moz-animation-name: fadeOutLeftBig;\n\t-o-animation-name: fadeOutLeftBig;\n\tanimation-name: fadeOutLeftBig;\n}\n@-webkit-keyframes fadeOutRightBig {\n\t0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(2000px);\n\t}\n}\n@-moz-keyframes fadeOutRightBig {\n\t0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(2000px);\n\t}\n}\n@-o-keyframes fadeOutRightBig {\n\t0% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(2000px);\n\t}\n}\n@keyframes fadeOutRightBig {\n\t0% {\n\t\topacity: 1;\n\t\ttransform: translateX(0);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateX(2000px);\n\t}\n}\n\n.fadeOutRightBig {\n\t-webkit-animation-name: fadeOutRightBig;\n\t-moz-animation-name: fadeOutRightBig;\n\t-o-animation-name: fadeOutRightBig;\n\tanimation-name: fadeOutRightBig;\n}\n@-webkit-keyframes bounceIn {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: scale(.3);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\t-webkit-transform: scale(1.05);\n\t}\n\t\n\t70% {\n\t\t-webkit-transform: scale(.9);\n\t}\n\t\n\t100% {\n\t\t-webkit-transform: scale(1);\n\t}\n}\n\n@-moz-keyframes bounceIn {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: scale(.3);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\t-moz-transform: scale(1.05);\n\t}\n\t\n\t70% {\n\t\t-moz-transform: scale(.9);\n\t}\n\t\n\t100% {\n\t\t-moz-transform: scale(1);\n\t}\n}\n\n@-o-keyframes bounceIn {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: scale(.3);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\t-o-transform: scale(1.05);\n\t}\n\t\n\t70% {\n\t\t-o-transform: scale(.9);\n\t}\n\t\n\t100% {\n\t\t-o-transform: scale(1);\n\t}\n}\n\n@keyframes bounceIn {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: scale(.3);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\ttransform: scale(1.05);\n\t}\n\t\n\t70% {\n\t\ttransform: scale(.9);\n\t}\n\t\n\t100% {\n\t\ttransform: scale(1);\n\t}\n}\n\n.bounceIn {\n\t-webkit-animation-name: bounceIn;\n\t-moz-animation-name: bounceIn;\n\t-o-animation-name: bounceIn;\n\tanimation-name: bounceIn;\n}\n@-webkit-keyframes bounceInUp {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(-30px);\n\t}\n\t\n\t80% {\n\t\t-webkit-transform: translateY(10px);\n\t}\n\t\n\t100% {\n\t\t-webkit-transform: translateY(0);\n\t}\n}\n@-moz-keyframes bounceInUp {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(-30px);\n\t}\n\t\n\t80% {\n\t\t-moz-transform: translateY(10px);\n\t}\n\t\n\t100% {\n\t\t-moz-transform: translateY(0);\n\t}\n}\n\n@-o-keyframes bounceInUp {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(-30px);\n\t}\n\t\n\t80% {\n\t\t-o-transform: translateY(10px);\n\t}\n\t\n\t100% {\n\t\t-o-transform: translateY(0);\n\t}\n}\n\n@keyframes bounceInUp {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\ttransform: translateY(-30px);\n\t}\n\t\n\t80% {\n\t\ttransform: translateY(10px);\n\t}\n\t\n\t100% {\n\t\ttransform: translateY(0);\n\t}\n}\n\n.bounceInUp {\n\t-webkit-animation-name: bounceInUp;\n\t-moz-animation-name: bounceInUp;\n\t-o-animation-name: bounceInUp;\n\tanimation-name: bounceInUp;\n}\n@-webkit-keyframes bounceInDown {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(30px);\n\t}\n\t\n\t80% {\n\t\t-webkit-transform: translateY(-10px);\n\t}\n\t\n\t100% {\n\t\t-webkit-transform: translateY(0);\n\t}\n}\n\n@-moz-keyframes bounceInDown {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(30px);\n\t}\n\t\n\t80% {\n\t\t-moz-transform: translateY(-10px);\n\t}\n\t\n\t100% {\n\t\t-moz-transform: translateY(0);\n\t}\n}\n\n@-o-keyframes bounceInDown {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(30px);\n\t}\n\t\n\t80% {\n\t\t-o-transform: translateY(-10px);\n\t}\n\t\n\t100% {\n\t\t-o-transform: translateY(0);\n\t}\n}\n\n@keyframes bounceInDown {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateY(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\ttransform: translateY(30px);\n\t}\n\t\n\t80% {\n\t\ttransform: translateY(-10px);\n\t}\n\t\n\t100% {\n\t\ttransform: translateY(0);\n\t}\n}\n\n.bounceInDown {\n\t-webkit-animation-name: bounceInDown;\n\t-moz-animation-name: bounceInDown;\n\t-o-animation-name: bounceInDown;\n\tanimation-name: bounceInDown;\n}\n@-webkit-keyframes bounceInLeft {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(30px);\n\t}\n\t\n\t80% {\n\t\t-webkit-transform: translateX(-10px);\n\t}\n\t\n\t100% {\n\t\t-webkit-transform: translateX(0);\n\t}\n}\n\n@-moz-keyframes bounceInLeft {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(30px);\n\t}\n\t\n\t80% {\n\t\t-moz-transform: translateX(-10px);\n\t}\n\t\n\t100% {\n\t\t-moz-transform: translateX(0);\n\t}\n}\n\n@-o-keyframes bounceInLeft {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(30px);\n\t}\n\t\n\t80% {\n\t\t-o-transform: translateX(-10px);\n\t}\n\t\n\t100% {\n\t\t-o-transform: translateX(0);\n\t}\n}\n\n@keyframes bounceInLeft {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateX(-2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\ttransform: translateX(30px);\n\t}\n\t\n\t80% {\n\t\ttransform: translateX(-10px);\n\t}\n\t\n\t100% {\n\t\ttransform: translateX(0);\n\t}\n}\n\n.bounceInLeft {\n\t-webkit-animation-name: bounceInLeft;\n\t-moz-animation-name: bounceInLeft;\n\t-o-animation-name: bounceInLeft;\n\tanimation-name: bounceInLeft;\n}\n@-webkit-keyframes bounceInRight {\n\t0% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(-30px);\n\t}\n\t\n\t80% {\n\t\t-webkit-transform: translateX(10px);\n\t}\n\t\n\t100% {\n\t\t-webkit-transform: translateX(0);\n\t}\n}\n\n@-moz-keyframes bounceInRight {\n\t0% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(-30px);\n\t}\n\t\n\t80% {\n\t\t-moz-transform: translateX(10px);\n\t}\n\t\n\t100% {\n\t\t-moz-transform: translateX(0);\n\t}\n}\n\n@-o-keyframes bounceInRight {\n\t0% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(-30px);\n\t}\n\t\n\t80% {\n\t\t-o-transform: translateX(10px);\n\t}\n\t\n\t100% {\n\t\t-o-transform: translateX(0);\n\t}\n}\n\n@keyframes bounceInRight {\n\t0% {\n\t\topacity: 0;\n\t\ttransform: translateX(2000px);\n\t}\n\t\n\t60% {\n\t\topacity: 1;\n\t\ttransform: translateX(-30px);\n\t}\n\t\n\t80% {\n\t\ttransform: translateX(10px);\n\t}\n\t\n\t100% {\n\t\ttransform: translateX(0);\n\t}\n}\n\n.bounceInRight {\n\t-webkit-animation-name: bounceInRight;\n\t-moz-animation-name: bounceInRight;\n\t-o-animation-name: bounceInRight;\n\tanimation-name: bounceInRight;\n}\n@-webkit-keyframes bounceOut {\n\t0% {\n\t\t-webkit-transform: scale(1);\n\t}\n\t\n\t25% {\n\t\t-webkit-transform: scale(.95);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\t-webkit-transform: scale(1.1);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: scale(.3);\n\t}\t\n}\n\n@-moz-keyframes bounceOut {\n\t0% {\n\t\t-moz-transform: scale(1);\n\t}\n\t\n\t25% {\n\t\t-moz-transform: scale(.95);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\t-moz-transform: scale(1.1);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: scale(.3);\n\t}\t\n}\n\n@-o-keyframes bounceOut {\n\t0% {\n\t\t-o-transform: scale(1);\n\t}\n\t\n\t25% {\n\t\t-o-transform: scale(.95);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\t-o-transform: scale(1.1);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: scale(.3);\n\t}\t\n}\n\n@keyframes bounceOut {\n\t0% {\n\t\ttransform: scale(1);\n\t}\n\t\n\t25% {\n\t\ttransform: scale(.95);\n\t}\n\t\n\t50% {\n\t\topacity: 1;\n\t\ttransform: scale(1.1);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: scale(.3);\n\t}\t\n}\n\n.bounceOut {\n\t-webkit-animation-name: bounceOut;\n\t-moz-animation-name: bounceOut;\n\t-o-animation-name: bounceOut;\n\tanimation-name: bounceOut;\n}\n@-webkit-keyframes bounceOutUp {\n\t0% {\n\t\t-webkit-transform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(-2000px);\n\t}\n}\n\n@-moz-keyframes bounceOutUp {\n\t0% {\n\t\t-moz-transform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(-2000px);\n\t}\n}\n\n@-o-keyframes bounceOutUp {\n\t0% {\n\t\t-o-transform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(-2000px);\n\t}\n}\n\n@keyframes bounceOutUp {\n\t0% {\n\t\ttransform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\ttransform: translateY(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateY(-2000px);\n\t}\n}\n\n.bounceOutUp {\n\t-webkit-animation-name: bounceOutUp;\n\t-moz-animation-name: bounceOutUp;\n\t-o-animation-name: bounceOutUp;\n\tanimation-name: bounceOutUp;\n}\n@-webkit-keyframes bounceOutDown {\n\t0% {\n\t\t-webkit-transform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateY(2000px);\n\t}\n}\n\n@-moz-keyframes bounceOutDown {\n\t0% {\n\t\t-moz-transform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-moz-transform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateY(2000px);\n\t}\n}\n\n@-o-keyframes bounceOutDown {\n\t0% {\n\t\t-o-transform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-o-transform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateY(2000px);\n\t}\n}\n\n@keyframes bounceOutDown {\n\t0% {\n\t\ttransform: translateY(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\ttransform: translateY(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateY(2000px);\n\t}\n}\n\n.bounceOutDown {\n\t-webkit-animation-name: bounceOutDown;\n\t-moz-animation-name: bounceOutDown;\n\t-o-animation-name: bounceOutDown;\n\tanimation-name: bounceOutDown;\n}\n@-webkit-keyframes bounceOutLeft {\n\t0% {\n\t\t-webkit-transform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(-2000px);\n\t}\n}\n\n@-moz-keyframes bounceOutLeft {\n\t0% {\n\t\t-moz-transform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(-2000px);\n\t}\n}\n\n@-o-keyframes bounceOutLeft {\n\t0% {\n\t\t-o-transform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(-2000px);\n\t}\n}\n\n@keyframes bounceOutLeft {\n\t0% {\n\t\ttransform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\ttransform: translateX(20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateX(-2000px);\n\t}\n}\n\n.bounceOutLeft {\n\t-webkit-animation-name: bounceOutLeft;\n\t-moz-animation-name: bounceOutLeft;\n\t-o-animation-name: bounceOutLeft;\n\tanimation-name: bounceOutLeft;\n}\n@-webkit-keyframes bounceOutRight {\n\t0% {\n\t\t-webkit-transform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(2000px);\n\t}\n}\n\n@-moz-keyframes bounceOutRight {\n\t0% {\n\t\t-moz-transform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(2000px);\n\t}\n}\n\n@-o-keyframes bounceOutRight {\n\t0% {\n\t\t-o-transform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(2000px);\n\t}\n}\n\n@keyframes bounceOutRight {\n\t0% {\n\t\ttransform: translateX(0);\n\t}\n\t\n\t20% {\n\t\topacity: 1;\n\t\ttransform: translateX(-20px);\n\t}\n\t\n\t100% {\n\t\topacity: 0;\n\t\ttransform: translateX(2000px);\n\t}\n}\n\n.bounceOutRight {\n\t-webkit-animation-name: bounceOutRight;\n\t-moz-animation-name: bounceOutRight;\n\t-o-animation-name: bounceOutRight;\n\tanimation-name: bounceOutRight;\n}\n@-webkit-keyframes rotateIn {\n\t0% {\n\t\t-webkit-transform-origin: center center;\n\t\t-webkit-transform: rotate(-200deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: center center;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n@-moz-keyframes rotateIn {\n\t0% {\n\t\t-moz-transform-origin: center center;\n\t\t-moz-transform: rotate(-200deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: center center;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n@-o-keyframes rotateIn {\n\t0% {\n\t\t-o-transform-origin: center center;\n\t\t-o-transform: rotate(-200deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: center center;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n@keyframes rotateIn {\n\t0% {\n\t\ttransform-origin: center center;\n\t\ttransform: rotate(-200deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: center center;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n.rotateIn {\n\t-webkit-animation-name: rotateIn;\n\t-moz-animation-name: rotateIn;\n\t-o-animation-name: rotateIn;\n\tanimation-name: rotateIn;\n}\n@-webkit-keyframes rotateInUpLeft {\n\t0% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-moz-keyframes rotateInUpLeft {\n\t0% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-o-keyframes rotateInUpLeft {\n\t0% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@keyframes rotateInUpLeft {\n\t0% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n.rotateInUpLeft {\n\t-webkit-animation-name: rotateInUpLeft;\n\t-moz-animation-name: rotateInUpLeft;\n\t-o-animation-name: rotateInUpLeft;\n\tanimation-name: rotateInUpLeft;\n}\n@-webkit-keyframes rotateInDownLeft {\n\t0% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-moz-keyframes rotateInDownLeft {\n\t0% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-o-keyframes rotateInDownLeft {\n\t0% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@keyframes rotateInDownLeft {\n\t0% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n.rotateInDownLeft {\n\t-webkit-animation-name: rotateInDownLeft;\n\t-moz-animation-name: rotateInDownLeft;\n\t-o-animation-name: rotateInDownLeft;\n\tanimation-name: rotateInDownLeft;\n}\n@-webkit-keyframes rotateInUpRight {\n\t0% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-moz-keyframes rotateInUpRight {\n\t0% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-o-keyframes rotateInUpRight {\n\t0% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@keyframes rotateInUpRight {\n\t0% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n.rotateInUpRight {\n\t-webkit-animation-name: rotateInUpRight;\n\t-moz-animation-name: rotateInUpRight;\n\t-o-animation-name: rotateInUpRight;\n\tanimation-name: rotateInUpRight;\n}\n@-webkit-keyframes rotateInDownRight {\n\t0% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-moz-keyframes rotateInDownRight {\n\t0% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@-o-keyframes rotateInDownRight {\n\t0% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n@keyframes rotateInDownRight {\n\t0% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(90deg);\n\t\topacity: 0;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n}\n\n.rotateInDownRight {\n\t-webkit-animation-name: rotateInDownRight;\n\t-moz-animation-name: rotateInDownRight;\n\t-o-animation-name: rotateInDownRight;\n\tanimation-name: rotateInDownRight;\n}\n@-webkit-keyframes rotateOut {\n\t0% {\n\t\t-webkit-transform-origin: center center;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: center center;\n\t\t-webkit-transform: rotate(200deg);\n\t\topacity: 0;\n\t}\n}\n\n@-moz-keyframes rotateOut {\n\t0% {\n\t\t-moz-transform-origin: center center;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: center center;\n\t\t-moz-transform: rotate(200deg);\n\t\topacity: 0;\n\t}\n}\n\n@-o-keyframes rotateOut {\n\t0% {\n\t\t-o-transform-origin: center center;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: center center;\n\t\t-o-transform: rotate(200deg);\n\t\topacity: 0;\n\t}\n}\n\n@keyframes rotateOut {\n\t0% {\n\t\ttransform-origin: center center;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: center center;\n\t\ttransform: rotate(200deg);\n\t\topacity: 0;\n\t}\n}\n\n.rotateOut {\n\t-webkit-animation-name: rotateOut;\n\t-moz-animation-name: rotateOut;\n\t-o-animation-name: rotateOut;\n\tanimation-name: rotateOut;\n}\n@-webkit-keyframes rotateOutUpLeft {\n\t0% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-moz-keyframes rotateOutUpLeft {\n\t0% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-o-keyframes rotateOutUpLeft {\n\t0% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n@keyframes rotateOutUpLeft {\n\t0% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n.rotateOutUpLeft {\n\t-webkit-animation-name: rotateOutUpLeft;\n\t-moz-animation-name: rotateOutUpLeft;\n\t-o-animation-name: rotateOutUpLeft;\n\tanimation-name: rotateOutUpLeft;\n}\n@-webkit-keyframes rotateOutDownLeft {\n\t0% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: left bottom;\n\t\t-webkit-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-moz-keyframes rotateOutDownLeft {\n\t0% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: left bottom;\n\t\t-moz-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-o-keyframes rotateOutDownLeft {\n\t0% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: left bottom;\n\t\t-o-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n@keyframes rotateOutDownLeft {\n\t0% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: left bottom;\n\t\ttransform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n.rotateOutDownLeft {\n\t-webkit-animation-name: rotateOutDownLeft;\n\t-moz-animation-name: rotateOutDownLeft;\n\t-o-animation-name: rotateOutDownLeft;\n\tanimation-name: rotateOutDownLeft;\n}\n@-webkit-keyframes rotateOutUpRight {\n\t0% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-moz-keyframes rotateOutUpRight {\n\t0% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-o-keyframes rotateOutUpRight {\n\t0% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n@keyframes rotateOutUpRight {\n\t0% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(90deg);\n\t\topacity: 0;\n\t}\n}\n\n.rotateOutUpRight {\n\t-webkit-animation-name: rotateOutUpRight;\n\t-moz-animation-name: rotateOutUpRight;\n\t-o-animation-name: rotateOutUpRight;\n\tanimation-name: rotateOutUpRight;\n}\n@-webkit-keyframes rotateOutDownRight {\n\t0% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-webkit-transform-origin: right bottom;\n\t\t-webkit-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-moz-keyframes rotateOutDownRight {\n\t0% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-moz-transform-origin: right bottom;\n\t\t-moz-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n@-o-keyframes rotateOutDownRight {\n\t0% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\t-o-transform-origin: right bottom;\n\t\t-o-transform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n@keyframes rotateOutDownRight {\n\t0% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(0);\n\t\topacity: 1;\n\t}\n\t\n\t100% {\n\t\ttransform-origin: right bottom;\n\t\ttransform: rotate(-90deg);\n\t\topacity: 0;\n\t}\n}\n\n.rotateOutDownRight {\n\t-webkit-animation-name: rotateOutDownRight;\n\t-moz-animation-name: rotateOutDownRight;\n\t-o-animation-name: rotateOutDownRight;\n\tanimation-name: rotateOutDownRight;\n}\n@-webkit-keyframes hinge {\n\t0% { -webkit-transform: rotate(0); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; }\t\n\t20%, 60% { -webkit-transform: rotate(80deg); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; }\t\n\t40% { -webkit-transform: rotate(60deg); -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; }\t\n\t80% { -webkit-transform: rotate(60deg) translateY(0); opacity: 1; -webkit-transform-origin: top left; -webkit-animation-timing-function: ease-in-out; }\t\n\t100% { -webkit-transform: translateY(700px); opacity: 0; }\n}\n\n@-moz-keyframes hinge {\n\t0% { -moz-transform: rotate(0); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; }\t\n\t20%, 60% { -moz-transform: rotate(80deg); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; }\t\n\t40% { -moz-transform: rotate(60deg); -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; }\t\n\t80% { -moz-transform: rotate(60deg) translateY(0); opacity: 1; -moz-transform-origin: top left; -moz-animation-timing-function: ease-in-out; }\t\n\t100% { -moz-transform: translateY(700px); opacity: 0; }\n}\n\n@-o-keyframes hinge {\n\t0% { -o-transform: rotate(0); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; }\t\n\t20%, 60% { -o-transform: rotate(80deg); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; }\t\n\t40% { -o-transform: rotate(60deg); -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; }\t\n\t80% { -o-transform: rotate(60deg) translateY(0); opacity: 1; -o-transform-origin: top left; -o-animation-timing-function: ease-in-out; }\t\n\t100% { -o-transform: translateY(700px); opacity: 0; }\n}\n\n@keyframes hinge {\n\t0% { transform: rotate(0); transform-origin: top left; animation-timing-function: ease-in-out; }\t\n\t20%, 60% { transform: rotate(80deg); transform-origin: top left; animation-timing-function: ease-in-out; }\t\n\t40% { transform: rotate(60deg); transform-origin: top left; animation-timing-function: ease-in-out; }\t\n\t80% { transform: rotate(60deg) translateY(0); opacity: 1; transform-origin: top left; animation-timing-function: ease-in-out; }\t\n\t100% { transform: translateY(700px); opacity: 0; }\n}\n\n.hinge {\n\t-webkit-animation-name: hinge;\n\t-moz-animation-name: hinge;\n\t-o-animation-name: hinge;\n\tanimation-name: hinge;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes rollIn {\n\t0% { opacity: 0; -webkit-transform: translateX(-100%) rotate(-120deg); }\n\t100% { opacity: 1; -webkit-transform: translateX(0px) rotate(0deg); }\n}\n\n@-moz-keyframes rollIn {\n\t0% { opacity: 0; -moz-transform: translateX(-100%) rotate(-120deg); }\n\t100% { opacity: 1; -moz-transform: translateX(0px) rotate(0deg); }\n}\n\n@-o-keyframes rollIn {\n\t0% { opacity: 0; -o-transform: translateX(-100%) rotate(-120deg); }\n\t100% { opacity: 1; -o-transform: translateX(0px) rotate(0deg); }\n}\n\n@keyframes rollIn {\n\t0% { opacity: 0; transform: translateX(-100%) rotate(-120deg); }\n\t100% { opacity: 1; transform: translateX(0px) rotate(0deg); }\n}\n\n.rollIn {\n\t-webkit-animation-name: rollIn;\n\t-moz-animation-name: rollIn;\n\t-o-animation-name: rollIn;\n\tanimation-name: rollIn;\n}\n/* originally authored by Nick Pettit - https://github.com/nickpettit/glide */\n\n@-webkit-keyframes rollOut {\n    0% {\n\t\topacity: 1;\n\t\t-webkit-transform: translateX(0px) rotate(0deg);\n\t}\n\n    100% {\n\t\topacity: 0;\n\t\t-webkit-transform: translateX(100%) rotate(120deg);\n\t}\n}\n\n@-moz-keyframes rollOut {\n    0% {\n\t\topacity: 1;\n\t\t-moz-transform: translateX(0px) rotate(0deg);\n\t}\n\n    100% {\n\t\topacity: 0;\n\t\t-moz-transform: translateX(100%) rotate(120deg);\n\t}\n}\n\n@-o-keyframes rollOut {\n    0% {\n\t\topacity: 1;\n\t\t-o-transform: translateX(0px) rotate(0deg);\n\t}\n\n    100% {\n\t\topacity: 0;\n\t\t-o-transform: translateX(100%) rotate(120deg);\n\t}\n}\n\n@keyframes rollOut {\n    0% {\n\t\topacity: 1;\n\t\ttransform: translateX(0px) rotate(0deg);\n\t}\n\n    100% {\n\t\topacity: 0;\n\t\ttransform: translateX(100%) rotate(120deg);\n\t}\n}\n\n.rollOut {\n\t-webkit-animation-name: rollOut;\n\t-moz-animation-name: rollOut;\n\t-o-animation-name: rollOut;\n\tanimation-name: rollOut;\n}\n\n/* originally authored by Angelo Rohit - https://github.com/angelorohit */\n\n@-webkit-keyframes lightSpeedIn {\n\t0% { -webkit-transform: translateX(100%) skewX(-30deg); opacity: 0; }\n\t60% { -webkit-transform: translateX(-20%) skewX(30deg); opacity: 1; }\n\t80% { -webkit-transform: translateX(0%) skewX(-15deg); opacity: 1; }\n\t100% { -webkit-transform: translateX(0%) skewX(0deg); opacity: 1; }\n}\n\n@-moz-keyframes lightSpeedIn {\n\t0% { -moz-transform: translateX(100%) skewX(-30deg); opacity: 0; }\n\t60% { -moz-transform: translateX(-20%) skewX(30deg); opacity: 1; }\n\t80% { -moz-transform: translateX(0%) skewX(-15deg); opacity: 1; }\n\t100% { -moz-transform: translateX(0%) skewX(0deg); opacity: 1; }\n}\n\n@-o-keyframes lightSpeedIn {\n\t0% { -o-transform: translateX(100%) skewX(-30deg); opacity: 0; }\n\t60% { -o-transform: translateX(-20%) skewX(30deg); opacity: 1; }\n\t80% { -o-transform: translateX(0%) skewX(-15deg); opacity: 1; }\n\t100% { -o-transform: translateX(0%) skewX(0deg); opacity: 1; }\n}\n\n@keyframes lightSpeedIn {\n\t0% { transform: translateX(100%) skewX(-30deg); opacity: 0; }\n\t60% { transform: translateX(-20%) skewX(30deg); opacity: 1; }\n\t80% { transform: translateX(0%) skewX(-15deg); opacity: 1; }\n\t100% { transform: translateX(0%) skewX(0deg); opacity: 1; }\n}\n\n.lightSpeedIn {\n    -webkit-animation-name: lightSpeedIn;\n    -moz-animation-name: lightSpeedIn;\n    -o-animation-name: lightSpeedIn;\n    animation-name: lightSpeedIn;\n\n    -webkit-animation-timing-function: ease-out;\n    -moz-animation-timing-function: ease-out;\n    -o-animation-timing-function: ease-out;\n    animation-timing-function: ease-out;\n}\n\n.animated.lightSpeedIn {\n    -webkit-animation-duration: 0.5s;\n    -moz-animation-duration: 0.5s;\n    -o-animation-duration: 0.5s;\n    animation-duration: 0.5s;\n}\n\n/* originally authored by Angelo Rohit - https://github.com/angelorohit */\n\n@-webkit-keyframes lightSpeedOut {\n    0% { -webkit-transform: translateX(0%) skewX(0deg); opacity: 1; }\n\t100% { -webkit-transform: translateX(100%) skewX(-30deg); opacity: 0; }\n}\n\n@-moz-keyframes lightSpeedOut {\n\t0% { -moz-transform: translateX(0%) skewX(0deg); opacity: 1; }\n\t100% { -moz-transform: translateX(100%) skewX(-30deg); opacity: 0; }\n}\n\n@-o-keyframes lightSpeedOut {\n\t0% { -o-transform: translateX(0%) skewX(0deg); opacity: 1; }\n\t100% { -o-transform: translateX(100%) skewX(-30deg); opacity: 0; }\n}\n\n@keyframes lightSpeedOut {\n\t0% { transform: translateX(0%) skewX(0deg); opacity: 1; }\n\t100% { transform: translateX(100%) skewX(-30deg); opacity: 0; }\n}\n\n.lightSpeedOut {\n    -webkit-animation-name: lightSpeedOut;\n    -moz-animation-name: lightSpeedOut;\n    -o-animation-name: lightSpeedOut;\n    animation-name: lightSpeedOut;\n\n    -webkit-animation-timing-function: ease-in;\n    -moz-animation-timing-function: ease-in;\n    -o-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n}\n\n.animated.lightSpeedOut {\n    -webkit-animation-duration: 0.25s;\n    -moz-animation-duration: 0.25s;\n    -o-animation-duration: 0.25s;\n    animation-duration: 0.25s;\n}\n\n/* originally authored by Angelo Rohit - https://github.com/angelorohit */\n\n@-webkit-keyframes wiggle {\n    0% { -webkit-transform: skewX(9deg); }\n    10% { -webkit-transform: skewX(-8deg); }\n    20% { -webkit-transform: skewX(7deg); }\n    30% { -webkit-transform: skewX(-6deg); }\n    40% { -webkit-transform: skewX(5deg); }\n    50% { -webkit-transform: skewX(-4deg); }\n    60% { -webkit-transform: skewX(3deg); }\n    70% { -webkit-transform: skewX(-2deg); }\n    80% { -webkit-transform: skewX(1deg); }\n    90% { -webkit-transform: skewX(0deg); }\n\t100% { -webkit-transform: skewX(0deg); }\n}\n\n@-moz-keyframes wiggle {\n    0% { -moz-transform: skewX(9deg); }\n    10% { -moz-transform: skewX(-8deg); }\n    20% { -moz-transform: skewX(7deg); }\n    30% { -moz-transform: skewX(-6deg); }\n    40% { -moz-transform: skewX(5deg); }\n    50% { -moz-transform: skewX(-4deg); }\n    60% { -moz-transform: skewX(3deg); }\n    70% { -moz-transform: skewX(-2deg); }\n    80% { -moz-transform: skewX(1deg); }\n    90% { -moz-transform: skewX(0deg); }\n\t100% { -moz-transform: skewX(0deg); }\n}\n\n@-o-keyframes wiggle {\n    0% { -o-transform: skewX(9deg); }\n    10% { -o-transform: skewX(-8deg); }\n    20% { -o-transform: skewX(7deg); }\n    30% { -o-transform: skewX(-6deg); }\n    40% { -o-transform: skewX(5deg); }\n    50% { -o-transform: skewX(-4deg); }\n    60% { -o-transform: skewX(3deg); }\n    70% { -o-transform: skewX(-2deg); }\n    80% { -o-transform: skewX(1deg); }\n    90% { -o-transform: skewX(0deg); }\n\t100% { -o-transform: skewX(0deg); }\n}\n\n@keyframes wiggle {\n    0% { transform: skewX(9deg); }\n    10% { transform: skewX(-8deg); }\n    20% { transform: skewX(7deg); }\n    30% { transform: skewX(-6deg); }\n    40% { transform: skewX(5deg); }\n    50% { transform: skewX(-4deg); }\n    60% { transform: skewX(3deg); }\n    70% { transform: skewX(-2deg); }\n    80% { transform: skewX(1deg); }\n    90% { transform: skewX(0deg); }\n\t100% { transform: skewX(0deg); }\n}\n\n.wiggle {\n    -webkit-animation-name: wiggle;\n    -moz-animation-name: wiggle;\n    -o-animation-name: wiggle;\n    animation-name: wiggle;\n\n    -webkit-animation-timing-function: ease-in;\n    -moz-animation-timing-function: ease-in;\n    -o-animation-timing-function: ease-in;\n    animation-timing-function: ease-in;\n}\n\n.animated.wiggle {\n    -webkit-animation-duration: 0.75s;\n    -moz-animation-duration: 0.75s;\n    -o-animation-duration: 0.75s;\n    animation-duration: 0.75s;\n}\n"
  },
  {
    "path": "scripts/build.js",
    "content": "const fsSync = require('fs');\nconst fs = fsSync.promises;\nconst path = require('path');\n\nconst browserify = require('browserify');\nconst exorcist = require('exorcist');\nconst less = require('less');\nconst mkdirp = require('mkdirp').mkdirp;\nconst tsify = require('tsify');\n\nconst baseDir = path.join(__dirname, '..');\n\n(async () => {\n  await mkdirp(path.join(baseDir, 'public', 'css'));\n  await mkdirp(path.join(baseDir, 'public', 'js'));\n\n  const dir = await fs.readdir('components', { withFileTypes: true });\n  const components = dir\n    .filter((component) => component.isDirectory())\n    .map((component) => component.name);\n\n  // less\n  console.log('less:common');\n  await lessFile(\n    path.join(baseDir, 'public/less/styles.less'),\n    path.join(baseDir, 'public/css/styles.css')\n  );\n\n  console.log('less:components');\n  await Promise.all(\n    components.map(async (component) => {\n      const componentPath = path.join(baseDir, `components/${component}/${component}`);\n      try {\n        await fs.access(`${componentPath}.less`);\n      } catch {\n        /* ignore */\n        return;\n      }\n      return lessFile(`${componentPath}.less`, `${componentPath}.css`);\n    })\n  );\n\n  // browserify\n  console.log('browserify:common');\n  const publicSourceDir = path.join(baseDir, 'public/source');\n  const b = browserify(path.join(baseDir, 'public/source/main.js'), {\n    noParse: ['dnd-page-scroll', 'jquery', 'knockout'],\n    debug: true,\n  });\n  b.require(path.join(publicSourceDir, 'components.js'), { expose: 'ungit-components' });\n  b.require(path.join(publicSourceDir, 'main.js'), { expose: 'ungit-main' });\n  b.require(path.join(publicSourceDir, 'navigation.js'), { expose: 'ungit-navigation' });\n  b.require(path.join(publicSourceDir, 'program-events.js'), { expose: 'ungit-program-events' });\n  b.require(path.join(publicSourceDir, 'storage.js'), { expose: 'ungit-storage' });\n  b.require(path.join(baseDir, 'source/address-parser.js'), { expose: 'ungit-address-parser' });\n  b.require('bluebird', { expose: 'bluebird' });\n  b.require('blueimp-md5', { expose: 'blueimp-md5' });\n  b.require('diff2html', { expose: 'diff2html' });\n  b.require('jquery', { expose: 'jquery' });\n  b.require('knockout', { expose: 'knockout' });\n  b.require('lodash', { expose: 'lodash' });\n  b.require(path.join(baseDir, 'node_modules/snapsvg/src/mina.js'), { expose: 'mina' });\n  b.require('moment', { expose: 'moment' });\n  b.require('@primer/octicons', { expose: 'octicons' });\n  b.require('signals', { expose: 'signals' });\n  b.require('winston', { expose: 'winston' });\n  const ungitjsFile = path.join(baseDir, 'public/js/ungit.js');\n  const mapFile = path.join(baseDir, 'public/js/ungit.js.map');\n  await new Promise((resolve) => {\n    const outFile = fsSync.createWriteStream(ungitjsFile);\n    outFile.on('close', () => resolve());\n    b.bundle().pipe(exorcist(mapFile)).pipe(outFile);\n  });\n  console.log(`browserify ${path.relative(baseDir, ungitjsFile)}`);\n\n  console.log('browserify:components');\n  for (const component of components) {\n    console.log(`browserify:components:${component}`);\n    const sourcePrefix = path.join(baseDir, `components/${component}/${component}`);\n    const destination = path.join(baseDir, `components/${component}/${component}.bundle.js`);\n\n    const jsSource = `${sourcePrefix}.js`;\n    try {\n      await fs.access(jsSource);\n      await browserifyFile(jsSource, destination);\n    } catch {\n      const tsSource = `${sourcePrefix}.ts`;\n      try {\n        await fs.access(tsSource);\n        await browserifyFile(tsSource, destination);\n      } catch {\n        console.warn(\n          `${sourcePrefix} does not exist. If this component is obsolete, please remove that directory or perform a clean build.`\n        );\n      }\n    }\n  }\n\n  // copy\n  console.log('copy bootstrap fonts');\n  await Promise.all(\n    [\n      'node_modules/bootstrap/fonts/glyphicons-halflings-regular.eot',\n      'node_modules/bootstrap/fonts/glyphicons-halflings-regular.svg',\n      'node_modules/bootstrap/fonts/glyphicons-halflings-regular.ttf',\n      'node_modules/bootstrap/fonts/glyphicons-halflings-regular.woff',\n      'node_modules/bootstrap/fonts/glyphicons-halflings-regular.woff2',\n    ].map(async (file) => {\n      await copyToFolder(file, 'public/fonts');\n    })\n  );\n\n  console.log('copy raven');\n  await Promise.all(\n    ['node_modules/raven-js/dist/raven.min.js', 'node_modules/raven-js/dist/raven.min.js.map'].map(\n      async (file) => {\n        await copyToFolder(file, 'public/js');\n      }\n    )\n  );\n})();\n\nasync function lessFile(source, destination) {\n  const input = await fs.readFile(source, { encoding: 'utf8' });\n  const output = await less.render(input, {\n    filename: source,\n    sourceMap: {\n      outputSourceFiles: true,\n      sourceMapURL: `${path.basename(destination)}.map`,\n    },\n  });\n  await fs.writeFile(destination, output.css);\n  await fs.writeFile(`${destination}.map`, output.map);\n  console.log(`less ${path.relative(baseDir, destination)}`);\n}\n\nasync function browserifyFile(source, destination) {\n  const mapDestination = `${destination}.map`;\n  await new Promise((resolve) => {\n    const b = browserify(source, {\n      bundleExternal: false,\n      debug: true,\n    }).plugin(tsify, {});\n\n    const outFile = fsSync.createWriteStream(destination);\n    outFile.on('close', () => resolve());\n    b.bundle().pipe(exorcist(mapDestination)).pipe(outFile);\n  });\n  console.log(`browserify ${path.relative(baseDir, destination)}`);\n}\nasync function copyToFolder(source, destination) {\n  source = path.join(baseDir, source);\n  destination = path.join(baseDir, destination, path.basename(source));\n  await fs.copyFile(source, destination);\n  console.log(`copy ${path.relative(baseDir, destination)}`);\n}\n"
  },
  {
    "path": "scripts/electronpackage.js",
    "content": "const process = require('process');\nconst path = require('path');\nconst fs = require('fs').promises;\nconst electronPackager = require('electron-packager');\n\nconst baseDir = path.join(__dirname, '..');\nconst outDir = path.join(baseDir, 'build');\n\nconst builds = process.argv.includes('--all') // keep in sync with ci.yml (https://github.com/electron/electron-packager/blob/af334e33c9228493597afcc3931336124d6180c6/src/targets.js#L9-L14)\n  ? {\n      darwin: ['x64', 'arm64'],\n      linux: ['x64', 'armv7l', 'arm64'],\n      win32: ['ia32', 'x64', 'arm64'],\n    }\n  : { current: undefined };\n\n(async () => {\n  try {\n    await fs.mkdir(outDir);\n  } catch (e) {\n    if (e.code != 'EEXIST') {\n      throw e;\n    }\n  }\n\n  for (const platform of Object.keys(builds)) {\n    await electronPackager({\n      dir: baseDir,\n      out: outDir,\n      icon: path.join(baseDir, 'public/images/icon'),\n      platform: platform == 'current' ? undefined : platform,\n      arch: builds[platform],\n      asar: true,\n      overwrite: platform == 'current',\n      appCopyright: 'Copyright (c) 2013-2026 Fredrik Norén',\n      ignore: [\n        /^\\/(?:[^/]+?\\/)*(?:\\..+|.+\\.less)$/, // dot-files and less files anywhere\n        /^\\/(?:\\..+|assets|clicktests|coverage|dist|scripts|test)\\//, // folders in root\n        /^\\/[^/]+?\\.(?:js|md|png|tgz|yml)$/, // files in root\n        /^\\/public\\/(?:source|vendor)\\//, // folders in /public\n      ],\n    });\n  }\n})();\n"
  },
  {
    "path": "scripts/electronzip.js",
    "content": "const fsSync = require('fs');\nconst fs = fsSync.promises;\nconst path = require('path');\nconst archiver = require('archiver');\n\nconst baseDir = path.join(__dirname, '..');\nconst buildDir = path.join(baseDir, 'build');\nconst distDir = path.join(baseDir, 'dist');\n\n(async () => {\n  let distFiles = [];\n  try {\n    distFiles = await fs.readdir(distDir);\n  } catch {\n    await fs.mkdir(distDir);\n  }\n  for (const distFile of distFiles) {\n    await fs.unlink(path.join(distDir, distFile));\n  }\n\n  let buildFolders;\n  try {\n    buildFolders = await fs.readdir(buildDir);\n  } catch (e) {\n    console.error('Run \"npm run electronpackage\" before zipping');\n    throw e;\n  }\n  return Promise.all(\n    buildFolders.map((folder) => {\n      const source = path.join(buildDir, folder);\n      const destination = path.join(distDir, `${folder}.zip`);\n      return zipDirectory(source, destination);\n    })\n  );\n})();\n\nasync function zipDirectory(source, destination) {\n  console.log(`start zip ${path.relative(baseDir, destination)}`);\n  await new Promise((resolve, reject) => {\n    const archive = archiver('zip');\n    const stream = fsSync.createWriteStream(destination);\n\n    archive\n      .directory(source, path.basename(source))\n      .on('error', (err) => reject(err))\n      .pipe(stream);\n\n    stream.on('close', () => resolve());\n    archive.finalize();\n  });\n  console.log(`finish zip ${path.relative(baseDir, destination)}`);\n}\n"
  },
  {
    "path": "scripts/npmpublish.js",
    "content": "const fs = require('fs').promises;\nconst path = require('path');\n\nmodule.exports = async ({ github, context, core, exec }) => {\n  core.info('Preparing npm publish');\n  const hash = context.sha.substring(0, 8);\n  const packageJson = JSON.parse(await fs.readFile('package.json', { encoding: 'utf8' }));\n  const version = packageJson.version;\n  const tag = `v${version}`;\n  packageJson.version += `+${hash}`;\n  await fs.writeFile('package.json', `${JSON.stringify(packageJson, null, 2)}\\n`);\n  core.info(`Publish ${packageJson.version} to npm`);\n  try {\n    if ((await exec.exec('npm publish', ['--provenance', '--access public'])) != 0) {\n      core.info('npm publish failed.');\n      return;\n    }\n  } catch (e) {\n    core.info(`npm publish failed: ${e}`);\n    return;\n  }\n  core.info(`Creating release ${tag}`);\n  const release = await github.rest.repos.createRelease({\n    owner: context.repo.owner,\n    repo: context.repo.repo,\n    name: tag,\n    tag_name: tag,\n    body: `[Changelog](https://github.com/FredrikNoren/ungit/blob/master/CHANGELOG.md#${version.replace(\n      /\\./g,\n      ''\n    )})`,\n  });\n  const filePaths = await fs.readdir('dist');\n  for (const file of filePaths) {\n    const filePath = path.join('dist', file);\n    core.info(`Uploading release asset ${filePath}`);\n    await github.rest.repos.uploadReleaseAsset({\n      owner: context.repo.owner,\n      repo: context.repo.repo,\n      release_id: release.data.id,\n      name: file.replace('ungit', `ungit-${version}`),\n      data: await fs.readFile(filePath),\n    });\n  }\n};\n"
  },
  {
    "path": "scripts/teststabilitytester.js",
    "content": "// This repeatedly runs the click and unit tests to verify their stability\n\nvar childProcess = require('child_process');\nvar moment = require('moment');\n\nvar count = 0;\nvar clickTestErrors = 0;\nvar unitTestErrors = 0;\nvar startTime = Date.now();\nvar run = function () {\n  var testTime = Date.now();\n  count++;\n  console.log('Round ' + count + '...');\n  childProcess.exec('npm run clicktest', function (err, stdout, stderr) {\n    if (err) {\n      clickTestErrors++;\n      console.log(stdout);\n      console.log(stderr);\n      console.log('Clicktest failed!');\n    }\n    childProcess.exec('npm run unittest', function (err, stdout, stderr) {\n      if (err) {\n        unitTestErrors++;\n        console.log(stdout);\n        console.log(stderr);\n        console.log('Unittest failed!');\n      }\n      console.log(\n        count +\n          ' test run, ' +\n          clickTestErrors +\n          ' clicktest errors (' +\n          Math.floor((100 * clickTestErrors) / count) +\n          '%), ' +\n          unitTestErrors +\n          ' unittest errors (' +\n          Math.floor((100 * unitTestErrors) / count) +\n          '%) ' +\n          '(this round: ' +\n          moment.duration(Date.now() - testTime).asSeconds() +\n          'sec, total: ' +\n          moment.duration(Date.now() - startTime).humanize() +\n          ')'\n      );\n      run();\n    });\n  });\n};\nrun();\n"
  },
  {
    "path": "source/address-parser.js",
    "content": "'use strict';\n\n// USED BY FRONT END\n// DO NOT GO ES6\nconst addressWindowsLocalRegexp = /[a-zA-Z]:\\\\([^\\\\]+\\\\?)*/;\nconst addressSshWithPortRegexp = /ssh:\\/\\/(.*):(\\d*)\\/(.*)/;\nconst addressSshWithoutPortRegexp = /ssh:\\/\\/([^/]*)\\/(.*)/;\nconst addressGitWithoutPortWithUsernamePortRegexp = /([^@]*)@([^:]*):([^.]*)(\\.git)?$/;\nconst addressGitWithoutPortWithoutUsernameRegexp = /([^:]*):([^.]*)(\\.git)?$/;\nconst addressHttpsRegexp = /https:\\/\\/([^/]*)\\/([^.]*)(\\.git)?$/;\nconst addressUnixLocalRegexp = /.*\\/([^/]+)/;\n\n/**\n * Show slashes in path parameter.\n *\n * @param {string} path\n */\nexports.encodePath = (path) => encodeURIComponent(path).replace(/%2F/g, '/');\n\nexports.parseAddress = (remote) => {\n  let match = addressWindowsLocalRegexp.exec(remote);\n  if (match) {\n    let project = match[1];\n    if (project[project.length - 1] == '\\\\') project = project.slice(0, project.length - 1);\n    return { address: remote, host: 'localhost', project: project, shortProject: project };\n  }\n\n  match = addressSshWithPortRegexp.exec(remote);\n  if (match)\n    return {\n      address: remote,\n      host: match[1],\n      port: match[2],\n      project: match[3],\n      shortProject: match[3].split('/').pop(),\n    };\n\n  match = addressSshWithoutPortRegexp.exec(remote);\n  if (match)\n    return {\n      address: remote,\n      host: match[1],\n      project: match[2],\n      shortProject: match[2].split('/').pop(),\n    };\n\n  match = addressGitWithoutPortWithUsernamePortRegexp.exec(remote);\n  if (match)\n    return {\n      address: remote,\n      username: match[1],\n      host: match[2],\n      project: match[3],\n      shortProject: match[3].split('/').pop(),\n    };\n\n  match = addressGitWithoutPortWithoutUsernameRegexp.exec(remote);\n  if (match)\n    return {\n      address: remote,\n      host: match[1],\n      project: match[2],\n      shortProject: match[2].split('/').pop(),\n    };\n\n  match = addressHttpsRegexp.exec(remote);\n  if (match)\n    return {\n      address: remote,\n      host: match[1],\n      project: match[2],\n      shortProject: match[2].split('/').pop(),\n    };\n\n  match = addressUnixLocalRegexp.exec(remote);\n  if (match)\n    return { address: remote, host: 'localhost', project: match[1], shortProject: match[1] };\n\n  return { address: remote };\n};\n"
  },
  {
    "path": "source/bugtracker.js",
    "content": "'use strict';\n\nconst logger = require('./utils/logger');\nconst sysinfo = require('./sysinfo');\nconst config = require('./config');\nconst raven = require('raven-js');\nconst client = new raven.Client(\n  'https://58f16d6f010d4c77900bb1de9c02185f:84b7432f56674fbc8522bc84cc7b30f4@app.getsentry.com/12434'\n);\n\nclass BugTracker {\n  constructor(subsystem) {\n    if (!config.bugtracking) return;\n\n    this.subsystem = subsystem;\n    this.appVersion = 'unknown';\n    this.userHash = sysinfo.getUserHash();\n    this.appVersion = config.ungitDevVersion;\n    logger.info(`BugTracker set version: ${this.appVersion}`);\n  }\n  notify(exception) {\n    if (!config.bugtracking) return;\n\n    const options = {\n      user: { id: this.userHash },\n      tags: {\n        version: this.appVersion,\n        subsystem: this.subsystem,\n        deployment: config.desktopMode ? 'desktop' : 'web',\n      },\n    };\n\n    client.captureException(exception, options);\n  }\n}\nmodule.exports = BugTracker;\n"
  },
  {
    "path": "source/config.js",
    "content": "'use strict';\n\nconst rc = require('rc');\nconst path = require('path');\nconst fs = require('fs');\nconst process = require('process');\nconst yargs = require('yargs/yargs')(process.argv.slice(2));\nconst homedir = require('os').homedir();\nconst child_process = require('child_process');\nconst semver = require('semver');\n\nconst defaultConfig = {\n  // The port ungit is exposed on.\n  port: 8448,\n\n  // The base URL ungit will be accessible from.\n  urlBase: 'http://localhost',\n\n  // The URL root path under which ungit will be accesible.\n  rootPath: '',\n\n  // Directory to output log files.\n  logDirectory: null,\n\n  // Write REST requests to the log\n  logRESTRequests: true,\n\n  // Write git commands issued to the log\n  logGitCommands: false,\n\n  // Write the result of git commands issued to the log\n  logGitOutput: false,\n\n  // This will automatically send anonymous bug reports.\n  bugtracking: false,\n\n  // True to enable authentication. Users are defined in the users configuration property.\n  authentication: false,\n\n  // Map of username/passwords which are granted access.\n  users: {},\n\n  // Set to false to show rebase and merge on drag and drop on all nodes.\n  showRebaseAndMergeOnlyOnRefs: true,\n\n  // Maximum number of concurrent git operations\n  maxConcurrentGitOperations: 4,\n\n  // Launch a browser window with ungit when ungit is started\n  launchBrowser: true,\n\n  // Instead of launching ungit with the current folder force a different path to be used. Can be set to null to force the home screen.\n  forcedLaunchPath: undefined,\n\n  // Closes the server after x ms of inactivity. Mainly used by the clicktesting.\n  autoShutdownTimeout: undefined,\n\n  // Don't fast forward git mergers. See git merge --no-ff documentation\n  noFFMerge: true,\n\n  // Automatically fetch from remote when entering a repository using ungit, periodically on activity detection, or on directory change\n  autoFetch: true,\n\n  // Used for development purposes.\n  dev: false,\n\n  // Assigns the log level. Possible values, in order from quietest to loudest, are\n  // \"none\", \"error\", \"warn\", \"info\", \"verbose\", \"debug\", and \"silly\"\n  logLevel: 'warn',\n\n  // Specify a custom command to launch. `%U` will be replaced with the URL\n  //  that corresponds with the working git directory.\n  //\n  // NOTE: This will execute *before* opening the browser window if the\n  //        `launchBrowser` option is `true`.\n  // Example:\n  //     # Override the browser launch command; use chrome's \"app\"\n  //     #   argument to get a new, chromeless window for that \"native feel\"\n  //     $ ungit --launchBrowser=0 --launchCommand \"chrome --app=%U\"\n  launchCommand: undefined,\n\n  // Allow checking out nodes (which results in a detached head)\n  allowCheckoutNodes: false,\n\n  // An array of ip addresses that can connect to ungit. All others are denied.\n  // null indicates all IPs are allowed.\n  // Example (only allow localhost): allowedIPs: [\"127.0.0.1\"]\n  allowedIPs: null,\n\n  // Automatically remove remote tracking branches that have been removed on the\n  // server when fetching. (git fetch -p)\n  autoPruneOnFetch: true,\n\n  // Directory to look for plugins\n  pluginDirectory: path.join(homedir, '.ungit', 'plugins'),\n\n  // Name-object pairs of configurations for plugins. To disable a plugin, use \"disabled\": true, for example:\n  // \"pluginConfigs\": { \"gerrit\": { \"disabled\": true } }\n  pluginConfigs: {},\n\n  // Don't show errors when the user is using a bad or undecidable git version\n  gitVersionCheckOverride: false,\n\n  // Don't show upgrade message when the user is using an older version of ungit\n  ungitVersionCheckOverride: false,\n\n  // Automatically does stash -> operation -> stash pop when you checkout, reset or cherry pick. This makes it\n  // possible to perform those actions even when you have a dirty working directory.\n  autoStashAndPop: true,\n\n  fileSeparator: path.sep,\n\n  // disable warning popup at discard\n  disableDiscardWarning: false,\n\n  // Duration of discard warning dialog mute time should it be muted.\n  disableDiscardMuteTime: 60 * 1000 * 5, // 5 mins\n\n  // Allowed number of retry for git \"index.lock\" conflict\n  lockConflictRetryCount: 3,\n\n  // Auto checkout the created branch on creation\n  autoCheckoutOnBranchCreate: false,\n\n  // Always load with active checkout branch (deprecated: use `maxActiveBranchSearchIteration`)\n  alwaysLoadActiveBranch: false,\n\n  // Max search iterations for active branch.  ( value means not searching for active branch)\n  maxActiveBranchSearchIteration: -1,\n\n  // number of nodes to load for each git.log call\n  numberOfNodesPerLoad: 25,\n\n  // Specifies a custom git merge tool to use when resolving conflicts. Your git configuration must be set up to use this!\n  // A true value will use the default tool while a string value will use the tool of that specified name.\n  mergeTool: false,\n\n  // Preferred default diff type used. Can be `\"textdiff\"` or `\"sidebysidediff\"`.\n  diffType: undefined,\n\n  // Specify whether to Ignore or Show white space diff\n  ignoreWhiteSpaceDiff: false,\n\n  // Specify tab size as number of spaces\n  tabSize: null,\n\n  // Number of refs to show on git commit bubbles to limit too many refs to appear.\n  numRefsToShow: 5,\n\n  // Force gpg sign for tags and commits.  (additionally one can set up `git config commit.gpgsign true`\n  // instead of this flag)  more on this: https://help.github.com/articles/signing-commits-using-gpg/\n  isForceGPGSign: false,\n\n  // Array of local git repo paths to display at the ungit home page\n  defaultRepositories: [],\n\n  // a string of ip to bind to, default is `127.0.0.1`\n  ungitBindIp: '127.0.0.1',\n\n  // is front end animation enabled\n  isAnimate: true,\n\n  // disable progress bar (front end api)\n  isDisableProgressBar: false,\n\n  // git binary path, not including git binary path. (i.e. /bin or /usr/bin/)\n  gitBinPath: null,\n\n  // when false, disable numstats durin status for performance.  see #1193\n  isEnableNumStat: true,\n};\n\n// Works for now but should be moved to bin/ungit\nconst argv = yargs\n  .usage('$0 [-v] [-b] [--cliconfigonly] [--gitVersionCheckOverride]')\n  .example('$0 --port=8888', 'Run Ungit on port 8888')\n  .example(\n    '$0 --no-logRESTRequests --logGitCommands',\n    'Turn off REST logging but turn on git command log'\n  )\n  .help('help')\n  .version()\n  .alias('b', 'launchBrowser')\n  .boolean('launchBrowser')\n  .alias('h', 'help')\n  .alias('o', 'gitVersionCheckOverride')\n  .boolean('gitVersionCheckOverride')\n  .alias('v', 'version')\n  .describe(\n    'o',\n    'Ignore git version check and allow ungit to run with possibly lower versions of git'\n  )\n  .boolean('o')\n  .describe('ungitVersionCheckOverride', 'Ignore check for older version of ungit')\n  .boolean('ungitVersionCheckOverride')\n  .describe(\n    'b',\n    'Launch a browser window with ungit when the ungit server is started. --no-b or --no-launchBrowser disables this'\n  )\n  .boolean('b')\n  .describe(\n    'cliconfigonly',\n    'Ignore the default configuration points and only use parameters sent on the command line'\n  )\n  .boolean('cliconfigonly')\n  .describe('port', 'The port ungit is exposed on')\n  .describe('urlBase', 'The base URL ungit will be accessible from')\n  .describe('rootPath', 'The root path ungit will be accessible from')\n  .describe('logDirectory', 'Directory to output log files')\n  .describe('logRESTRequests', 'Write REST requests to the log')\n  .boolean('logRESTRequests')\n  .describe('logGitCommands', 'Write git commands issued to the log')\n  .boolean('logGitCommands')\n  .describe('logGitOutput', 'Write the result of git commands issued to the log')\n  .boolean('logGitOutput')\n  .describe('bugtracking', 'This will automatically send anonymous bug reports')\n  .boolean('bugtracking')\n  .describe(\n    'authentication',\n    'True to enable authentication. Users are defined in the users configuration property'\n  )\n  .boolean('authentication')\n  .describe('users', 'Map of username/passwords which are granted access')\n  .describe(\n    'showRebaseAndMergeOnlyOnRefs',\n    'Set to false to show rebase and merge on drag and drop on all nodes'\n  )\n  .boolean('showRebaseAndMergeOnlyOnRefs')\n  .describe('maxConcurrentGitOperations', 'Maximum number of concurrent git operations')\n  .describe(\n    'forcedLaunchPath',\n    'Define path to be used on open. Can be set to null to force the home screen'\n  )\n  .describe(\n    'autoShutdownTimeout',\n    'Closes the server after x ms of inactivity. Mainly used by the clicktesting'\n  )\n  .describe('noFFMerge', \"Don't fast forward git mergers. See git merge --no-ff documentation\")\n  .boolean('noFFMerge')\n  .describe(\n    'autoFetch',\n    'Automatically fetch from remote when entering a repository using ungit, periodically on activity detection, or on directory change'\n  )\n  .boolean('autoFetch')\n  .describe('dev', 'Used for development purposes')\n  .boolean('dev')\n  .describe(\n    'logLevel',\n    'The logging level, possible values are none, error, warn, info, verbose, debug, and silly.'\n  )\n  .describe(\n    'launchCommand',\n    'Specify a custom command to launch. `%U` will be replaced with the URL that corresponds with the working git directory.'\n  )\n  .describe('allowCheckoutNodes', 'Allow checking out nodes (which results in a detached head)')\n  .boolean('allowCheckoutNodes')\n  .describe(\n    'allowedIPs',\n    'An array of ip addresses that can connect to ungit. All others are denied'\n  )\n  .describe(\n    'autoPruneOnFetch',\n    'Automatically remove remote tracking branches that have been removed on the server when fetching. (git fetch -p)'\n  )\n  .boolean('autoPruneOnFetch')\n  .describe('pluginDirectory', 'Directory to look for plugins')\n  // --pluginConfigs doesn't work...  Probably only works in .ungitrc as a json file\n  .describe(\n    'pluginConfigs',\n    'No supported as a command line argument, use ungitrc config file.  See README.md'\n  )\n  .describe('autoStashAndPop', 'Used for development purposes')\n  .boolean('autoStashAndPop')\n  .describe('fileSeparator', 'OS dependent file separator')\n  .describe('disableDiscardWarning', 'disable warning popup at discard')\n  .boolean('disableDiscardWarning')\n  .describe(\n    'disableDiscardMuteTime',\n    'duration of discard warning dialog mute time should it be muted'\n  )\n  .describe('lockConflictRetryCount', 'Allowed number of retry for git \"index.lock\" conflict')\n  .describe('autoCheckoutOnBranchCreate', 'Auto checkout the created branch on creation')\n  .boolean('autoCheckoutOnBranchCreate')\n  .describe(\n    'alwaysLoadActiveBranch',\n    'Always load with active checkout branch (DEPRECATED, use `maxActiveBranchSearchIteration`)'\n  )\n  .boolean('alwaysLoadActiveBranch')\n  .describe(\n    'maxActiveBranchSearchIteration',\n    'Max search iterations for active branch.  (-1 means not searching for active branch)'\n  )\n  .describe('numberOfNodesPerLoad', 'number of nodes to load for each git.log call')\n  .describe('mergeTool', 'the git merge tool to use when resolving conflicts')\n  .describe(\n    'diffType',\n    'Prefered default diff type used. Can be `\"textdiff\"` or `\"sidebysidediff\"`.'\n  )\n  .describe('ignoreWhiteSpaceDiff', 'Specify whether to Ignore or Show white space diff')\n  .boolean('ignoreWhiteSpaceDiff')\n  .describe(\n    'numRefsToShow',\n    'Number of refs to show on git commit bubbles to limit too many refs to appear.'\n  )\n  .describe('tabSize', 'Specify tab size as number of spaces')\n  .describe('isForceGPGSign', 'Force gpg sign for tags and commits.')\n  .boolean('isForceGPGSign')\n  .describe(\n    'defaultRepositories',\n    'Array of local git repo paths to display at the ungit home page'\n  )\n  .describe('ungitBindIp', 'a string of ip to bind to, default is `127.0.0.1`')\n  .describe('isAnimate', 'is front end animation enabled')\n  .boolean('isAnimate')\n  .describe('isDisableProgressBar', 'disable progress bar (front end api)')\n  .boolean('isDisableProgressBar')\n  .describe(\n    'gitBinPath',\n    'git binary path, not including git binary path. (i.e. /bin or /usr/bin/)'\n  )\n  .describe(\n    'isEnableNumStat',\n    'when false, disables numstats during git status for performance.  see #1193'\n  )\n  .boolean('isEnableNumStat');\nconst argvConfig = argv.argv;\n\n// When ungit is started normally, $0 == ungit, and non-hyphenated options exists, show help and exit.\nif (argvConfig.$0.endsWith('ungit') && argvConfig._ && argvConfig._.length > 0) {\n  yargs.showHelp();\n  process.exit(1);\n}\n\nlet rcConfig = {};\nif (!argvConfig.cliconfigonly) {\n  try {\n    rcConfig = rc('ungit');\n    // rc return additional options that must be ignored\n    delete rcConfig['config'];\n    delete rcConfig['configs'];\n  } catch (err) {\n    console.error(`Stop at reading ~/.ungitrc because ${err}`);\n    throw err;\n  }\n}\n\nmodule.exports = argv.default(defaultConfig).default(rcConfig).argv;\n\nmodule.exports.homedir = homedir;\n\nlet currentRootPath = module.exports.rootPath;\nif (typeof currentRootPath !== 'string') {\n  currentRootPath = '';\n} else if (currentRootPath !== '') {\n  // must start with a slash\n  if (currentRootPath.charAt(0) !== '/') {\n    currentRootPath = '/' + currentRootPath;\n  }\n  // can not end with a trailing slash\n  if (currentRootPath.charAt(currentRootPath.length - 1) === '/') {\n    currentRootPath = currentRootPath.substring(0, currentRootPath.length - 1);\n  }\n}\nmodule.exports.rootPath = currentRootPath;\n\n// Errors can not be serialized with JSON.stringify without this fix\n// http://stackoverflow.com/a/18391400\nObject.defineProperty(Error.prototype, 'toJSON', {\n  value: function () {\n    const alt = {};\n    Object.getOwnPropertyNames(this).forEach((key) => {\n      alt[key] = this[key];\n    });\n    return alt;\n  },\n  configurable: true,\n});\n\ntry {\n  module.exports.gitVersion = /.*?(\\d+[.]\\d+[.]\\d+).*/.exec(\n    child_process.execSync('git --version').toString()\n  )[1];\n} catch (e) {\n  console.error(\n    'Can\\'t run \"git --version\". Is git installed and available in your path?',\n    e.stderr\n  );\n  throw e;\n}\n\nmodule.exports.ungitPackageVersion = require('../package.json').version;\n\nlet devVersion = module.exports.ungitPackageVersion;\nif (fs.existsSync(path.join(__dirname, '..', '.git'))) {\n  const revision = child_process\n    .execSync('git rev-parse --short HEAD', { cwd: path.join(__dirname, '..') })\n    .toString()\n    .replace('\\n', ' ')\n    .trim();\n  devVersion = `dev-${module.exports.ungitPackageVersion}-${revision}`;\n}\nmodule.exports.ungitDevVersion = devVersion;\n\nif (module.exports.alwaysLoadActiveBranch) {\n  module.exports.maxActiveBranchSearchIteration = 25;\n}\n\nmodule.exports.isGitOptionalLocks = semver.satisfies(module.exports.gitVersion, '2.15.0');\n\nif (argvConfig.$0.endsWith('mocha')) {\n  console.warn('Running mocha test run, overriding few test variables...');\n  module.exports.logLevel = 'debug';\n  module.exports.dev = true;\n}\n"
  },
  {
    "path": "source/git-api.js",
    "content": "const path = require('path');\nconst temp = require('temp');\nconst gitParser = require('./git-parser');\nconst logger = require('./utils/logger');\nconst os = require('os');\nconst mkdirp = require('mkdirp').mkdirp;\nconst rimraf = require('rimraf').rimraf;\nconst _ = require('lodash');\nconst gitPromise = require('./git-promise');\nconst fs = require('fs').promises;\nconst chokidar = require('chokidar');\nconst ignore = require('ignore');\nconst { EventEmitter } = require('events');\n\nconst tenMinTimeoutMs = 10 * 60 * 1000;\n\nexports.pathPrefix = '';\n\nexports.registerApi = (env) => {\n  const app = env.app;\n  const ensureAuthenticated = env.ensureAuthenticated || ((req, res, next) => next());\n  const config = env.config;\n  const io = env.socketIO;\n  const socketsById = env.socketsById || {};\n\n  if (config.dev) temp.track();\n\n  if (io) {\n    io.on('connection', (socket) => {\n      socket.on('disconnect', () => {\n        stopDirectoryWatch(socket);\n      });\n      socket.on('watch', async (data) => {\n        stopDirectoryWatch(socket); // clean possibly lingering connections\n        socket.watcherPath = path.normalize(data.path);\n        socket.join(socket.watcherPath); // join room for this path\n\n        const watcher = await watchRepo(socket.watcherPath);\n        watcher.on('workdir', (changedPath) => {\n          logger.info(`${changedPath} triggered workdir refresh for ${socket.watcherPath}`);\n          emitWorkingTreeChanged(socket.watcherPath);\n        });\n        watcher.on('git', (changedPath) => {\n          logger.info(`${changedPath} triggered git refresh for ${socket.watcherPath}`);\n          emitGitDirectoryChanged(socket.watcherPath);\n        });\n        watcher.on('error', (err) => {\n          logger.warn(`Error watching ${socket.watcherPath}: `, JSON.stringify(err));\n        });\n        socket.watcher = watcher;\n      });\n    });\n  }\n\n  let watcherId = 1;\n  class RepoWatcher extends EventEmitter {\n    constructor() {\n      super();\n      this.watcherId = watcherId++;\n      this.watchers = [];\n    }\n    async watchItem(name, item, filter) {\n      if ((await fs.access(item).catch(() => false)) === false) {\n        logger.debug(`[${this.watcherId}] path does not exist`, item);\n        return;\n      }\n      const watcher = chokidar.watch(item, { ignored: filter });\n      const changed = (changedPath) => {\n        logger.silly(`[${this.watcherId}] ${name}`, changedPath);\n        this.emit(name, changedPath);\n      };\n\n      watcher.on('change', changed);\n      watcher.on('add', changed);\n      watcher.on('unlink', changed);\n      watcher.on('addDir', changed);\n      watcher.on('unlinkDir', changed);\n      this.watchers.push(watcher);\n    }\n    addWorkdir(item, filter) {\n      return this.watchItem('workdir', item, filter);\n    }\n    addGit(item, filter) {\n      return this.watchItem('git', item, filter);\n    }\n    close() {\n      this.watchers.forEach((w) => w.close());\n    }\n  }\n\n  const readIgnore = async (pathToWatch) => {\n    logger.debug(`Parsing .gitignore for ${pathToWatch}`);\n    const out = ignore();\n    const ignoreContent = await fs\n      .readFile(path.join(pathToWatch, '.gitignore'), { encoding: 'utf8' })\n      .catch(() => null);\n    if (ignoreContent) out.add(ignoreContent);\n    return out;\n  };\n\n  // TODO move to nodegit\n  const watchRepo = async (pathToWatch) => {\n    logger.info(`Start watching ${pathToWatch}`);\n    const watcher = new RepoWatcher();\n    let repoPath = path.join(pathToWatch, '.git');\n    if ((await fs.access(repoPath).catch(() => false)) === undefined) {\n      // Looks like a repo, let's watch workdir\n      let gitIgnore = await readIgnore(pathToWatch);\n      await watcher.addWorkdir(pathToWatch, (watch_path) => {\n        const ignore = true,\n          watch = false;\n        const filePath = path.relative(pathToWatch, watch_path);\n        if (!filePath) return watch; // root\n        if (filePath === '.gitignore') {\n          readIgnore(pathToWatch).then(\n            (ign) => (gitIgnore = ign),\n            (err) => logger.error('Could not parse .gitignore for', pathToWatch, err)\n          );\n        }\n        // We monitor the repo separately\n        if (filePath === '.git' || filePath.startsWith('.git' + path.sep)) return ignore;\n        // We add / to test for directories, we can't have a file named like a directory\n        // and otherwise directory `foo` won't match ignore `foo/`\n        if (gitIgnore.ignores(filePath) || gitIgnore.ignores(`${filePath}/`)) {\n          // TODO https://github.com/kaelzhang/node-ignore/issues/78\n          // optimization: assume these are permanent skips\n          if (filePath.includes('node_modules')) return ignore;\n          return ignore;\n        }\n        return watch;\n      });\n    } else {\n      // Could be bare\n      repoPath = pathToWatch;\n    }\n    // Here we watch the git state\n    await watcher.addGit(path.join(repoPath, 'refs'), (watch_path) => {\n      const ignore = true,\n        watch = false;\n\n      if (watch_path.endsWith('.lock')) {\n        return ignore;\n      }\n\n      return watch;\n    });\n    await watcher.addGit(path.join(repoPath, 'HEAD'));\n    await watcher.addGit(path.join(repoPath, 'index'));\n\n    return watcher;\n  };\n\n  const stopDirectoryWatch = (socket) => {\n    if (!socket.watcherPath) return;\n    logger.info(`Stop watching ${socket.watcherPath}`);\n    socket.leave(socket.watcherPath);\n    socket.watcherPath = undefined;\n    socket.ignore = undefined;\n    socket.watcher && socket.watcher.close();\n  };\n\n  const ensurePathExists = (req, res, next) => {\n    fs.access(req.query.path || req.body.path)\n      .then(() => {\n        next();\n      })\n      .catch(() => {\n        res.status(400).json({ error: `'No such path: ${path}`, errorCode: 'no-such-path' });\n      });\n  };\n\n  const ensureValidSocketId = (req, res, next) => {\n    const socketId = req.query.socketId || req.body.socketId;\n    if (socketId == 'ignore') return next(); // Used in unit tests\n    const socket = socketsById[socketId];\n    if (!socket) {\n      res\n        .status(400)\n        .json({ error: `No such socket: ${socketId}`, errorCode: 'invalid-socket-id' });\n    } else {\n      next();\n    }\n  };\n\n  const emitWorkingTreeChanged = _.debounce(\n    (repoPath) => {\n      if (io && repoPath) {\n        io.in(path.normalize(repoPath)).emit('working-tree-changed', { repository: repoPath });\n        logger.info('emitting working-tree-changed to sockets, manually triggered');\n      }\n    },\n    500,\n    { maxWait: 1000 }\n  );\n  const emitGitDirectoryChanged = _.debounce(\n    (repoPath) => {\n      if (io && repoPath) {\n        io.in(path.normalize(repoPath)).emit('git-directory-changed', { repository: repoPath });\n        logger.info('emitting git-directory-changed to sockets, manually triggered');\n      }\n    },\n    500,\n    { maxWait: 1000 }\n  );\n\n  const autoStashExecuteAndPop = (commands, repoPath, allowedCodes, outPipe, inPipe, timeout) => {\n    if (config.autoStashAndPop) {\n      return gitPromise.stashExecuteAndPop(\n        commands,\n        repoPath,\n        allowedCodes,\n        outPipe,\n        inPipe,\n        timeout\n      );\n    } else {\n      return gitPromise(commands, repoPath, allowedCodes, outPipe, inPipe, timeout);\n    }\n  };\n\n  const jsonResultOrFailProm = (res, promise) => {\n    return promise\n      .then((result) => {\n        res.json(result || {});\n      })\n      .catch((err) => {\n        logger.warn('Responding with ERROR: ', JSON.stringify(err));\n        res.status(400).json(err);\n      });\n  };\n\n  const credentialsOption = (socketId, remote) => {\n    let portAndRootPath = `${config.port}`;\n    if (config.rootPath) {\n      portAndRootPath = `${config.port}${config.rootPath}`;\n    }\n    const credentialsHelperPath = path\n      .resolve(__dirname, '..', 'bin', 'credentials-helper')\n      .replace(/\\\\/g, '/');\n    return [\n      '-c',\n      `credential.helper=${credentialsHelperPath} ${socketId} ${portAndRootPath} ${remote}`,\n    ];\n  };\n\n  const getNumber = (value, nullValue) => {\n    const finalValue = parseInt(value ? value : nullValue);\n    if (finalValue || finalValue === 0) {\n      return finalValue;\n    } else {\n      throw { error: 'invalid number' };\n    }\n  };\n\n  app.get(`${exports.pathPrefix}/status`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(res, gitPromise.status(req.query.path, null));\n  });\n\n  app.post(`${exports.pathPrefix}/init`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise(req.body.bare ? ['init', '--bare', '--shared'] : ['init'], req.body.path)\n    );\n  });\n\n  app.post(\n    `${exports.pathPrefix}/clone`,\n    ensureAuthenticated,\n    ensurePathExists,\n    ensureValidSocketId,\n    (req, res) => {\n      // Default timeout is 2min but clone can take much longer than that (allows up to 2h)\n      const timeoutMs = 2 * 60 * 60 * 1000;\n      if (res.setTimeout) res.setTimeout(timeoutMs);\n\n      let url = req.body.url.trim();\n      if (url.indexOf('git clone ') == 0) url = url.slice('git clone '.length);\n\n      const commands = ['clone', url, req.body.destinationDir.trim()];\n      if (req.body.isRecursiveSubmodule) {\n        commands.push('--recurse-submodules');\n      }\n\n      const task = gitPromise({\n        commands: credentialsOption(req.body.socketId, url).concat(commands),\n        repoPath: req.body.path,\n        timeout: timeoutMs,\n      }).then(() => {\n        return { path: path.resolve(req.body.path, req.body.destinationDir) };\n      });\n\n      jsonResultOrFailProm(res, task).finally(emitGitDirectoryChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.get(\n    `${exports.pathPrefix}/fetch`,\n    ensureAuthenticated,\n    ensurePathExists,\n    ensureValidSocketId,\n    (req, res) => {\n      // Allow a little longer timeout on fetch (10min)\n      if (res.setTimeout) res.setTimeout(tenMinTimeoutMs);\n\n      const task = gitPromise({\n        commands: credentialsOption(req.query.socketId, req.query.remote).concat([\n          'fetch',\n          config.autoPruneOnFetch ? '--prune' : '',\n          '--',\n          req.query.remote,\n          req.query.ref ? req.query.ref : '',\n        ]),\n        repoPath: req.query.path,\n        timeout: tenMinTimeoutMs,\n      });\n\n      jsonResultOrFailProm(res, task).finally(emitGitDirectoryChanged.bind(null, req.query.path));\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/push`,\n    ensureAuthenticated,\n    ensurePathExists,\n    ensureValidSocketId,\n    (req, res) => {\n      // Allow a little longer timeout on push (10min)\n      if (res.setTimeout) res.setTimeout(tenMinTimeoutMs);\n      const task = gitPromise({\n        commands: credentialsOption(req.body.socketId, req.body.remote).concat([\n          'push',\n          req.body.remote,\n          (req.body.refSpec ? req.body.refSpec : 'HEAD') +\n            (req.body.remoteBranch ? `:${req.body.remoteBranch}` : ''),\n          req.body.force ? '-f' : '',\n        ]),\n        repoPath: req.body.path,\n        timeout: tenMinTimeoutMs,\n      });\n\n      jsonResultOrFailProm(res, task).finally(emitGitDirectoryChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.post(`${exports.pathPrefix}/reset`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      autoStashExecuteAndPop(['reset', `--${req.body.mode}`, req.body.to], req.body.path)\n    )\n      .then(emitGitDirectoryChanged.bind(null, req.body.path))\n      .then(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.get(`${exports.pathPrefix}/diff`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const isIgnoreWhiteSpace = req.query.whiteSpace === 'true' ? true : false;\n    jsonResultOrFailProm(\n      res,\n      gitPromise.diffFile(\n        req.query.path,\n        req.query.file,\n        req.query.oldFile,\n        req.query.sha1,\n        isIgnoreWhiteSpace\n      )\n    );\n  });\n\n  app.get(`${exports.pathPrefix}/diff/image`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    res.type(path.extname(req.query.filename));\n    if (req.query.version !== 'current') {\n      gitPromise.binaryFileContent(req.query.path, req.query.filename, req.query.version, res);\n    } else {\n      res.sendFile(path.join(req.query.path, req.query.filename));\n    }\n  });\n\n  app.post(\n    `${exports.pathPrefix}/discardchanges`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      const task = req.body.all\n        ? gitPromise.discardAllChanges(req.body.path)\n        : gitPromise.discardChangesInFile(req.body.path, req.body.file.trim());\n      jsonResultOrFailProm(res, task.then(emitWorkingTreeChanged.bind(null, req.body.path)));\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/ignorefile`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      const currentPath = req.body.path.trim();\n      const gitIgnoreFile = `${currentPath}/.gitignore`;\n      const ignoreFile = req.body.file.trim();\n      const task = fs.appendFile(gitIgnoreFile, os.EOL + ignoreFile).catch(() => {\n        throw {\n          errorCode: 'error-appending-ignore',\n          error: 'Error while appending to .gitignore file.',\n        };\n      });\n\n      jsonResultOrFailProm(res, task).finally(emitWorkingTreeChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.post(`${exports.pathPrefix}/commit`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise.commit(\n        req.body.path,\n        req.body.amend,\n        req.body.emptyCommit,\n        req.body.message,\n        req.body.files\n      )\n    )\n      .then(emitGitDirectoryChanged.bind(null, req.body.path))\n      .then(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.post(`${exports.pathPrefix}/revert`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const task = gitPromise(['revert', req.body.commit], req.body.path).catch((e) => {\n      if (e.message.indexOf('is a merge but no -m option was given.') > 0) {\n        return gitPromise(['revert', '-m', 1, req.body.commit], req.body.path);\n      } else {\n        throw e;\n      }\n    });\n    jsonResultOrFailProm(res, task)\n      .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n      .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.get(`${exports.pathPrefix}/gitlog`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const limit = getNumber(req.query.limit, config.numberOfNodesPerLoad || 25);\n    const skip = getNumber(req.query.skip, 0);\n    const task = gitPromise\n      .log(req.query.path, limit, skip, config.maxActiveBranchSearchIteration)\n      .catch((err) => {\n        if (\n          err.errorCode === 'no-head' ||\n          err.errorCode === 'no-commits' ||\n          err.errorCode === 'not-a-repository'\n        )\n          return { limit: limit, skip: skip, nodes: [] };\n        throw err;\n      });\n    jsonResultOrFailProm(res, task);\n  });\n\n  app.get(`${exports.pathPrefix}/show`, ensureAuthenticated, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise(['show', '--numstat', '-z', req.query.sha1], req.query.path).then(\n        gitParser.parseGitLog\n      )\n    );\n  });\n\n  app.get(`${exports.pathPrefix}/head`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const task = gitPromise(\n      ['log', '--decorate=full', '--pretty=fuller', '-z', '--parents', '--max-count=1'],\n      req.query.path\n    )\n      .then(gitParser.parseGitLog)\n      .catch((err) => {\n        if (\n          err.errorCode === 'no-head' ||\n          err.errorCode === 'no-commits' ||\n          err.errorCode === 'not-a-repository'\n        )\n          return [];\n        throw err;\n      });\n    jsonResultOrFailProm(res, task);\n  });\n\n  app.get(`${exports.pathPrefix}/refs`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    if (res.setTimeout) res.setTimeout(tenMinTimeoutMs);\n\n    let task = Promise.resolve();\n    if (req.query.remoteFetch) {\n      task = task.then(() =>\n        gitPromise(['remote'], req.query.path).then((remoteText) => {\n          const remotes = remoteText.trim().split('\\n');\n\n          // making calls serially as credential helpers may get confused to which cred to get.\n          return remotes.reduce((promise, remote) => {\n            if (!remote || remote === '') return promise;\n            return promise.then(() => {\n              return gitPromise({\n                commands: credentialsOption(req.query.socketId, remote).concat(['fetch', remote]),\n                repoPath: req.query.path,\n                timeout: tenMinTimeoutMs,\n              }).catch((e) => logger.warn('err during remote fetch for /refs', e)); // ignore fetch err as it is most likely credential\n            });\n          }, Promise.resolve());\n        })\n      );\n    }\n    task = task\n      .then(() => gitPromise(['show-ref', '-d'], req.query.path))\n      // On new fresh repos, empty string is returned but has status code of error, simply ignoring them\n      .catch((e) => {\n        if (e.message !== '') throw e;\n      })\n      .then((refs) => {\n        const results = [];\n        if (refs) {\n          refs\n            .trim()\n            .split('\\n')\n            .forEach((n) => {\n              const splitted = n.split(' ');\n              const sha1 = splitted[0];\n              const name = splitted[1];\n              if (name.indexOf('refs/tags') > -1 && name.indexOf('^{}') > -1) {\n                results[results.length - 1].sha1 = sha1;\n              } else {\n                results.push({\n                  name: name,\n                  sha1: sha1,\n                });\n              }\n            });\n        }\n        return results;\n      });\n    jsonResultOrFailProm(res, task);\n  });\n\n  app.get(`${exports.pathPrefix}/branches`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const isLocalBranchOnly = req.query.isLocalBranchOnly == 'false';\n    jsonResultOrFailProm(\n      res,\n      gitPromise(['branch', isLocalBranchOnly ? '-a' : ''], req.query.path).then(\n        gitParser.parseGitBranches\n      )\n    );\n  });\n\n  app.post(`${exports.pathPrefix}/branches`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const commands = [\n      'branch',\n      req.body.force ? '-f' : '',\n      req.body.name.trim(),\n      (req.body.sha1 || 'HEAD').trim(),\n    ];\n\n    jsonResultOrFailProm(res, gitPromise(commands, req.body.path)).finally(\n      emitGitDirectoryChanged.bind(null, req.body.path)\n    );\n  });\n\n  app.delete(\n    `${exports.pathPrefix}/branches`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(\n        res,\n        gitPromise(['branch', '-D', req.query.name.trim()], req.query.path)\n      ).finally(emitGitDirectoryChanged.bind(null, req.query.path));\n    }\n  );\n\n  app.delete(\n    `${exports.pathPrefix}/remote/branches`,\n    ensureAuthenticated,\n    ensurePathExists,\n    ensureValidSocketId,\n    (req, res) => {\n      const commands = credentialsOption(req.query.socketId, req.query.remote).concat([\n        'push',\n        req.query.remote,\n        `:${req.query.name.trim()}`,\n      ]);\n      const task = gitPromise(commands, req.query.path).catch((err) => {\n        if (!(err.stderr && err.stderr.indexOf('remote ref does not exist') > -1)) {\n          throw err;\n        }\n      });\n\n      jsonResultOrFailProm(res, task).finally(emitGitDirectoryChanged.bind(null, req.query.path));\n    }\n  );\n\n  app.get(`${exports.pathPrefix}/tags`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const task = gitPromise(['tag', '-l'], req.query.path).then(gitParser.parseGitTags);\n    jsonResultOrFailProm(res, task);\n  });\n\n  app.get(\n    `${exports.pathPrefix}/remote/tags`,\n    ensureAuthenticated,\n    ensurePathExists,\n    ensureValidSocketId,\n    (req, res) => {\n      const task = gitPromise(\n        credentialsOption(req.query.socketId, req.query.remote).concat([\n          'ls-remote',\n          '--tags',\n          req.query.remote,\n        ]),\n        req.query.path\n      )\n        .then(gitParser.parseGitLsRemote)\n        .then((result) => {\n          result.forEach((r) => {\n            r.remote = req.query.remote;\n          });\n          return result;\n        });\n      jsonResultOrFailProm(res, task);\n    }\n  );\n\n  app.post(`${exports.pathPrefix}/tags`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const annotateFlag = config.isForceGPGSign ? '-s' : '-a';\n    const forceFlag = req.body.force ? '-f' : '';\n    const sha1 = (req.body.sha1 || 'HEAD').trim();\n    const commands = [\n      'tag',\n      forceFlag,\n      annotateFlag,\n      req.body.name.trim(),\n      '-m',\n      req.body.name.trim(),\n      sha1,\n    ];\n\n    jsonResultOrFailProm(res, gitPromise(commands, req.body.path)).finally(\n      emitGitDirectoryChanged.bind(null, req.body.path)\n    );\n  });\n\n  app.delete(`${exports.pathPrefix}/tags`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise(['tag', '-d', req.query.name.trim()], req.query.path)\n    ).finally(emitGitDirectoryChanged.bind(null, req.query.path));\n  });\n\n  app.delete(\n    `${exports.pathPrefix}/remote/tags`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      const commands = credentialsOption(req.query.socketId, req.query.remote).concat([\n        'push',\n        req.query.remote,\n        `:refs/tags/${req.query.name.trim()}`,\n      ]);\n      const task = gitPromise(['tag', '-d', req.query.name.trim()], req.query.path)\n        .catch(() => {}) // might have already deleted, so ignoring error\n        .then(() => gitPromise(commands, req.query.path));\n\n      jsonResultOrFailProm(res, task).finally(emitGitDirectoryChanged.bind(null, req.query.path));\n    }\n  );\n\n  app.post(`${exports.pathPrefix}/checkout`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const arg = req.body.sha1\n      ? ['checkout', '-b', req.body.name.trim(), req.body.sha1]\n      : ['checkout', req.body.name.trim()];\n\n    jsonResultOrFailProm(res, autoStashExecuteAndPop(arg, req.body.path))\n      .then(emitGitDirectoryChanged.bind(null, req.body.path))\n      .then(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.post(\n    `${exports.pathPrefix}/cherrypick`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(\n        res,\n        autoStashExecuteAndPop(['cherry-pick', req.body.name.trim()], req.body.path)\n      )\n        .then(emitGitDirectoryChanged.bind(null, req.body.path))\n        .then(emitWorkingTreeChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.get(`${exports.pathPrefix}/checkout`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(res, gitPromise.getCurrentBranch(req.query.path));\n  });\n\n  app.get(`${exports.pathPrefix}/remotes`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise(['remote', '-v'], req.query.path).then(gitParser.parseGitRemotes)\n    );\n  });\n\n  app.get(\n    `${exports.pathPrefix}/remotes/:name`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(res, gitPromise.getRemoteAddress(req.query.path, req.params.name));\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/remotes/:name`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(\n        res,\n        gitPromise(['remote', 'add', req.params.name, req.body.url], req.body.path)\n      );\n    }\n  );\n\n  app.delete(\n    `${exports.pathPrefix}/remotes/:name`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(res, gitPromise(['remote', 'remove', req.params.name], req.query.path));\n    }\n  );\n\n  app.post(`${exports.pathPrefix}/merge`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise(['merge', config.noFFMerge ? '--no-ff' : '', req.body.with.trim()], req.body.path)\n    )\n      .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n      .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.post(\n    `${exports.pathPrefix}/merge/continue`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      const args = {\n        commands: ['commit', '--file=-'],\n        repoPath: req.body.path,\n        inPipe: req.body.message,\n      };\n\n      jsonResultOrFailProm(res, gitPromise(args))\n        .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n        .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/merge/abort`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(res, gitPromise(['merge', '--abort'], req.body.path))\n        .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n        .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.post(`${exports.pathPrefix}/squash`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise(['merge', '--squash', req.body.target.trim()], req.body.path)\n    )\n      .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n      .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.post(`${exports.pathPrefix}/rebase`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(res, gitPromise(['rebase', req.body.onto.trim()], req.body.path))\n      .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n      .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.post(\n    `${exports.pathPrefix}/rebase/continue`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(res, gitPromise(['rebase', '--continue'], req.body.path))\n        .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n        .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/rebase/abort`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(res, gitPromise(['rebase', '--abort'], req.body.path))\n        .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n        .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/resolveconflicts`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      logger.info('resolve conflicts');\n      jsonResultOrFailProm(res, gitPromise.resolveConflicts(req.body.path, req.body.files)).then(\n        emitWorkingTreeChanged.bind(null, req.body.path)\n      );\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/launchmergetool`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      const commands = [\n        'mergetool',\n        ...(typeof req.body.tool === 'string' ? ['--tool ', req.body.tool] : []),\n        '--no-prompt',\n        req.body.file,\n      ];\n      gitPromise(commands, req.body.path);\n      // Send immediate response, this is because merging may take a long time\n      // and there is no need to wait for it to finish.\n      res.json({});\n    }\n  );\n\n  app.get(\n    `${exports.pathPrefix}/baserepopath`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      const currentPath = path.resolve(path.join(req.query.path, '..'));\n      jsonResultOrFailProm(\n        res,\n        gitPromise(['rev-parse', '--show-toplevel'], currentPath)\n          .then((baseRepoPath) => {\n            return { path: path.resolve(baseRepoPath.trim()) };\n          })\n          .catch((e) => {\n            if (e.errorCode === 'not-a-repository' || e.errorCode === 'must-be-in-working-tree') {\n              // not a repository or a bare repository\n              return {};\n            }\n            throw e;\n          })\n      );\n    }\n  );\n\n  app.get(`${exports.pathPrefix}/submodules`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const filename = path.join(req.query.path, '.gitmodules');\n\n    const task = fs\n      .access(filename)\n      .then(() => {\n        return fs.readFile(filename, { encoding: 'utf8' }).then(gitParser.parseGitSubmodule);\n      })\n      .catch(() => {\n        return {};\n      });\n    jsonResultOrFailProm(res, task);\n  });\n\n  app.post(\n    `${exports.pathPrefix}/submodules/update`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(\n        res,\n        gitPromise(['submodule', 'init'], req.body.path).then(\n          gitPromise.bind(null, ['submodule', 'update'], req.body.path)\n        )\n      );\n    }\n  );\n\n  app.post(\n    `${exports.pathPrefix}/submodules/add`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      jsonResultOrFailProm(\n        res,\n        gitPromise(\n          ['submodule', 'add', req.body.submoduleUrl.trim(), req.body.submodulePath.trim()],\n          req.body.path\n        )\n      )\n        .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n        .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n    }\n  );\n\n  app.delete(\n    `${exports.pathPrefix}/submodules`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      // -f is needed for the cases when added submodule change is not in the staging or committed\n      const task = gitPromise(\n        ['submodule', 'deinit', '-f', req.query.submoduleName],\n        req.query.path\n      )\n        .then(gitPromise.bind(null, ['rm', '-f', req.query.submoduleName], req.query.path))\n        .then(() => {\n          return Promise.all([\n            rimraf(path.join(req.query.path, req.query.submodulePath)),\n            rimraf(path.join(req.query.path, '.git', 'modules', req.query.submodulePath)),\n          ]);\n        });\n\n      jsonResultOrFailProm(res, task);\n    }\n  );\n\n  app.get(`${exports.pathPrefix}/quickstatus`, ensureAuthenticated, (req, res) => {\n    const task = fs\n      .access(req.query.path)\n      .then(() => {\n        return gitPromise.revParse(req.query.path);\n      })\n      .then((revParseRes) => {\n        if (revParseRes.type !== 'uninited') {\n          return revParseRes;\n        }\n\n        // for uninited directory, let's check if it's any immediate directories are\n        // git repository so we can display them.\n        return fs\n          .readdir(req.query.path)\n          .then((filePaths) => {\n            return Promise.all(\n              filePaths\n                .filter((filePath) => !filePath.startsWith('.'))\n                .map((filePath) => gitPromise.revParse(path.join(req.query.path, filePath)))\n            );\n          })\n          .then((pathRevParses) => {\n            revParseRes.subRepos = pathRevParses\n              .filter(\n                (pathRevParse) => pathRevParse.type === 'inited' || pathRevParse.type === 'bare'\n              )\n              .map((pathRevParse) => pathRevParse.gitRootPath);\n            return revParseRes;\n          });\n      })\n      .catch((e) => {\n        logger.error('failed during /quickstatus', e);\n        return { type: 'no-such-path', gitRootPath: req.query.path };\n      });\n    jsonResultOrFailProm(res, task);\n  });\n\n  app.get(`${exports.pathPrefix}/stashes`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    const task = gitPromise(\n      ['stash', 'list', '--decorate=full', '--pretty=fuller', '-z', '--parents', '--numstat'],\n      req.query.path\n    ).then(gitParser.parseGitLog);\n    jsonResultOrFailProm(res, task);\n  });\n\n  app.post(`${exports.pathPrefix}/stashes`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    jsonResultOrFailProm(\n      res,\n      gitPromise(['stash', 'save', '--include-untracked', req.body.message || ''], req.body.path)\n    )\n      .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n      .finally(emitWorkingTreeChanged.bind(null, req.body.path));\n  });\n\n  app.delete(\n    `${exports.pathPrefix}/stashes/:id`,\n    ensureAuthenticated,\n    ensurePathExists,\n    (req, res) => {\n      const type = req.query.apply === 'true' ? 'apply' : 'drop';\n      jsonResultOrFailProm(\n        res,\n        gitPromise(['stash', type, `stash@{${req.params.id}}`], req.query.path)\n      )\n        .finally(emitGitDirectoryChanged.bind(null, req.query.path))\n        .finally(emitWorkingTreeChanged.bind(null, req.query.path));\n    }\n  );\n\n  app.get(`${exports.pathPrefix}/gitconfig`, ensureAuthenticated, (req, res) => {\n    jsonResultOrFailProm(res, gitPromise(['config', '--list']).then(gitParser.parseGitConfig));\n  });\n\n  // This method isn't called by the client but by credentials-helper.js\n  app.get(`${exports.pathPrefix}/credentials`, (req, res) => {\n    // this endpoint can only be invoked from localhost, since the credentials-helper is always\n    // on the same machine that we're running ungit on\n    if (req.ip != '127.0.0.1' && req.ip != '::ffff:127.0.0.1') {\n      logger.info(`Trying to get credentials from unathorized ip: ${req.ip}`);\n      res.status(400).json({ errorCode: 'request-from-unathorized-location' });\n      return;\n    }\n    const socket = socketsById[req.query.socketId];\n    const remote = req.query.remote;\n    if (!socket) {\n      // We're using the socket to display an authentication dialog in the ui,\n      // so if the socket is closed/unavailable we pretty much can't get the username/password.\n      logger.info(`Trying to get credentials from unavailable socket: ${req.query.socketId}`);\n      res.status(400).json({ errorCode: 'socket-unavailable' });\n    } else {\n      socket.once('credentials', (data) => res.json(data));\n      socket.emit('request-credentials', { remote: remote });\n    }\n  });\n\n  app.post(`${exports.pathPrefix}/createdir`, ensureAuthenticated, (req, res) => {\n    const dir = req.query.dir || req.body.dir;\n    if (!dir) {\n      return res.status(400).json({\n        errorCode: 'missing-request-parameter',\n        error: 'You need to supply the path request parameter',\n      });\n    }\n\n    mkdirp(dir)\n      .then(() => res.json({}))\n      .catch((err) => res.status(400).json(err));\n  });\n\n  app.get(`${exports.pathPrefix}/gitignore`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    fs.readFile(path.join(req.query.path, '.gitignore'), { encoding: 'utf8' })\n      .then((ignoreContent) => res.status(200).json({ content: ignoreContent }))\n      .catch((e) => {\n        if (e && e.message && e.message.indexOf('no such file or directory') > -1) {\n          res.status(200).json({ content: '' });\n        } else {\n          res.status(500).json(e);\n        }\n      });\n  });\n  app.put(`${exports.pathPrefix}/gitignore`, ensureAuthenticated, ensurePathExists, (req, res) => {\n    if (!req.body.data && req.body.data !== '') {\n      return res.status(400).json({ message: 'Invalid .gitignore content' });\n    }\n    fs.writeFile(path.join(req.body.path, '.gitignore'), req.body.data)\n      .then(() => res.status(200).json({}))\n      .finally(emitGitDirectoryChanged.bind(null, req.body.path))\n      .catch((e) => res.status(500).json(e));\n  });\n\n  if (config.dev) {\n    app.post(`${exports.pathPrefix}/testing/createtempdir`, ensureAuthenticated, (req, res) => {\n      temp.mkdir('test-temp-dir', (err, tempPath) => res.json({ path: path.normalize(tempPath) }));\n    });\n    app.post(`${exports.pathPrefix}/testing/createfile`, ensureAuthenticated, (req, res) => {\n      const content = req.body.content ? req.body.content : `test content\\n${Math.random()}\\n`;\n      fs.writeFile(req.body.file, content)\n        .then(() => res.json({}))\n        .then(emitWorkingTreeChanged.bind(null, req.body.path));\n    });\n    app.post(`${exports.pathPrefix}/testing/changefile`, ensureAuthenticated, (req, res) => {\n      const content = req.body.content ? req.body.content : `test content\\n${Math.random()}\\n`;\n      fs.writeFile(req.body.file, content)\n        .then(() => res.json({}))\n        .then(emitWorkingTreeChanged.bind(null, req.body.path));\n    });\n    app.post(`${exports.pathPrefix}/testing/createimagefile`, ensureAuthenticated, (req, res) => {\n      fs.writeFile(req.body.file, 'png', { encoding: 'binary' })\n        .then(() => res.json({}))\n        .then(emitWorkingTreeChanged.bind(null, req.body.path));\n    });\n    app.post(`${exports.pathPrefix}/testing/changeimagefile`, ensureAuthenticated, (req, res) => {\n      fs.writeFile(req.body.file, 'png ~~', { encoding: 'binary' })\n        .then(() => res.json({}))\n        .then(emitWorkingTreeChanged.bind(null, req.body.path));\n    });\n    app.post(`${exports.pathPrefix}/testing/removefile`, ensureAuthenticated, (req, res) => {\n      fs.unlink(req.body.file)\n        .then(() => res.json({}))\n        .then(emitWorkingTreeChanged.bind(null, req.body.path));\n    });\n    app.post(`${exports.pathPrefix}/testing/git`, ensureAuthenticated, (req, res) => {\n      jsonResultOrFailProm(res, gitPromise(req.body.command, req.body.path)).then(\n        emitWorkingTreeChanged.bind(null, req.body.path)\n      );\n    });\n    app.post(`${exports.pathPrefix}/testing/cleanup`, (req, res) => {\n      temp.cleanup((err, cleaned) => {\n        logger.info('Cleaned up: ' + JSON.stringify(cleaned));\n        res.json({ result: cleaned });\n      });\n    });\n  }\n};\n"
  },
  {
    "path": "source/git-parser.js",
    "content": "const path = require('path');\nconst fileType = require('./utils/file-type.js');\n\nexports.parseGitStatus = (text) => {\n  let lines = text.split('\\x00');\n  const branch = lines[0].split(' ').pop();\n  // skipping first line...\n  lines = lines.slice(1);\n  const files = {};\n\n  for (let i = 0; i < lines.length; i++) {\n    const line = lines[i];\n    if (line == '') continue;\n    const status = line.slice(0, 2);\n    const newFileName = line.slice(3).trim();\n    let oldFileName;\n    let displayName;\n    if (status[0] == 'R') {\n      oldFileName = lines[++i];\n      displayName = `${oldFileName} → ${newFileName}`;\n    } else {\n      oldFileName = newFileName;\n      displayName = newFileName;\n    }\n    files[newFileName] = {\n      fileName: newFileName,\n      oldFileName: oldFileName,\n      displayName: displayName,\n      staged: status[0] == 'A' || status[0] == 'M',\n      removed: status[0] == 'D' || status[1] == 'D',\n      isNew: (status[0] == '?' || status[0] == 'A') && status[1] != 'D',\n      conflict: (status[0] == 'A' && status[1] == 'A') || status[0] == 'U' || status[1] == 'U',\n      renamed: status[0] == 'R',\n      type: fileType(newFileName),\n    };\n  }\n\n  return {\n    branch: branch,\n    files: files,\n  };\n};\n\nconst fileChangeRegex =\n  /(?<additions>[\\d-]+)\\t(?<deletions>[\\d-]+)\\t((?<fileName>[^\\x00]+?)\\x00|\\x00(?<oldFileName>[^\\x00]+?)\\x00(?<newFileName>[^\\x00]+?)\\x00)/g;\n\nexports.parseGitStatusNumstat = (text) => {\n  const result = {};\n  fileChangeRegex.lastIndex = 0;\n  let match = fileChangeRegex.exec(text);\n  while (match !== null) {\n    result[match.groups.fileName || match.groups.newFileName] = {\n      additions: match.groups.additions,\n      deletions: match.groups.deletions,\n    };\n    match = fileChangeRegex.exec(text);\n  }\n  return result;\n};\n\nconst authorRegexp = /([^<]+)<([^>]+)>/;\nconst gitLogHeaders = {\n  Author: (currentCommmit, author) => {\n    const capture = authorRegexp.exec(author);\n    if (capture) {\n      currentCommmit.authorName = capture[1].trim();\n      currentCommmit.authorEmail = capture[2].trim();\n    } else {\n      currentCommmit.authorName = author;\n    }\n  },\n  Commit: (currentCommmit, author) => {\n    const capture = authorRegexp.exec(author);\n    if (capture) {\n      currentCommmit.committerName = capture[1].trim();\n      currentCommmit.committerEmail = capture[2].trim();\n    } else {\n      currentCommmit.committerName = author;\n    }\n  },\n  AuthorDate: (currentCommmit, date) => {\n    currentCommmit.authorDate = date;\n  },\n  CommitDate: (currentCommmit, date) => {\n    currentCommmit.commitDate = date;\n  },\n  Reflog: (currentCommmit, data) => {\n    currentCommmit.reflogId = /\\{(.*?)\\}/.exec(data)[1];\n    currentCommmit.reflogName = data.substring(0, data.indexOf(' ')).replace('refs/', '');\n    const author = data.substring(data.indexOf('(') + 1, data.length - 1);\n    const capture = authorRegexp.exec(author);\n    if (capture) {\n      currentCommmit.reflogAuthorName = capture[1].trim();\n      currentCommmit.reflogAuthorEmail = capture[2].trim();\n    } else {\n      currentCommmit.reflogAuthorName = author;\n    }\n  },\n  gpg: (currentCommit, data) => {\n    if (data.startsWith('Signature made')) {\n      // extract sign date\n      currentCommit.signatureDate = data.slice('Signature made '.length);\n    } else if (data.indexOf('Good signature from') > -1) {\n      // fully verified.\n      currentCommit.signatureMade = data\n        .slice('Good signature from '.length)\n        .replace('[ultimate]', '')\n        .trim();\n    } else if (data.indexOf(\"Can't check signature\") > -1) {\n      // pgp signature attempt is made but failed to verify\n      delete currentCommit.signatureDate;\n    }\n  },\n};\nexports.parseGitLog = (data) => {\n  const commits = [];\n  let currentCommmit;\n  const parseCommitLine = (row) => {\n    if (!row.trim()) return;\n    currentCommmit = { refs: [], fileLineDiffs: [], additions: 0, deletions: 0 };\n    const refStartIndex = row.indexOf('(');\n    const sha1s = row\n      .substring(0, refStartIndex < 0 ? row.length : refStartIndex)\n      .split(' ')\n      .slice(1)\n      .filter((sha1) => {\n        return sha1 && sha1.length;\n      });\n    currentCommmit.sha1 = sha1s[0];\n    currentCommmit.parents = sha1s.slice(1);\n    if (refStartIndex > 0) {\n      const refs = row.substring(refStartIndex + 1, row.length - 1);\n      currentCommmit.refs = refs.split(/ -> |, /g);\n    }\n    currentCommmit.isHead = currentCommmit.refs.some((item) => {\n      return item.trim() === 'HEAD';\n    });\n    commits.isHeadExist = commits.isHeadExist || currentCommmit.isHead;\n    commits.push(currentCommmit);\n    parser = parseHeaderLine;\n  };\n  const parseHeaderLine = (row) => {\n    if (row.trim() == '') {\n      parser = parseCommitMessage;\n    } else {\n      for (const key in gitLogHeaders) {\n        if (row.indexOf(`${key}: `) == 0) {\n          gitLogHeaders[key](currentCommmit, row.slice(`${key}: `.length).trim());\n          return;\n        }\n      }\n    }\n  };\n  const parseCommitMessage = (row, index) => {\n    if (currentCommmit.message) currentCommmit.message += '\\n';\n    else currentCommmit.message = '';\n    currentCommmit.message += row.trim();\n    if (/[\\d-]+\\t[\\d-]+\\t.+/g.test(rows[index + 1])) {\n      parser = parseFileChanges;\n      return;\n    }\n    if (rows[index + 1] && /^\\u0000+commit/.test(rows[index + 1])) {\n      parser = parseCommitLine;\n      return;\n    }\n  };\n  const parseFileChanges = (row, index) => {\n    // git log is using -z so all the file changes are on one line\n    // merge commits start the file changes with a null\n    if (row[0] === '\\x00') {\n      row = row.slice(1);\n    }\n    fileChangeRegex.lastIndex = 0;\n    while (row[fileChangeRegex.lastIndex] && row[fileChangeRegex.lastIndex] !== '\\x00') {\n      const match = fileChangeRegex.exec(row);\n      const fileName = match.groups.fileName || match.groups.newFileName;\n      const oldFileName = match.groups.oldFileName || match.groups.fileName;\n      let displayName;\n      if (match.groups.oldFileName) {\n        displayName = `${match.groups.oldFileName} → ${match.groups.newFileName}`;\n      } else {\n        displayName = fileName;\n      }\n      currentCommmit.fileLineDiffs.push({\n        additions: match.groups.additions,\n        deletions: match.groups.deletions,\n        fileName: fileName,\n        oldFileName: oldFileName,\n        displayName: displayName,\n        type: fileType(fileName),\n      });\n    }\n    const nextRow = row.slice(fileChangeRegex.lastIndex + 1);\n    for (const fileLineDiff of currentCommmit.fileLineDiffs) {\n      if (!isNaN(parseInt(fileLineDiff.additions, 10))) {\n        currentCommmit.additions += fileLineDiff.additions = parseInt(fileLineDiff.additions, 10);\n      }\n      if (!isNaN(parseInt(fileLineDiff.deletions, 10))) {\n        currentCommmit.deletions += fileLineDiff.deletions = parseInt(fileLineDiff.deletions, 10);\n      }\n    }\n    parser = parseCommitLine;\n    if (nextRow) {\n      parser(nextRow, index);\n    }\n    return;\n  };\n  let parser = parseCommitLine;\n  const rows = data.split('\\n');\n  rows.forEach((row, index) => {\n    parser(row, index);\n  });\n\n  commits.forEach((commit) => {\n    commit.message = typeof commit.message === 'string' ? commit.message.trim() : '';\n  });\n  return commits;\n};\n\nexports.parseGitConfig = (text) => {\n  const conf = {};\n  text.split('\\n').forEach((row) => {\n    const ss = row.split('=');\n    conf[ss[0]] = ss[1];\n  });\n  return conf;\n};\n\nexports.parseGitBranches = (text) => {\n  const branches = [];\n  text.split('\\n').forEach((row) => {\n    if (row.trim() == '') return;\n    const branch = { name: row.slice(2) };\n    if (row[0] == '*') branch.current = true;\n    branches.push(branch);\n  });\n  return branches;\n};\n\nexports.parseGitTags = (text) => {\n  return text.split('\\n').filter((tag) => {\n    return tag != '';\n  });\n};\n\nexports.parseGitRemotes = (text) => {\n  const remotes = {};\n  text.split('\\n').forEach((row) => {\n    if (row.trim() == '') return;\n    const parts = row.split('\\t');\n    const name = parts[0];\n    const remote = remotes[name] || { name };\n    if (parts.length > 1) {\n      const url = parts[1];\n      if (url.endsWith(' (fetch)')) {\n        remote.fetchUrl = url.substring(0, url.length - 8);\n      } else if (url.endsWith(' (push)')) {\n        remote.pushUrl = url.substring(0, url.length - 7);\n      } else {\n        remote.url = url;\n      }\n    }\n    remotes[name] = remote;\n  });\n  return Object.values(remotes);\n};\n\nexports.parseGitLsRemote = (text) => {\n  return text\n    .split('\\n')\n    .filter((item) => {\n      return item && item.indexOf('From ') != 0;\n    })\n    .map((line) => {\n      const sha1 = line.slice(0, 40);\n      const name = line.slice(41).trim();\n      return { sha1: sha1, name: name };\n    });\n};\n\nexports.parseGitStashShow = (text) => {\n  const lines = text.split('\\n').filter((item) => item);\n  return lines.slice(0, lines.length - 1).map((line) => {\n    return { filename: line.substring(0, line.indexOf('|')).trim() };\n  });\n};\n\nexports.parseGitSubmodule = (text) => {\n  if (!text) {\n    return [];\n  }\n\n  let submodule;\n  const submodules = [];\n\n  text\n    .trim()\n    .split('\\n')\n    .filter((line) => line)\n    .forEach((line) => {\n      if (line.indexOf('[submodule') === 0) {\n        submodule = { name: line.match(/\"(.*?)\"/)[1] };\n        submodules.push(submodule);\n      } else {\n        const parts = line.split('=');\n        const key = parts[0].trim();\n        let value = parts.slice(1).join('=').trim();\n\n        if (key == 'path') {\n          value = path.normalize(value);\n        } else if (key == 'url') {\n          // keep a reference to the raw url\n          let url = (submodule.rawUrl = value);\n\n          // When a repo is checkout with ssh or git instead of an url\n          if (url.indexOf('http') != 0) {\n            if (url.indexOf('git:') == 0) {\n              // git\n              url = `http${url.substring(url.indexOf(':'))}`;\n            } else {\n              // ssh\n              url = `http://${url.substring(url.indexOf('@') + 1).replace(':', '/')}`;\n            }\n          }\n\n          value = url;\n        }\n\n        submodule[key] = value;\n      }\n    });\n\n  const sorted_submodules = submodules.sort((a, b) => a.name.localeCompare(b.name));\n\n  return sorted_submodules;\n};\n\nconst updatePatchHeader = (\n  result,\n  lastHeaderIndex,\n  ignoredDiffCountTotal,\n  ignoredDiffCountCurrent\n) => {\n  const splitedHeader = result[lastHeaderIndex].split(' ');\n  const start = splitedHeader[1].split(','); // start of block\n  const end = splitedHeader[2].split(','); // end of block\n  const startLeft = Math.abs(start[0]);\n  const startRight = Math.abs(start[1]);\n  const endLeft = end[0];\n  const endRight = end[1];\n\n  splitedHeader[1] = `-${startLeft - ignoredDiffCountTotal},${startRight}`;\n  splitedHeader[2] = `+${endLeft - ignoredDiffCountTotal},${endRight - ignoredDiffCountCurrent}`;\n\n  let allSpace = true;\n  for (let i = lastHeaderIndex + 1; i < result.length; i++) {\n    if (result[i][0] != ' ') {\n      allSpace = false;\n      break;\n    }\n  }\n  if (allSpace) result.splice(lastHeaderIndex, result.length - lastHeaderIndex);\n  else result[lastHeaderIndex] = splitedHeader.join(' ');\n};\n\nexports.parsePatchDiffResult = (patchLineList, text) => {\n  if (!text) return null;\n\n  const lines = text.trim().split('\\n');\n  const result = [];\n  let ignoredDiffCountTotal = 0;\n  let ignoredDiffCountCurrent = 0;\n  let lastHeaderIndex = -1;\n  let n = 0;\n  let selectedLines = 0;\n\n  // first add all lines until diff block header is found\n  while (!/@@ -[0-9]+,[0-9]+ \\+[0-9]+,[0-9]+ @@/.test(lines[n])) {\n    result.push(lines[n]);\n    n++;\n  }\n\n  // per rest of the lines\n  while (n < lines.length) {\n    const line = lines[n];\n\n    if (/^[-+]/.test(line)) {\n      // Modified line\n      if (patchLineList.shift()) {\n        selectedLines++;\n        // diff is selected to be committed\n        result.push(line);\n      } else if (line[0] === '+') {\n        // added line diff is selected to be ignored\n        ignoredDiffCountCurrent++;\n      } else {\n        // lines[0] === '-'\n        // deleted line diff is selected to be ignored\n        ignoredDiffCountCurrent--;\n        result.push(` ${line.slice(1)}`);\n      }\n    } else {\n      // none modified line or diff block header\n      if (/@@ -[0-9]+,[0-9]+ \\+[0-9]+,[0-9]+ @@/.test(line)) {\n        // update previous header to match line numbers\n        if (lastHeaderIndex > -1) {\n          updatePatchHeader(\n            result,\n            lastHeaderIndex,\n            ignoredDiffCountTotal,\n            ignoredDiffCountCurrent\n          );\n        }\n        // diff block header\n        ignoredDiffCountTotal += ignoredDiffCountCurrent;\n        ignoredDiffCountCurrent = 0;\n        lastHeaderIndex = result.length;\n      }\n      result.push(line);\n    }\n    n++;\n  }\n\n  // We don't want to leave out last diff block header...\n  updatePatchHeader(result, lastHeaderIndex, ignoredDiffCountTotal, ignoredDiffCountCurrent);\n\n  if (selectedLines > 0) {\n    return result.join('\\n');\n  } else {\n    return null;\n  }\n};\n"
  },
  {
    "path": "source/git-promise.js",
    "content": "const child_process = require('child_process');\nconst gitParser = require('./git-parser');\nconst path = require('path');\nconst config = require('./config');\nconst logger = require('./utils/logger');\nconst addressParser = require('./address-parser');\nconst isWindows = /^win/.test(process.platform);\nconst pLimitPromise = import('p-limit');\nconst fs = require('fs').promises;\nconst gitEmptyReproSha1 = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'; // https://stackoverflow.com/q/9765453\nconst gitEmptyReproSha256 = '6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321'; // https://stackoverflow.com/q/9765453\nconst gitConfigArguments = [\n  '-c',\n  'color.ui=false',\n  '-c',\n  'core.quotepath=false',\n  '-c',\n  'core.pager=cat',\n  '-c',\n  'core.editor=:',\n];\nconst gitOptionalLocks = config.isGitOptionalLocks ? '--no-optional-locks' : '';\nconst gitBin = (() => {\n  if (config.gitBinPath) {\n    return (config.gitBinPath.endsWith('/') ? config.gitBinPath : config.gitBinPath + '/') + 'git';\n  }\n  return 'git';\n})();\n\nconst isRetryableError = (err) => {\n  const errMsg = (err || {}).error || '';\n  // Dued to git operation parallelization it is possible that race condition may happen\n  if (errMsg.indexOf(\"index.lock': File exists\") > -1) return true;\n  // TODO: Issue #796, based on the conversation with Appveyor team, I guess Windows system\n  // can report \"Permission denied\" for the file locking issue.\n  if (errMsg.indexOf('index file open failed: Permission denied') > -1) return true;\n  return false;\n};\n\nlet pLimit = (fn) => {\n  try {\n    return Promise.resolve(fn());\n  } catch (err) {\n    return Promise.reject(err);\n  }\n};\npLimitPromise.then((limit) => {\n  pLimit = limit.default(config.maxConcurrentGitOperations);\n});\n\nconst gitExecutorProm = (args, retryCount) => {\n  let timeoutTimer;\n  return pLimit(() => {\n    return new Promise((resolve, reject) => {\n      if (config.logGitCommands)\n        logger.info(`git executing: ${args.repoPath} ${args.commands.join(' ')}`);\n      let rejectedError = null;\n      let stdout = '';\n      let stderr = '';\n      const env = JSON.parse(JSON.stringify(process.env));\n      env['LC_ALL'] = 'C';\n      const procOpts = {\n        cwd: args.repoPath,\n        maxBuffer: 1024 * 1024 * 100,\n        detached: false,\n        env: env,\n      };\n      const gitProcess = child_process.spawn(gitBin, args.commands, procOpts);\n      timeoutTimer = setTimeout(() => {\n        if (!timeoutTimer) return;\n        timeoutTimer = null;\n\n        logger.warn(`command timedout: ${args.commands.join(' ')}\\n`);\n        gitProcess.kill('SIGINT');\n      }, args.timeout);\n\n      if (args.outPipe) {\n        gitProcess.stdout.pipe(args.outPipe);\n      } else {\n        gitProcess.stdout.on('data', (data) => (stdout += data.toString()));\n      }\n      if (args.inPipe) {\n        gitProcess.stdin.end(args.inPipe);\n      }\n      gitProcess.stderr.on('data', (data) => (stderr += data.toString()));\n      gitProcess.on('error', (error) => (rejectedError = error));\n\n      gitProcess.on('close', (code) => {\n        if (config.logGitCommands)\n          logger.info(\n            `git result (first 400 bytes): ${args.commands.join(' ')}\\n${stderr.slice(\n              0,\n              400\n            )}\\n${stdout.slice(0, 400)}`\n          );\n        if (rejectedError) {\n          reject(rejectedError);\n        } else if (code === 0 || (code === 1 && args.allowError)) {\n          resolve(stdout);\n        } else {\n          reject(getGitError(args, stderr, stdout));\n        }\n      });\n    });\n  })\n    .catch((err) => {\n      if (retryCount > 0 && isRetryableError(err)) {\n        return new Promise((resolve) => {\n          logger.warn(\n            'retrying git commands after lock acquired fail. (If persists, lower \"maxConcurrentGitOperations\")'\n          );\n          // sleep random amount between 250 ~ 750 ms\n          setTimeout(resolve, Math.floor(Math.random() * 500 + 250));\n        }).then(gitExecutorProm.bind(null, args, retryCount - 1));\n      } else {\n        throw err;\n      }\n    })\n    .finally(() => {\n      if (args.outPipe) args.outPipe.end();\n      if (timeoutTimer) clearTimeout(timeoutTimer);\n    });\n};\n\n/**\n * Returns a promise that executes git command with given arguments.\n *\n * @function\n * @param {Object | string[]} commands    - An object that represents all parameters or first\n *                                        parameter only, which is an array of commands.\n * @param {string}            repoPath    - path to the git repository.\n * @param {boolean=}          allowError  - true if return code of 1 is acceptable as some cases\n *                                        errors are acceptable.\n * @param {WritableStream=}   outPipe     - if this argument exists, stdout is piped to this object.\n * @param {ReadableStream=}   inPipe      - if this argument exists, data is piped to stdin process\n *                                        on start.\n * @param {number=}           timeout     - execution timeout, default is 2 mins.\n * @returns {promise} Execution promise.\n * @example\n *\n *   getGitExecuteTask({ commands: ['show'], repoPath: '/tmp' });\n *\n * @example\n *\n *   getGitExecuteTask(['show'], '/tmp');\n *\n */\nconst git = (commands, repoPath, allowError, outPipe, inPipe, timeout) => {\n  let args = {};\n  if (Array.isArray(commands)) {\n    args.commands = commands;\n    args.repoPath = repoPath;\n    args.outPipe = outPipe;\n    args.inPipe = inPipe;\n    args.allowError = allowError;\n    args.timeout = timeout;\n  } else {\n    args = commands;\n  }\n\n  args.commands = gitConfigArguments.concat(\n    args.commands.filter((element) => {\n      return element;\n    })\n  );\n  args.timeout = args.timeout || 2 * 60 * 1000; // Default timeout tasks after 2 min\n  args.startTime = Date.now();\n\n  return gitExecutorProm(args, config.lockConflictRetryCount);\n};\n\nconst getGitError = (args, stderr, stdout) => {\n  const err = {};\n  err.isGitError = true;\n  err.errorCode = 'unknown';\n  err.command = args.commands.join(' ');\n  err.workingDirectory = args.repoPath;\n  err.error = stderr.toString();\n  err.message = err.error.split('\\n')[0];\n  err.stderr = stderr;\n  err.stdout = stdout;\n  err.stdoutLower = (stdout || '').toLowerCase();\n  err.stderrLower = (stderr || '').toLowerCase();\n  if (err.stderrLower.indexOf('not a git repository') >= 0) {\n    err.errorCode = 'not-a-repository';\n  } else if (err.stderrLower.indexOf(\"bad default revision 'head'\") != -1) {\n    err.errorCode = 'no-head';\n  } else if (err.stderrLower.indexOf('does not have any commits yet') != -1) {\n    err.errorCode = 'no-commits';\n  } else if (err.stderrLower.indexOf('connection timed out') != -1) {\n    err.errorCode = 'remote-timeout';\n  } else if (err.stderrLower.indexOf('permission denied (publickey)') != -1) {\n    err.errorCode = 'permision-denied-publickey';\n  } else if (\n    err.stderrLower.indexOf('ssh: connect to host') != -1 &&\n    err.stderrLower.indexOf('bad file number') != -1\n  ) {\n    err.errorCode = 'ssh-bad-file-number';\n  } else if (err.stderrLower.indexOf('no remote configured to list refs from.') != -1) {\n    err.errorCode = 'no-remote-configured';\n  } else if (\n    (err.stderrLower.indexOf('unable to access') != -1 &&\n      err.stderrLower.indexOf('could not resolve host:') != -1) ||\n    err.stderrLower.indexOf('could not resolve hostname') != -1\n  ) {\n    err.errorCode = 'offline';\n  } else if (err.stderrLower.indexOf('proxy authentication required') != -1) {\n    err.errorCode = 'proxy-authentication-required';\n  } else if (err.stderrLower.indexOf('please tell me who you are') != -1) {\n    err.errorCode = 'no-git-name-email-configured';\n  } else if (\n    err.stderrLower.indexOf(\n      'fatal error: disconnected: no supported authentication methods available (server sent: publickey)'\n    ) == 0\n  ) {\n    err.errorCode = 'no-supported-authentication-provided';\n  } else if (err.stderrLower.indexOf('fatal: no remote repository specified.') == 0) {\n    err.errorCode = 'no-remote-specified';\n  } else if (err.stderrLower.indexOf('non-fast-forward') != -1) {\n    err.errorCode = 'non-fast-forward';\n  } else if (\n    err.stderrLower.indexOf('failed to merge in the changes.') == 0 ||\n    err.stdoutLower.indexOf('conflict (content): merge conflict in') != -1 ||\n    err.stderrLower.indexOf('after resolving the conflicts') != -1\n  ) {\n    err.errorCode = 'merge-failed';\n  } else if (err.stderrLower.indexOf('this operation must be run in a work tree') != -1) {\n    err.errorCode = 'must-be-in-working-tree';\n  } else if (\n    err.stderrLower.indexOf(\n      'your local changes to the following files would be overwritten by checkout'\n    ) != -1\n  ) {\n    err.errorCode = 'local-changes-would-be-overwritten';\n  }\n\n  return err;\n};\n\ngit.status = (repoPath, file) => {\n  return Promise.all([\n    // 0: numStatsStaged\n    git([gitOptionalLocks, 'diff', '--numstat', '--cached', '-z', '--', file || ''], repoPath).then(\n      gitParser.parseGitStatusNumstat\n    ),\n    // 1: numStatsUnstaged\n    config.isEnableNumStat\n      ? git([gitOptionalLocks, 'diff', '--numstat', '-z', '--', file || ''], repoPath).then(\n          gitParser.parseGitStatusNumstat\n        )\n      : {},\n    // 2: status\n    git([gitOptionalLocks, 'status', '-s', '-b', '-u', '-z', file || ''], repoPath)\n      .then(gitParser.parseGitStatus)\n      .then((status) => {\n        return Promise.all([\n          // 0: isRebaseMerge\n          fs\n            .access(path.join(repoPath, '.git', 'rebase-merge'))\n            .then(() => true)\n            .catch(() => false),\n          // 1: isRebaseApply\n          fs\n            .access(path.join(repoPath, '.git', 'rebase-apply'))\n            .then(() => true)\n            .catch(() => false),\n          // 2: isMerge\n          fs\n            .access(path.join(repoPath, '.git', 'MERGE_HEAD'))\n            .then(() => true)\n            .catch(() => false),\n          // 3: inCherry\n          fs\n            .access(path.join(repoPath, '.git', 'CHERRY_PICK_HEAD'))\n            .then(() => true)\n            .catch(() => false),\n        ])\n          .then((result) => {\n            status.inRebase = result[0] || result[1];\n            status.inMerge = result[2];\n            status.inCherry = result[3];\n          })\n          .then(() => {\n            if (status.inMerge || status.inCherry) {\n              return fs\n                .readFile(path.join(repoPath, '.git', 'MERGE_MSG'), { encoding: 'utf8' })\n                .then((commitMessage) => {\n                  status.commitMessage = commitMessage;\n                  return status;\n                })\n                .catch(() => {\n                  // 'MERGE_MSG' file is gone away, which means we are no longer in merge state\n                  // and state changed while this call is being made.\n                  status.inMerge = status.inCherry = false;\n                  return status;\n                });\n            }\n            return status;\n          });\n      }),\n  ]).then((result) => {\n    const numstats = [result[0], result[1]].reduce(Object.assign, {});\n    const status = result[2];\n    status.inConflict = false;\n\n    // merge numstats\n    Object.keys(status.files).forEach((filename) => {\n      // git diff returns paths relative to git repo but git status does not\n      const absoluteFilename = filename.replace(/\\.\\.\\//g, '');\n      const stats = numstats[absoluteFilename] || { additions: '-', deletions: '-' };\n      const fileObj = status.files[filename];\n      fileObj.additions = stats.additions;\n      fileObj.deletions = stats.deletions;\n      if (!status.inConflict && fileObj.conflict) {\n        status.inConflict = true;\n      }\n    });\n\n    return status;\n  });\n};\n\ngit.getRemoteAddress = (repoPath, remoteName) => {\n  return git(['config', '--get', `remote.${remoteName}.url`], repoPath).then((text) =>\n    addressParser.parseAddress(text.split('\\n')[0])\n  );\n};\n\ngit.resolveConflicts = (repoPath, files) => {\n  const toAdd = [];\n  const toRemove = [];\n  return Promise.all(\n    (files || []).map((file) => {\n      return fs\n        .access(path.join(repoPath, file))\n        .then(() => {\n          toAdd.push(file);\n        })\n        .catch(() => {\n          toRemove.push(file);\n        });\n    })\n  ).then(() => {\n    const addExec = toAdd.length > 0 ? git(['add', toAdd], repoPath) : null;\n    const removeExec = toRemove.length > 0 ? git(['rm', toRemove], repoPath) : null;\n    return Promise.all([addExec, removeExec]);\n  });\n};\n\ngit.stashExecuteAndPop = (commands, repoPath, allowError, outPipe, inPipe, timeout) => {\n  let hadLocalChanges = true;\n\n  return git(['stash'], repoPath)\n    .catch((err) => {\n      if (err.stderr.indexOf('You do not have the initial commit yet') != -1) {\n        hadLocalChanges = err.stderr.indexOf('You do not have the initial commit yet') == -1;\n      } else {\n        throw err;\n      }\n    })\n    .then((result) => {\n      if (!result || result.indexOf('No local changes to save') != -1) {\n        hadLocalChanges = false;\n      }\n      return git(commands, repoPath, allowError, outPipe, inPipe, timeout);\n    })\n    .then(() => {\n      return hadLocalChanges ? git(['stash', 'pop'], repoPath) : null;\n    });\n};\n\ngit.binaryFileContent = (repoPath, filename, version, outPipe) => {\n  return git(['show', `${version}:${filename}`], repoPath, null, outPipe);\n};\n\ngit.diffFile = (repoPath, filename, oldFilename, sha1, ignoreWhiteSpace) => {\n  if (sha1) {\n    return git(['rev-list', '--max-parents=0', sha1], repoPath).then((initialCommitSha1) => {\n      const prevSha1 =\n        sha1 == initialCommitSha1.trim()\n          ? sha1.length == 64\n            ? gitEmptyReproSha256\n            : gitEmptyReproSha1\n          : `${sha1}^`;\n      if (oldFilename && oldFilename !== filename) {\n        return git(\n          [\n            'diff',\n            ignoreWhiteSpace ? '-w' : '',\n            `${prevSha1}:${oldFilename.trim()}`,\n            `${sha1}:${filename.trim()}`,\n          ],\n          repoPath\n        );\n      } else {\n        return git(\n          ['diff', ignoreWhiteSpace ? '-w' : '', prevSha1, sha1, '--', filename.trim()],\n          repoPath\n        );\n      }\n    });\n  }\n\n  return git\n    .revParse(repoPath)\n    .then((revParse) => {\n      return revParse.type === 'bare' ? { files: {} } : git.status(repoPath);\n    }) // if bare do not call status\n    .then((status) => {\n      const file = status.files[filename];\n      if (!file) {\n        return fs\n          .access(path.join(repoPath, filename))\n          .then(() => {\n            return [];\n          })\n          .catch(() => {\n            throw { error: `No such file: ${filename}`, errorCode: 'no-such-file' };\n          });\n        // If the file is new or if it's a directory, i.e. a submodule\n      } else {\n        if (file && file.isNew) {\n          return git(\n            ['diff', '--no-index', isWindows ? 'NUL' : '/dev/null', filename.trim()],\n            repoPath,\n            true\n          );\n        } else if (file && file.renamed) {\n          return git(\n            ['diff', ignoreWhiteSpace ? '-w' : '', `HEAD:${oldFilename}`, filename.trim()],\n            repoPath\n          );\n        } else {\n          return git(\n            ['diff', ignoreWhiteSpace ? '-w' : '', 'HEAD', '--', filename.trim()],\n            repoPath\n          );\n        }\n      }\n    });\n};\n\ngit.getCurrentBranch = (repoPath) => {\n  return git(['branch'], repoPath)\n    .then(gitParser.parseGitBranches)\n    .then((branches) => {\n      const branch = branches.find((branch) => branch.current);\n      if (branch) {\n        return branch.name;\n      } else {\n        return '';\n      }\n    });\n};\n\ngit.discardAllChanges = (repoPath) => {\n  return git(['reset', '--hard', 'HEAD'], repoPath).then(() => {\n    return git(['clean', '-fd'], repoPath);\n  });\n};\n\ngit.discardChangesInFile = (repoPath, filename) => {\n  return git.status(repoPath, filename).then((status) => {\n    if (Object.keys(status.files).length == 0)\n      throw new Error(`No files in status in discard, filename: ${filename}`);\n    const fileStatus = status.files[Object.keys(status.files)[0]];\n    const fullPath = path.join(repoPath, filename);\n\n    if (fileStatus.staged) {\n      // if staged, just remove from git\n      return git(['rm', '-f', filename], repoPath);\n    } else if (fileStatus.isNew) {\n      // new file, junst unlink\n      return fs.unlink(fullPath).catch((err) => {\n        throw { command: 'unlink', error: err };\n      });\n    }\n\n    return fs\n      .stat(fullPath)\n      .then((stats) => stats.isDirectory())\n      .catch(() => false)\n      .then((isSubrepoChange) => {\n        if (isSubrepoChange) {\n          return git(['submodule', 'sync'], repoPath).then(() =>\n            git(['submodule', 'update', '--init', '-f', '--recursive', filename], repoPath)\n          );\n        } else {\n          return git(['checkout', 'HEAD', '--', filename], repoPath);\n        }\n      });\n  });\n};\n\ngit.applyPatchedDiff = (repoPath, patchedDiff) => {\n  if (patchedDiff) {\n    return git(['apply', '--cached'], repoPath, null, null, patchedDiff + '\\n\\n');\n  }\n};\n\ngit.commit = (repoPath, amend, emptyCommit, message, files) => {\n  return new Promise((resolve, reject) => {\n    if (message == undefined) {\n      reject({ error: 'Must specify commit message' });\n    }\n    if ((!Array.isArray(files) || files.length == 0) && !amend && !emptyCommit) {\n      reject({ error: 'Must specify files or amend to commit' });\n    }\n    resolve();\n  })\n    .then(() => {\n      return git.status(repoPath);\n    })\n    .then((status) => {\n      const toAdd = [];\n      const toRemove = [];\n      const promises = []; // promises that patches each files individually\n\n      for (const v in files) {\n        const file = files[v];\n        const fileStatus =\n          status.files[file.name] || status.files[path.relative(repoPath, file.name)];\n        if (!fileStatus) {\n          throw { error: `No such file in staging: ${file.name}` };\n        }\n\n        if (fileStatus.removed) {\n          toRemove.push(file.name.trim());\n        } else if (files[v].patchLineList) {\n          promises.push(\n            git(['diff', '--', file.name.trim()], repoPath)\n              .then(gitParser.parsePatchDiffResult.bind(null, file.patchLineList))\n              .then(git.applyPatchedDiff.bind(null, repoPath))\n          );\n        } else {\n          toAdd.push(file.name.trim());\n        }\n      }\n\n      promises.push(\n        Promise.resolve()\n          .then(() => {\n            if (toRemove.length > 0)\n              return git(\n                ['update-index', '--remove', '--stdin'],\n                repoPath,\n                null,\n                null,\n                toRemove.join('\\n')\n              );\n          })\n          .then(() => {\n            if (toAdd.length > 0)\n              return git(\n                ['update-index', '--add', '--stdin'],\n                repoPath,\n                null,\n                null,\n                toAdd.join('\\n')\n              );\n          })\n      );\n\n      return Promise.all(promises);\n    })\n    .then(() => {\n      const ammendFlag = amend ? '--amend' : '';\n      const allowedEmptyFlag = emptyCommit || amend ? '--allow-empty' : '';\n      const isGPGSign = config.isForceGPGSign ? '-S' : '';\n      return git(\n        ['commit', ammendFlag, allowedEmptyFlag, isGPGSign, '--file=-'],\n        repoPath,\n        null,\n        null,\n        message\n      );\n    })\n    .catch((err) => {\n      // ignore the case where nothing were added to be committed\n      if (!err.stdout || err.stdout.indexOf('Changes not staged for commit') === -1) {\n        throw err;\n      }\n    });\n};\n\ngit.revParse = (repoPath) => {\n  return git(['rev-parse', '--is-inside-work-tree', '--is-bare-repository'], repoPath)\n    .then((result) => {\n      const resultLines = result.split('\\n');\n      if (resultLines[1].indexOf('true') > -1) {\n        // bare repositories don't support `--show-toplevel` since git 2.25\n        return { type: 'bare', gitRootPath: repoPath };\n      }\n      return git(['rev-parse', '--show-toplevel'], repoPath).then((topLevel) => {\n        const rootPath = path.normalize(topLevel.trim() ? topLevel.trim() : repoPath);\n        if (resultLines[0].indexOf('true') > -1) {\n          return { type: 'inited', gitRootPath: rootPath };\n        }\n        return { type: 'uninited', gitRootPath: rootPath };\n      });\n    })\n    .catch(() => {\n      return { type: 'uninited', gitRootPath: path.normalize(repoPath) };\n    });\n};\n\ngit.log = (path, limit, skip, maxActiveBranchSearchIteration) => {\n  return git(\n    [\n      'log',\n      '--cc',\n      '--decorate=full',\n      '--show-signature',\n      '--date=default',\n      '--pretty=fuller',\n      '-z',\n      '--branches',\n      '--tags',\n      '--remotes',\n      '--parents',\n      '--no-notes',\n      '--numstat',\n      '--date-order',\n      `--max-count=${limit}`,\n      `--skip=${skip}`,\n    ],\n    path\n  )\n    .then(gitParser.parseGitLog)\n    .then((log) => {\n      log = log ? log : [];\n      if (maxActiveBranchSearchIteration > 0 && !log.isHeadExist && log.length > 0) {\n        return git\n          .log(\n            path,\n            config.numberOfNodesPerLoad + limit,\n            config.numberOfNodesPerLoad + skip,\n            maxActiveBranchSearchIteration - 1\n          )\n          .then((innerLog) => {\n            return {\n              limit: limit + (innerLog.isHeadExist ? 0 : config.numberOfNodesPerLoad),\n              skip: skip + (innerLog.isHeadExist ? 0 : config.numberOfNodesPerLoad),\n              nodes: log.concat(innerLog.nodes),\n              isHeadExist: innerLog.isHeadExist,\n            };\n          });\n      } else {\n        return { limit: limit, skip: skip, nodes: log, isHeadExist: log.isHeadExist };\n      }\n    });\n};\n\nmodule.exports = git;\n"
  },
  {
    "path": "source/server.js",
    "content": "const logger = require('./utils/logger');\nconst config = require('./config');\nconst BugTracker = require('./bugtracker');\nconst bugtracker = new BugTracker('server');\nconst express = require('express');\nconst gitApi = require('./git-api');\nconst sysinfo = require('./sysinfo');\nconst passport = require('passport');\nconst LocalStrategy = require('passport-local').Strategy;\nconst semver = require('semver');\nconst path = require('path');\nconst fs = require('fs').promises;\nconst signals = require('signals');\nconst os = require('os');\nconst cache = require('./utils/cache');\nconst UngitPlugin = require('./ungit-plugin');\nconst serveStatic = require('serve-static');\n\nprocess.on('uncaughtException', (err) => {\n  logger.error(err.stack ? err.stack.toString() : err.toString());\n  bugtracker.notify(err, 'ungit-launcher');\n  process.exit();\n});\n\nconst users = config.users;\nconfig.users = null; // So that we don't send the users to the client\n\nif (config.authentication) {\n  passport.serializeUser((username, done) => {\n    done(null, username);\n  });\n\n  passport.deserializeUser((username, done) => {\n    done(null, users[username] !== undefined ? username : null);\n  });\n\n  passport.use(\n    new LocalStrategy((username, password, done) => {\n      if (users[username] !== undefined && password === users[username]) done(null, username);\n      else done(null, false, { message: 'No such username/password' });\n    })\n  );\n}\n\nconst app = express();\nconst server = require('http').createServer(app);\n\ngitApi.pathPrefix = '/api';\n\napp.use((req, res, next) => {\n  const rootPath = config.rootPath;\n  if (req.url === rootPath) {\n    // always have a trailing slash\n    res.redirect(req.url + '/');\n    return;\n  }\n  if (req.url.indexOf(rootPath) === 0) {\n    req.url = req.url.substring(rootPath.length);\n    next();\n    return;\n  }\n  res.status(400).end();\n});\n\nif (config.logRESTRequests) {\n  app.use((req, res, next) => {\n    logger.info(req.method + ' ' + req.url);\n    next();\n  });\n}\n\nif (config.allowedIPs) {\n  app.use((req, res, next) => {\n    const ip =\n      req.ip ||\n      req.connection.remoteAddress ||\n      req.socket.remoteAddress ||\n      req.connection.socket.remoteAddress;\n    if (config.allowedIPs.indexOf(ip) >= 0) next();\n    else {\n      res\n        .status(403)\n        .send(\n          '<h3>This host is not authorized to connect</h3>' +\n            '<p>You are trying to connect to an Ungit instance from an unauthorized host.</p>'\n        );\n      logger.warn(`Host trying but not authorized to connect: ${ip}`);\n    }\n  });\n}\n\nconst noCache = (req, res, next) => {\n  res.set('Cache-Control', 'no-cache, no-store, must-revalidate');\n  res.set('Pragma', 'no-cache');\n  res.set('Expires', '0');\n  next();\n};\napp.use(noCache);\n\napp.use(require('body-parser').json());\n\nif (config.autoShutdownTimeout) {\n  let autoShutdownTimeout;\n  const refreshAutoShutdownTimeout = () => {\n    if (autoShutdownTimeout) clearTimeout(autoShutdownTimeout);\n    autoShutdownTimeout = setTimeout(() => {\n      logger.info(\n        `Shutting down ungit due to inactivity. (autoShutdownTimeout is set to ${config.autoShutdownTimeout} ms`\n      );\n      process.exit();\n    }, config.autoShutdownTimeout);\n  };\n  app.use((req, res, next) => {\n    refreshAutoShutdownTimeout();\n    next();\n  });\n  refreshAutoShutdownTimeout();\n}\n\nlet ensureAuthenticated = (req, res, next) => {\n  next();\n};\n\nif (config.authentication) {\n  const cookieParser = require('cookie-parser');\n  const session = require('express-session');\n  const MemoryStore = require('memorystore')(session);\n  app.use(cookieParser());\n  app.use(\n    session({\n      store: new MemoryStore({\n        checkPeriod: 86400000, // prune expired entries every 24h\n      }),\n      secret: 'ungit',\n      resave: true,\n      saveUninitialized: true,\n    })\n  );\n  app.use(passport.initialize());\n  app.use(passport.session());\n\n  app.post('/api/login', (req, res, next) => {\n    passport.authenticate('local', (err, user, info) => {\n      if (err) {\n        return next(err);\n      }\n      if (!user) {\n        res.status(401).json({ errorCode: 'authentication-failed', error: info.message });\n        return;\n      }\n      req.logIn(user, (err) => {\n        if (err) {\n          return next(err);\n        }\n        res.json({ ok: true });\n        return;\n      });\n    })(req, res, next);\n  });\n\n  app.get('/api/loggedin', (req, res) => {\n    if (req.isAuthenticated()) res.json({ loggedIn: true });\n    else res.json({ loggedIn: false });\n  });\n\n  app.get('/api/logout', (req, res) => {\n    req.logout();\n    res.json({ ok: true });\n  });\n\n  ensureAuthenticated = (req, res, next) => {\n    if (req.isAuthenticated()) {\n      return next();\n    }\n    res.status(401).json({\n      errorCode: 'authentication-required',\n      error: 'You have to authenticate to access this resource',\n    });\n  };\n}\n\nconst indexHtmlCacheKey = cache.registerFunc(() => {\n  return cache.resolveFunc(pluginsCacheKey).then((plugins) => {\n    return fs.readFile(__dirname + '/../public/index.html', { encoding: 'utf8' }).then((data) => {\n      return Promise.all(\n        Object.values(plugins).map((plugin) => {\n          return plugin.compile();\n        })\n      ).then((results) => {\n        data = data.replace('<!-- ungit-plugins-placeholder -->', results.join('\\n\\n'));\n        data = data.replace(/__ROOT_PATH__/g, config.rootPath);\n\n        return data;\n      });\n    });\n  });\n});\n\napp.get('/', (req, res) => {\n  if (config.dev) {\n    cache.invalidateFunc(pluginsCacheKey);\n    cache.invalidateFunc(indexHtmlCacheKey);\n  }\n  cache.resolveFunc(indexHtmlCacheKey).then((data) => {\n    res.end(data);\n  });\n});\n\napp.use(serveStatic(__dirname + '/../public'));\n\n// Socket-IO\nconst socketIO = require('socket.io');\nconst socketsById = {};\nlet socketIdCounter = 0;\nconst io = socketIO(server, {\n  path: config.rootPath + '/socket.io',\n  logger: {\n    debug: logger.debug.bind(logger),\n    info: logger.info.bind(logger),\n    error: logger.error.bind(logger),\n    warn: logger.warn.bind(logger),\n  },\n});\nio.on('connection', (socket) => {\n  const socketId = socketIdCounter++;\n  socketsById[socketId] = socket;\n  socket.socketId = socketId;\n  socket.emit('connected', { socketId: socketId });\n  socket.on('disconnect', () => delete socketsById[socketId]);\n});\n\nconst apiEnvironment = {\n  app: app,\n  server: server,\n  ensureAuthenticated: ensureAuthenticated,\n  config: config,\n  pathPrefix: gitApi.pathPrefix,\n  socketIO: io,\n  socketsById: socketsById,\n};\n\ngitApi.registerApi(apiEnvironment);\n\n// Init plugins\nconst loadPlugins = (plugins, pluginBasePath) => {\n  return fs.readdir(pluginBasePath).then((pluginDirs) => {\n    return Promise.all(\n      pluginDirs.map((pluginDir) => {\n        const pluginPath = path.join(pluginBasePath, pluginDir);\n        return fs\n          .access(path.join(pluginPath, 'ungit-plugin.json'))\n          .then(() => {\n            logger.info('Loading plugin: ' + pluginPath);\n            const plugin = new UngitPlugin({\n              dir: pluginDir,\n              httpBasePath: 'plugins/' + pluginDir,\n              path: pluginPath,\n            });\n            if (plugin.manifest.disabled || plugin.config.disabled) {\n              logger.info('Plugin disabled: ' + pluginDir);\n              return;\n            }\n            plugin.init(apiEnvironment);\n            plugins.push(plugin);\n            logger.info('Plugin loaded: ' + pluginDir);\n          })\n          .catch(() => {\n            // Skip direcories that don't contain an \"ungit-plugin.json\".\n          });\n      })\n    );\n  });\n};\nconst pluginsCacheKey = cache.registerFunc(() => {\n  const plugins = [];\n  return loadPlugins(plugins, path.join(__dirname, '..', 'components'))\n    .then(() => {\n      return fs\n        .access(config.pluginDirectory)\n        .then(() => loadPlugins(plugins, config.pluginDirectory))\n        .catch(() => {\n          /* ignore */\n        });\n    })\n    .then(() => plugins);\n});\n\napp.get('/serverdata.js', (req, res) => {\n  const text =\n    `ungit.config = ${JSON.stringify(config)};\\n` +\n    `ungit.userHash = \"${sysinfo.getUserHash()}\";\\n` +\n    `ungit.version = \"${config.ungitDevVersion}\";\\n` +\n    `ungit.platform = \"${os.platform()}\";\\n` +\n    `ungit.pluginApiVersion = \"${require('../package.json').ungitPluginApiVersion}\";\\n`;\n  res.set('Content-Type', 'application/javascript');\n  res.send(text);\n});\n\napp.get('/api/latestversion', (req, res) => {\n  sysinfo\n    .getUngitLatestVersion()\n    .then((latestVersion) => {\n      if (!semver.valid(config.ungitDevVersion)) {\n        res.json({\n          latestVersion: latestVersion,\n          currentVersion: config.ungitDevVersion,\n          outdated: false,\n        });\n      } else {\n        // We only want to show the \"new version\" banner if the major/minor version was bumped\n        const latestSansPatch = semver(latestVersion);\n        latestSansPatch.patch = 0;\n        const currentSansPatch = semver(config.ungitDevVersion);\n        currentSansPatch.patch = 0;\n        res.json({\n          latestVersion: latestVersion,\n          currentVersion: config.ungitDevVersion,\n          outdated: semver.gt(latestSansPatch, currentSansPatch),\n        });\n      }\n    })\n    .catch(() => {\n      res.json({\n        latestVersion: config.ungitDevVersion,\n        currentVersion: config.ungitDevVersion,\n        outdated: false,\n      });\n    });\n});\n\napp.get('/api/ping', (req, res) => res.json({}));\n\napp.get('/api/gitversion', (req, res) => {\n  res.json(sysinfo.getGitVersionInfo());\n});\n\nconst userConfigPath = path.join(config.homedir, '.ungitrc');\nconst readUserConfig = () => {\n  return fs\n    .access(userConfigPath)\n    .then(() => {\n      return fs\n        .readFile(userConfigPath, { encoding: 'utf8' })\n        .then((content) => {\n          return JSON.parse(content);\n        })\n        .catch((err) => {\n          logger.error(`Stop at reading ~/.ungitrc because ${err}`);\n          process.exit(1);\n        });\n    })\n    .catch(() => {\n      return {};\n    });\n};\nconst writeUserConfig = (configContent) => {\n  return fs.writeFile(userConfigPath, JSON.stringify(configContent, undefined, 2));\n};\n\napp.get('/api/userconfig', ensureAuthenticated, (req, res) => {\n  readUserConfig()\n    .then((userConfig) => {\n      res.json(userConfig);\n    })\n    .catch((err) => {\n      res.status(400).json(err);\n    });\n});\napp.post('/api/userconfig', ensureAuthenticated, (req, res) => {\n  writeUserConfig(req.body)\n    .then(() => {\n      res.json({});\n    })\n    .catch((err) => {\n      res.status(400).json(err);\n    });\n});\n\napp.get('/api/fs/exists', ensureAuthenticated, (req, res) => {\n  fs.access(req.query['path'])\n    .then(() => {\n      res.json(true);\n    })\n    .catch(() => {\n      res.json(false);\n    });\n});\n\napp.get('/api/fs/listDirectories', ensureAuthenticated, (req, res) => {\n  const dir = path.resolve(req.query.term.trim()).replace('/~', '');\n\n  fs.readdir(dir, { withFileTypes: true })\n    .then((files) => {\n      const dirs = [];\n      files.forEach((file) => {\n        if (file.isDirectory()) {\n          dirs.push(path.join(dir, file.name));\n        }\n      });\n      return dirs;\n    })\n    .then((filteredFiles) => {\n      filteredFiles.unshift(dir);\n      res.json(filteredFiles);\n    })\n    .catch((err) => res.status(400).json(err));\n});\n\n// Error handling\n// eslint-disable-next-line no-unused-vars\napp.use((err, req, res, next) => {\n  bugtracker.notify(err, 'ungit-node');\n  logger.error(err.stack);\n  res.status(500).send({ error: err.message, errorType: err.name, stack: err.stack });\n});\n\nexports.started = new signals.Signal();\n\nserver.listen({ port: config.port, host: config.ungitBindIp }, () => {\n  logger.info('Listening on port ' + config.port);\n  console.log('## Ungit started ##'); // Consumed by bin/ungit to figure out when the app is started\n  exports.started.dispatch();\n});\n"
  },
  {
    "path": "source/sysinfo.js",
    "content": "const getMac = require('getmac').default;\nconst md5 = require('blueimp-md5');\nconst semver = require('semver');\nconst logger = require('./utils/logger');\nconst config = require('./config');\n\nexports.getUngitLatestVersion = () => {\n  return import('latest-version').then((latestVersion) => {\n    return latestVersion.default('ungit');\n  });\n};\n\nexports.getUserHash = () => {\n  let addr;\n  try {\n    addr = getMac();\n  } catch (err) {\n    logger.error('attempt to get mac addr failed, using fake mac.', err);\n    addr = 'abcde';\n  }\n  return md5(addr);\n};\n\nexports.getGitVersionInfo = () => {\n  const result = {\n    requiredVersion: '>=1.8.x',\n    version: 'unkown',\n    satisfied: false,\n  };\n\n  if (!config.gitVersion) {\n    result.error = `Failed to parse git version number. Note that Ungit requires git version ${result.requiredVersion}`;\n  } else {\n    result.version = config.gitVersion;\n    result.satisfied = semver.satisfies(result.version, result.requiredVersion);\n    if (!result.satisfied) {\n      result.error = `Ungit requires git version ${result.requiredVersion}, you are currently running ${result.version}`;\n    }\n  }\n\n  return result;\n};\n"
  },
  {
    "path": "source/ungit-plugin.js",
    "content": "const fsSync = require('fs');\nconst fs = fsSync.promises;\nconst path = require('path');\nconst express = require('express');\nconst logger = require('./utils/logger');\nconst config = require('./config');\n\nconst assureArray = (obj) => {\n  return Array.isArray(obj) ? obj : [obj];\n};\n\nclass UngitPlugin {\n  constructor(args) {\n    this.dir = args.dir;\n    this.path = args.path;\n    this.httpBasePath = args.httpBasePath;\n    this.manifest = JSON.parse(\n      fsSync.readFileSync(path.join(this.path, 'ungit-plugin.json'), { encoding: 'utf8' })\n    );\n    this.name = this.manifest.name || this.dir;\n    this.config = config.pluginConfigs[this.name] || {};\n  }\n\n  init(env) {\n    if (this.manifest.server) {\n      const serverScript = require(path.join(this.path, this.manifest.server));\n      serverScript.install({\n        app: env.app,\n        httpServer: env.httpServer,\n        ensureAuthenticated: env.ensureAuthenticated,\n        ensurePathExists: env.ensurePathExists,\n        git: require('./git-promise'),\n        config: env.config,\n        socketIO: env.socketIO,\n        socketsById: env.socketsById,\n        pluginConfig: this.config,\n        httpPath: `${env.pathPrefix}/plugins/${this.name}`,\n        pluginApiVersion: require('../package.json').ungitPluginApiVersion,\n      });\n    }\n    env.app.use(`/plugins/${this.name}`, express.static(this.path));\n  }\n\n  compile() {\n    logger.info(`Compiling plugin ${this.path}`);\n    const exports = this.manifest.exports || {};\n\n    return Promise.resolve()\n      .then(() => {\n        if (exports.raw) {\n          return Promise.all(\n            assureArray(exports.raw).map((rawSource) => {\n              return fs\n                .readFile(path.join(this.path, rawSource), { encoding: 'utf8' })\n                .then((text) => {\n                  return text + '\\n';\n                });\n            })\n          ).then((result) => {\n            return result.join('\\n');\n          });\n        } else {\n          return '';\n        }\n      })\n      .then((result) => {\n        if (exports.javascript) {\n          return (\n            result +\n            assureArray(exports.javascript)\n              .map((filename) => {\n                return `<script type=\"text/javascript\" src=\"${config.rootPath}/plugins/${this.name}/${filename}\"></script>`;\n              })\n              .join('\\n')\n          );\n        } else {\n          return result;\n        }\n      })\n      .then((result) => {\n        if (exports.knockoutTemplates) {\n          return Promise.all(\n            Object.keys(exports.knockoutTemplates).map((templateName) => {\n              return fs\n                .readFile(path.join(this.path, exports.knockoutTemplates[templateName]), {\n                  encoding: 'utf8',\n                })\n                .then((text) => {\n                  return `<script type=\"text/html\" id=\"${templateName}\">\\n${text}\\n</script>`;\n                });\n            })\n          ).then((templates) => {\n            return result + templates.join('\\n');\n          });\n        } else {\n          return result;\n        }\n      })\n      .then((result) => {\n        if (exports.css) {\n          return (\n            result +\n            assureArray(exports.css)\n              .map((cssSource) => {\n                return `<link rel=\"stylesheet\" type=\"text/css\" href=\"${config.rootPath}/plugins/${this.name}/${cssSource}\" />`;\n              })\n              .join('\\n')\n          );\n        } else {\n          return result;\n        }\n      })\n      .then((result) => {\n        return `<!-- Component: ${this.name} -->\\n${result}`;\n      });\n  }\n}\nmodule.exports = UngitPlugin;\n"
  },
  {
    "path": "source/utils/cache.js",
    "content": "const NodeCache = require('node-cache');\nconst md5 = require('blueimp-md5');\nconst funcMap = {}; // Will there ever be a use case where this is a cache with TTL? func registration with TTL?\n\nclass OurCache extends NodeCache {\n  constructor() {\n    super({ stdTTL: 0 });\n  }\n\n  /**\n   * Get cached result associated with the key or execute a function to get the result.\n   *\n   * @param {string} [key]  - A key associated with a function to be executed.\n   * @returns {Promise} - Promise either resolved with cached result of the function or rejected\n   *                    with function not found.\n   */\n  resolveFunc(key) {\n    let result = this.get(key);\n    if (result !== undefined) {\n      return Promise.resolve(result);\n    }\n    result = funcMap[key];\n    if (result === undefined) {\n      return Promise.reject(new Error(`Cache entry ${key} not found`));\n    }\n    try {\n      result = result.func();\n    } catch (err) {\n      return Promise.reject(err);\n    }\n    return Promise.resolve(result) // func is found, resolve, set with TTL and return result\n      .then((r) => {\n        this.set(key, r, funcMap[key].ttl);\n        return r;\n      });\n  }\n\n  /**\n   * Register a function to cache it's result. If same key exists, key is deregistered and\n   * registered again.\n   *\n   * @param {function} [func]           - Function to be executed to get the result.\n   * @param {string}   [key=md5(func)]  - Key to retrieve cached function result. Default is\n   *                                    `md5(func)`.\n   * @param {number}   [ttl=0]          - Ttl in seconds to be used for the cached result of\n   *                                    function. Default is `0`.\n   * @returns {string} - Key to retrieve cached function result.\n   */\n  registerFunc(func, key, ttl) {\n    if (typeof func !== 'function') {\n      throw new Error('no function was passed in.');\n    }\n\n    key = key || md5(func);\n    ttl = ttl || this.options.stdTTL;\n\n    if (isNaN(ttl) || ttl < 0) {\n      throw new Error('ttl value is not valid.');\n    }\n\n    if (funcMap[key]) {\n      this.deregisterFunc(key);\n    }\n\n    funcMap[key] = {\n      func: func,\n      ttl: ttl,\n    };\n\n    return key;\n  }\n\n  /**\n   * Immediately invalidate cached function result despite ttl value.\n   *\n   * @param {string} [key]  - A key associated with a function to be executed.\n   */\n  invalidateFunc(key) {\n    this.del(key);\n  }\n\n  /**\n   * Remove function registration and invalidate it's cached value.\n   *\n   * @param {string} [key]  - A key associated with a function to be executed.\n   */\n  deregisterFunc(key) {\n    this.invalidateFunc(key);\n    delete funcMap[key];\n  }\n}\n\nmodule.exports = new OurCache();\n"
  },
  {
    "path": "source/utils/file-type.js",
    "content": "'use strict';\n\nconst path = require('path');\nconst imageFileExtensions = ['.PNG', '.JPG', '.BMP', '.GIF', '.JPEG'];\n\nmodule.exports = (fileName) =>\n  imageFileExtensions.indexOf(path.extname(fileName).toUpperCase()) > -1 ? 'image' : 'text';\n"
  },
  {
    "path": "source/utils/logger.js",
    "content": "const winston = require('winston');\nconst path = require('path');\nconst config = require('../config');\n\nconst transports = [new winston.transports.Console()];\nif (config.logDirectory) {\n  console.log('Added log file at ' + config.logLevel);\n  transports.push(\n    new winston.transports.File({\n      filename: path.join(config.logDirectory, 'server.log'),\n      maxsize: 100 * 1024,\n      maxFiles: 2,\n      format: winston.format.combine(winston.format.timestamp(), winston.format.json()),\n    })\n  );\n}\n\nconsole.log('Setting log level to ' + config.logLevel);\nconst logger = winston.createLogger({\n  level: config.logLevel || 'error',\n  format: winston.format.combine(\n    winston.format.timestamp(),\n    winston.format.colorize(),\n    winston.format.printf((info) => {\n      const splat = info[Symbol.for('splat')];\n      if (splat) {\n        const splatStr = splat.map((arg) => JSON.stringify(arg)).join('\\n');\n        return `${info.timestamp} - ${info.level}: ${info.message} ${splatStr}`;\n      }\n      return `${info.timestamp} - ${info.level}: ${info.message}`;\n    })\n  ),\n  transports: transports,\n});\n\nmodule.exports = logger;\n"
  },
  {
    "path": "test/common-es6.js",
    "content": "const expect = require('expect.js');\nconst path = require('path');\nconst restGit = require('../source/git-api');\n\nexports.makeRequest = (method, req, path, payload) => {\n  let r;\n  if (method === 'GET' || method === 'PNG') {\n    r = req.get(`${restGit.pathPrefix}${path}`);\n  } else if (method === 'POST') {\n    r = req.post(`${restGit.pathPrefix}${path}`);\n  } else if (method === 'DELETE') {\n    r = req.del(`${restGit.pathPrefix}${path}`);\n  } else if (method === 'PUT') {\n    r = req.put(`${restGit.pathPrefix}${path}`);\n  } else {\n    throw new Error({ message: `invalid method of ${method}` });\n  }\n\n  if (payload) {\n    payload.socketId = 'ignore';\n    if (method === 'POST' || method === 'PUT') {\n      r.send(payload);\n    } else {\n      r.query(payload);\n    }\n  }\n\n  return new Promise((resolve, reject) => {\n    r.expect('Content-Type', method === 'PNG' ? 'image/png' : /json/).end((err, res) => {\n      if (err) {\n        console.log(`failed path: ${path}`);\n        console.dir(err);\n        console.dir(res ? res.body : '');\n        reject(err);\n      } else {\n        let data = (res || {}).body;\n        try {\n          data = JSON.parse(data);\n        } catch {\n          /* Ignore error */\n        }\n        resolve(data);\n      }\n    });\n  });\n};\n\nexports.get = this.makeRequest.bind(this, 'GET');\nexports.getPng = this.makeRequest.bind(this, 'PNG');\nexports.post = this.makeRequest.bind(this, 'POST');\nexports.delete = this.makeRequest.bind(this, 'DELETE');\nexports.put = this.makeRequest.bind(this, 'PUT');\n\nexports.initRepo = async (req, config) => {\n  config = config || {};\n  const res = await this.post(req, '/testing/createtempdir', config.path);\n  expect(res.path).to.be.ok();\n  await this.post(req, '/init', { path: res.path, bare: !!config.bare });\n  return res.path;\n};\n\nexports.createSmallRepo = (req) => {\n  return this.initRepo(req).then((dir) => {\n    const testFile = 'smalltestfile.txt';\n    return this.post(req, '/testing/createfile', { file: path.join(dir, testFile) })\n      .then(() =>\n        this.post(req, '/commit', { path: dir, message: 'Init', files: [{ name: testFile }] })\n      )\n      .then(() => dir);\n  });\n};\n"
  },
  {
    "path": "test/spec.address-parser.js",
    "content": "const expect = require('expect.js');\nconst addressParser = require('../source/address-parser');\n\ndescribe('git-parser addresses', () => {\n  it('parseAddress ssh://some.address.com/my/awesome/project', () => {\n    const addr = 'ssh://some.address.com/my/awesome/project';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.port).to.be(undefined);\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress ssh://some.address.com:8080/my/awesome/project', () => {\n    const addr = 'ssh://some.address.com:8080/my/awesome/project';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.port).to.be('8080');\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress some.address.com:my/awesome/project.git', () => {\n    const addr = 'some.address.com:my/awesome/project.git';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress someuser@some.address.com:my/awesome/project.git', () => {\n    const addr = 'someuser@some.address.com:my/awesome/project.git';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.username).to.be('someuser');\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress some.address.com:my/awesome/project', () => {\n    const addr = 'some.address.com:my/awesome/project';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress someuser@some.address.com:my/awesome/project', () => {\n    const addr = 'someuser@some.address.com:my/awesome/project';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.username).to.be('someuser');\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress https://some.address.com/my/awesome/project', () => {\n    const addr = 'https://some.address.com/my/awesome/project';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress https://some.address.com/my/awesome/project.git', () => {\n    const addr = 'https://some.address.com/my/awesome/project.git';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('some.address.com');\n    expect(parsed.project).to.be('my/awesome/project');\n    expect(parsed.shortProject).to.be('project');\n  });\n\n  it('parseAddress /home/username/somerepo', () => {\n    const addr = '/home/username/somerepo';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('localhost');\n    expect(parsed.project).to.be('somerepo');\n    expect(parsed.shortProject).to.be('somerepo');\n  });\n\n  it('parseAddress ~/something/somerepo', () => {\n    const addr = '~/something/somerepo';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('localhost');\n    expect(parsed.project).to.be('somerepo');\n    expect(parsed.shortProject).to.be('somerepo');\n  });\n\n  it('parseAddress C:\\\\something\\\\somerepo', () => {\n    const addr = 'C:\\\\something\\\\somerepo';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('localhost');\n    expect(parsed.project).to.be('somerepo');\n    expect(parsed.shortProject).to.be('somerepo');\n  });\n\n  it('parseAddress C:\\\\somerepo', () => {\n    const addr = 'C:\\\\somerepo';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('localhost');\n    expect(parsed.project).to.be('somerepo');\n    expect(parsed.shortProject).to.be('somerepo');\n  });\n\n  it('parseAddress C:\\\\something\\\\somerepo\\\\', () => {\n    const addr = 'C:\\\\something\\\\somerepo\\\\';\n    const parsed = addressParser.parseAddress(addr);\n    expect(parsed.host).to.be('localhost');\n    expect(parsed.project).to.be('somerepo');\n    expect(parsed.shortProject).to.be('somerepo');\n  });\n});\n"
  },
  {
    "path": "test/spec.cache.js",
    "content": "const expect = require('expect.js');\nconst cache = require('../source/utils/cache');\n\ndescribe('cache', () => {\n  it('should be invokable several times', () => {\n    let i = 0;\n    const key = cache.registerFunc(() => i++);\n\n    return cache\n      .resolveFunc(key)\n      .then((val) => {\n        expect(val).to.be(0);\n      })\n      .then(() => cache.resolveFunc(key))\n      .then((val) => expect(val).to.be(0));\n  });\n\n  it('should work when failing sync', () => {\n    const errorMsg = 'A nasty error...';\n    const key = cache.registerFunc(() => {\n      throw new Error(errorMsg);\n    });\n\n    return cache\n      .resolveFunc(key)\n      .then(() => {\n        throw new Error('should have thrown exception!');\n      })\n      .catch((e) => {\n        if (e.message !== errorMsg) throw new Error('error message does not match!');\n      });\n  });\n\n  it('should work when failing async', () => {\n    const errorMsg = 'A nasty error...';\n    const key = cache.registerFunc(() => Promise.reject(new Error(errorMsg)));\n\n    return cache\n      .resolveFunc(key)\n      .then(() => {\n        throw new Error('should have thrown exception!');\n      })\n      .catch((e) => {\n        if (e.message !== errorMsg) throw new Error('error message does not match!');\n      });\n  });\n\n  it('should be possible to invalidate cache', () => {\n    let i = 0;\n    const key = cache.registerFunc(() => i++);\n\n    return cache\n      .resolveFunc(key)\n      .then((val) => {\n        expect(val).to.be(0);\n      })\n      .then(() => {\n        cache.invalidateFunc(key);\n        return cache.resolveFunc(key);\n      })\n      .then((val) => {\n        expect(val).to.be(1);\n      });\n  });\n\n  it('creating a same function with different keys', () => {\n    let i = 0;\n    const key1 = 'func1';\n    const key2 = 'func2';\n    const func = () => i++;\n    cache.registerFunc(func, key1);\n    cache.registerFunc(func, key2);\n\n    return cache\n      .resolveFunc(key1)\n      .then((val) => {\n        expect(val).to.be(0);\n      })\n      .then(() => cache.resolveFunc(key1))\n      .then((val) => {\n        expect(val).to.be(0);\n      })\n      .then(() => cache.resolveFunc(key2))\n      .then((val) => {\n        expect(val).to.be(1);\n      })\n      .then(() => {\n        cache.invalidateFunc(key1);\n        return cache.resolveFunc(key1);\n      })\n      .then((val) => {\n        expect(val).to.be(2);\n      })\n      .then(() => cache.resolveFunc(key2))\n      .then((val) => {\n        expect(val).to.be(1);\n      });\n  });\n\n  it('Testing ttl', function () {\n    let i = 0;\n    const func = () => i++;\n    const key = cache.registerFunc(func, null, 1);\n    this.timeout(3000);\n\n    return cache\n      .resolveFunc(key)\n      .then((val) => {\n        expect(val).to.be(0);\n      })\n      .then(() => new Promise((resolve) => setTimeout(resolve, 500)))\n      .then(() => {\n        return cache.resolveFunc(key);\n      })\n      .then((val) => {\n        expect(val).to.be(0);\n      })\n      .then(() => new Promise((resolve) => setTimeout(resolve, 1000)))\n      .then(() => {\n        return cache.resolveFunc(key);\n      })\n      .then((val) => {\n        expect(val).to.be(1);\n      })\n      .then(() => new Promise((resolve) => setTimeout(resolve, 500)))\n      .then(() => cache.resolveFunc(key))\n      .then((val) => {\n        expect(val).to.be(1);\n      });\n  });\n});\n"
  },
  {
    "path": "test/spec.credentials-helper.js",
    "content": "const expect = require('expect.js');\nconst child_process = require('child_process');\nconst http = require('http');\nconst config = require('../source/config');\n\ndescribe('credentials-helper', () => {\n  it('should be invokable', (done) => {\n    const socketId = Math.floor(Math.random() * 1000);\n    const remote = 'origin';\n    const payload = { username: 'testuser', password: 'testpassword' };\n    const server = http.createServer((req, res) => {\n      try {\n        const reqUrl = new URL(req.url, `http://${req.headers.host}`);\n        expect(reqUrl.pathname).to.be('/api/credentials');\n        expect(reqUrl.searchParams.get('remote')).to.be(`${remote}`);\n        expect(reqUrl.searchParams.get('socketId')).to.be(`${socketId}`);\n\n        res.writeHead(200, { 'Content-Type': 'application/json' });\n        res.end(JSON.stringify(payload));\n      } finally {\n        if (!res.writableFinished) {\n          res.statusCode = 500;\n          res.end();\n        }\n      }\n    });\n\n    server.listen({ port: config.port }, () => {\n      const command = `node bin/credentials-helper ${socketId} ${config.port} ${remote} get`;\n      child_process.exec(command, (err, stdout) => {\n        server.close();\n        expect(err).to.not.be.ok();\n        const ss = stdout.split('\\n');\n        expect(ss[0]).to.be(`username=${payload.username}`);\n        expect(ss[1]).to.be(`password=${payload.password}`);\n        done();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "test/spec.file-type.js",
    "content": "const fileType = require('../source/utils/file-type.js');\nconst expect = require('expect.js');\n\ndescribe('file type', () => {\n  it('should be able to detrmine image file vs text files.', () => {\n    expect(fileType('example.txt')).to.be('text');\n    expect(fileType('example')).to.be('text');\n    expect(fileType('example.aBc')).to.be('text');\n    expect(fileType('examplepng.jpg.er')).to.be('text');\n\n    expect(fileType('example.png')).to.be('image');\n    expect(fileType('example.jpg')).to.be('image');\n    expect(fileType('example.bmp')).to.be('image');\n    expect(fileType('example.gIf')).to.be('image');\n    expect(fileType('example.JPEG')).to.be('image');\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.branching.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nlet testDir;\nlet gitConfig;\n\nconst req = request(app);\n\ndescribe('git-api branching', function () {\n  before(() => {\n    return common\n      .initRepo(req)\n      .then((res) => {\n        testDir = res;\n      })\n      .then(() => common.get(req, '/gitconfig', { path: testDir }))\n      .then((res) => {\n        gitConfig = res;\n      });\n  });\n\n  after(() => common.post(req, '/testing/cleanup'));\n\n  const commitMessage = 'Commit 1';\n  const testFile1 = 'testfile1.txt';\n\n  it('should be possible to commit to master', () => {\n    return common\n      .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n      .then(() =>\n        common.post(req, '/commit', {\n          path: testDir,\n          message: commitMessage,\n          files: [{ name: testFile1 }],\n        })\n      );\n  });\n\n  it('listing branches should work', () => {\n    return common.get(req, '/branches', { path: testDir }).then((res) => {\n      expect(res.length).to.be(1);\n      expect(res[0].name).to.be('master');\n      expect(res[0].current).to.be(true);\n    });\n  });\n\n  const testBranch = 'testBranch';\n\n  it('creating a branch should work', () => {\n    return common.post(req, '/branches', { path: testDir, name: testBranch, startPoint: 'master' });\n  });\n\n  it('listing branches should show the new branch', () => {\n    return common.get(req, '/branches', { path: testDir }).then((res) => {\n      expect(res.length).to.be(2);\n      expect(res[0].name).to.be('master');\n      expect(res[0].current).to.be(true);\n      expect(res[1].name).to.be(testBranch);\n      expect(res[1].current).to.be(undefined);\n    });\n  });\n\n  it('should be possible to switch to a branch', () => {\n    return common.post(req, '/checkout', { path: testDir, name: testBranch });\n  });\n\n  it('listing branches should show the new branch as current', () => {\n    return common.get(req, '/branches', { path: testDir }).then((res) => {\n      expect(res.length).to.be(2);\n      expect(res[0].name).to.be('master');\n      expect(res[0].current).to.be(undefined);\n      expect(res[1].name).to.be(testBranch);\n      expect(res[1].current).to.be(true);\n    });\n  });\n\n  it('get branch should show the new branch as current', () => {\n    return common\n      .get(req, '/checkout', { path: testDir })\n      .then((res) => expect(res).to.be(testBranch));\n  });\n\n  const commitMessage3 = 'Commit 3';\n  const testFile2 = 'testfile2.txt';\n\n  it('should be possible to commit to the branch', () => {\n    return common\n      .post(req, '/testing/createfile', { file: path.join(testDir, testFile2) })\n      .then(() =>\n        common.post(req, '/commit', {\n          path: testDir,\n          message: commitMessage3,\n          files: [{ name: testFile2 }],\n        })\n      );\n  });\n\n  it('log should show both branches and all commits', () => {\n    return common.get(req, '/gitlog', { path: testDir }).then((res) => {\n      expect(res.skip).to.be(0);\n      expect(res.limit).to.be(25);\n\n      const nodes = res.nodes;\n      expect(nodes).to.be.a('array');\n      expect(nodes.length).to.be(2);\n      const objs = {};\n      nodes.forEach((obj) => {\n        obj.refs.sort();\n        objs[obj.refs[0]] = obj;\n      });\n      const master = objs['refs/heads/master'];\n      const HEAD = objs['HEAD'];\n      expect(master.message.indexOf(commitMessage)).to.be(0);\n      expect(master.authorDate).to.be.a('string');\n      expect(master.authorName).to.be(gitConfig['user.name']);\n      expect(master.authorEmail).to.be(gitConfig['user.email']);\n      expect(master.commitDate).to.be.a('string');\n      expect(master.committerName).to.be(gitConfig['user.name']);\n      expect(master.committerEmail).to.be(gitConfig['user.email']);\n      expect(master.refs).to.eql(['refs/heads/master']);\n      expect(master.parents).to.eql([]);\n      expect(master.sha1).to.be.ok();\n\n      expect(HEAD.message.indexOf(commitMessage3)).to.be(0);\n      expect(HEAD.authorDate).to.be.a('string');\n      expect(HEAD.authorName).to.be(gitConfig['user.name']);\n      expect(HEAD.authorEmail).to.be(gitConfig['user.email']);\n      expect(HEAD.commitDate).to.be.a('string');\n      expect(HEAD.committerName).to.be(gitConfig['user.name']);\n      expect(HEAD.committerEmail).to.be(gitConfig['user.email']);\n      expect(HEAD.refs).to.eql(['HEAD', `refs/heads/${testBranch}`]);\n      expect(HEAD.parents).to.eql([master.sha1]);\n      expect(HEAD.sha1).to.be.ok();\n    });\n  });\n\n  it('should be possible to modify some local file', () => {\n    return common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) });\n  });\n\n  it('should be possible to checkout another branch with local modifications', () => {\n    return common.post(req, '/checkout', { path: testDir, name: 'master' });\n  });\n\n  it('status should list the changed file', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile1]).to.eql({\n        displayName: testFile1,\n        fileName: testFile1,\n        oldFileName: testFile1,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '1',\n        deletions: '1',\n      });\n    });\n  });\n\n  it('should be possible to create a tag', () => {\n    return common.post(req, '/tags', { path: testDir, name: 'v1.0' });\n  });\n\n  it('should be possible to list tag', () => {\n    return common.get(req, '/tags', { path: testDir }).then((res) => expect(res.length).to.be(1));\n  });\n\n  it('should be possible to delete a tag', () => {\n    return common.delete(req, '/tags', { path: testDir, name: 'v1.0' });\n  });\n\n  it('tag should be removed', () => {\n    return common.get(req, '/tags', { path: testDir }).then((res) => expect(res.length).to.be(0));\n  });\n\n  it('should be possible to delete a branch', () => {\n    return common.delete(req, '/branches', { path: testDir, name: testBranch });\n  });\n\n  it('branch should be removed', () => {\n    return common\n      .get(req, '/branches', { path: testDir })\n      .then((res) => expect(res.length).to.be(1));\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.conflict-no-auto-stash.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\nconst req = request(app);\n\nrestGit.registerApi({ app: app, config: { dev: true, autoStashAndPop: false } });\n\nlet testDir;\n\ndescribe('git-api conflict checkout no auto stash', function () {\n  const testBranch = 'testBranch';\n  const testFile1 = 'testfile1.txt';\n\n  before(() => {\n    return common.initRepo(req).then((dir) => {\n      testDir = dir;\n      return common\n        .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: 'a',\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() =>\n          common.post(req, '/branches', { path: testDir, name: testBranch, startPoint: 'master' })\n        )\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: 'b',\n            files: [{ name: testFile1 }],\n          })\n        );\n    });\n  });\n\n  after(() => {\n    return common.post(req, '/testing/cleanup');\n  });\n\n  it('should be possible to make some changes', () => {\n    return common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) });\n  });\n\n  it('should not be possible to checkout with local files that will conflict', () => {\n    return common\n      .post(req, `${restGit.pathPrefix}/checkout`, { path: testDir, name: testBranch })\n      .then((gitErr) => expect(gitErr.errorCode).to.be('local-changes-would-be-overwritten'));\n  });\n\n  it('checkout should say we are still on master', () => {\n    return common\n      .get(req, '/checkout', { path: testDir })\n      .then((res) => expect(res).to.be('master'));\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.conflict.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true, autoStashAndPop: true } });\n\nlet testDir;\n\nconst req = request(app);\n\ndescribe('git-api conflict rebase', function () {\n  const commitMessage = 'Commit 1';\n  const testFile1 = 'testfile1.txt';\n  const testBranch = 'testBranch';\n\n  before(() => {\n    return common.initRepo(req).then((dir) => {\n      testDir = dir;\n\n      return common\n        .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: commitMessage,\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() =>\n          common.post(req, '/branches', { path: testDir, name: testBranch, startPoint: 'master' })\n        )\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: commitMessage,\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() => common.post(req, '/checkout', { path: testDir, name: testBranch }))\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: commitMessage,\n            files: [{ name: testFile1 }],\n          })\n        );\n    });\n  });\n\n  it('should be possible to rebase on master', (done) => {\n    req\n      .post(`${restGit.pathPrefix}/rebase`)\n      .send({ path: testDir, onto: 'master' })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end((err, res) => {\n        expect(res.body.errorCode).to.be('merge-failed');\n        done();\n      });\n  });\n\n  it('status should list files in conflict', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(res.inRebase).to.be(true);\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile1]).to.eql({\n        displayName: testFile1,\n        fileName: testFile1,\n        oldFileName: testFile1,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: true,\n        renamed: false,\n        type: 'text',\n        additions: '4',\n        deletions: '0',\n      });\n    });\n  });\n\n  it('should be possible fix the conflict', () => {\n    return common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) });\n  });\n\n  it('should be possible to resolve', () => {\n    return common.post(req, '/resolveconflicts', { path: testDir, files: [testFile1] });\n  });\n\n  it('should be possible continue the rebase', () => {\n    return common.post(req, '/rebase/continue', { path: testDir });\n  });\n});\n\ndescribe('git-api conflict checkout', function () {\n  const testBranch = 'testBranch';\n  const testFile1 = 'testfile1.txt';\n\n  before(() => {\n    return common.initRepo(req).then((dir) => {\n      testDir = dir;\n      return common\n        .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: 'a',\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() =>\n          common.post(req, '/branches', { path: testDir, name: testBranch, startPoint: 'master' })\n        )\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: 'b',\n            files: [{ name: testFile1 }],\n          })\n        );\n    });\n  });\n\n  it('should be possible to make some changes', () => {\n    return common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) });\n  });\n\n  it('should be possible to checkout with local files that will conflict', (done) => {\n    req\n      .post(`${restGit.pathPrefix}/checkout`)\n      .send({ path: testDir, name: testBranch })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end((err, res) => {\n        expect(res.body.errorCode).to.be('merge-failed');\n        done();\n      });\n  });\n\n  it('status should list files in conflict', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(res.inRebase).to.be(false);\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile1]).to.eql({\n        displayName: testFile1,\n        fileName: testFile1,\n        oldFileName: testFile1,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: true,\n        renamed: false,\n        type: 'text',\n        additions: '4',\n        deletions: '0',\n      });\n    });\n  });\n});\n\ndescribe('git-api conflict merge', function () {\n  const testBranch = 'testBranch1';\n  const testFile1 = 'testfile1.txt';\n\n  before(() => {\n    return common.initRepo(req).then((dir) => {\n      testDir = dir;\n      return common\n        .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: 'a',\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() =>\n          common.post(req, '/branches', { path: testDir, name: testBranch, startPoint: 'master' })\n        )\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: 'b',\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() => common.post(req, '/checkout', { path: testDir, name: testBranch }))\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: 'c',\n            files: [{ name: testFile1 }],\n          })\n        );\n    });\n  });\n\n  it('should be possible to merge the branches', (done) => {\n    req\n      .post(`${restGit.pathPrefix}/merge`)\n      .send({ path: testDir, with: 'master' })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end((err, res) => {\n        expect(res.body.errorCode).to.be('merge-failed');\n        done();\n      });\n  });\n\n  it('status should list files in conflict', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(res.inMerge).to.be(true);\n      expect(res.commitMessage).to.be.ok();\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile1]).to.eql({\n        displayName: testFile1,\n        fileName: testFile1,\n        oldFileName: testFile1,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: true,\n        renamed: false,\n        type: 'text',\n        additions: '4',\n        deletions: '0',\n      });\n    });\n  });\n\n  it('should be possible fix the conflict', () => {\n    return common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) });\n  });\n\n  it('should be possible to resolve', () => {\n    return common.post(req, '/resolveconflicts', { path: testDir, files: [testFile1] });\n  });\n\n  it('should be possible continue the merge', () => {\n    return common.post(req, '/merge/continue', { path: testDir, message: 'something' });\n  });\n\n  it('log should show changes on the merge commit', () => {\n    return common.get(req, '/gitlog', { path: testDir }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(4);\n      expect(res.nodes[0].additions).to.eql(1);\n      expect(res.nodes[0].deletions).to.eql(1);\n      expect(res.nodes[0].fileLineDiffs.length).to.be(1);\n      expect(res.nodes[0].fileLineDiffs[0]).to.eql({\n        additions: 1,\n        deletions: 1,\n        fileName: testFile1,\n        oldFileName: testFile1,\n        displayName: testFile1,\n        type: 'text',\n      });\n    });\n  });\n});\n\ndescribe('git-api conflict solve by deleting', function () {\n  const commitMessage = 'Commit 1';\n  const testFile1 = 'testfile1.txt';\n  const testBranch = 'testBranch';\n\n  before(() => {\n    return common.initRepo(req).then((dir) => {\n      testDir = dir;\n\n      return common\n        .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: commitMessage,\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() =>\n          common.post(req, '/branches', { path: testDir, name: testBranch, startPoint: 'master' })\n        )\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: commitMessage,\n            files: [{ name: testFile1 }],\n          })\n        )\n        .then(() => common.post(req, '/checkout', { path: testDir, name: testBranch }))\n        .then(() =>\n          common.post(req, '/testing/changefile', { file: path.join(testDir, testFile1) })\n        )\n        .then(() =>\n          common.post(req, '/commit', {\n            path: testDir,\n            message: commitMessage,\n            files: [{ name: testFile1 }],\n          })\n        );\n    });\n  });\n\n  it('should be possible to rebase on master', (done) => {\n    req\n      .post(`${restGit.pathPrefix}/rebase`)\n      .send({ path: testDir, onto: 'master' })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end((err, res) => {\n        expect(res.body.errorCode).to.be('merge-failed');\n        done();\n      });\n  });\n\n  it('status should list files in conflict', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(res.inRebase).to.be(true);\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile1]).to.eql({\n        displayName: testFile1,\n        fileName: testFile1,\n        oldFileName: testFile1,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: true,\n        renamed: false,\n        type: 'text',\n        additions: '4',\n        deletions: '0',\n      });\n    });\n  });\n\n  it('should be possible to remove the file', () => {\n    return common.post(req, '/testing/removefile', { file: path.join(testDir, testFile1) });\n  });\n\n  it('should be possible to resolve', () => {\n    return common.post(req, '/resolveconflicts', { path: testDir, files: [testFile1] });\n  });\n\n  it('should be possible continue the rebase', () => {\n    return common.post(req, '/rebase/continue', { path: testDir });\n  });\n\n  after(() => {\n    return common.post(req, '/testing/cleanup', undefined);\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.diff.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nconst req = request(app);\n\ndescribe('git-api diff', () => {\n  let testDir, testBareDir;\n\n  before(() => {\n    return common\n      .initRepo(req)\n      .then((dir) => (testDir = dir))\n      .then(() => common.initRepo(req, { bare: true }))\n      .then((dir) => (testBareDir = dir));\n  });\n\n  after(() => common.post(req, '/testing/cleanup', undefined));\n\n  const testFile = 'afile.txt';\n  const testFile2 = 'anotherfile.txt';\n  const testImage = 'icon.png';\n\n  it('diff on non existing file should fail', () => {\n    return common.get(req, '/diff', { path: testDir, file: testFile });\n  });\n\n  let content;\n\n  it('should be possible to create a file', () => {\n    content = ['A', 'few', 'lines', 'of', 'content', ''];\n\n    return common.post(req, '/testing/createfile', {\n      file: path.join(testDir, testFile),\n      content: content.join('\\n'),\n    });\n  });\n\n  it('should be possible to create an image file', () => {\n    return common.post(req, '/testing/createimagefile', { file: path.join(testDir, testImage) });\n  });\n\n  it('diff on created file should work', () => {\n    return common.get(req, '/diff', { path: testDir, file: testFile }).then((res) => {\n      for (let i = 0; i < content.length; i++) {\n        expect(res.indexOf(content[i])).to.be.above(-1);\n      }\n    });\n  });\n\n  it('diff on image file should work', () => {\n    return common\n      .getPng(req, '/diff/image', { path: testDir, filename: testImage, version: 'current' })\n      .then((res) => expect(res.toString()).to.be('png'));\n  });\n\n  it('should be possible to commit a file', () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: 'Init File',\n      files: [{ name: testFile }],\n    });\n  });\n\n  it('should be possible to commit an image file', () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: 'Init Image',\n      files: [{ name: testImage }],\n    });\n  });\n\n  it('diff on first commit should work', () => {\n    return common\n      .get(req, '/gitlog', { path: testDir })\n      .then((res) => {\n        expect(res.nodes.length).to.be(2);\n        return common.get(req, '/diff', { path: testDir, file: testFile, sha1: res.nodes[1].sha1 });\n      })\n      .then((res) => {\n        for (let i = 0; i < content.length; i++) {\n          expect(res.indexOf(content[i])).to.be.above(-1);\n        }\n      });\n  });\n\n  it('diff on commited file should work', () => {\n    return common.get(req, '/diff', { path: testDir, file: testFile }).then((res) => {\n      expect(res).to.be.an('array');\n      expect(res.length).to.be(0);\n    });\n  });\n\n  it('diff on commited image file should work', () => {\n    return common\n      .getPng(req, '/diff/image', { path: testDir, filename: testImage, version: 'current' })\n      .then((res) => expect(res.toString()).to.be('png'));\n  });\n\n  it('should be possible to modify a file', () => {\n    content.splice(2, 0, 'more');\n    return common.post(req, '/testing/changefile', {\n      file: path.join(testDir, testFile),\n      content: content.join('\\n'),\n    });\n  });\n\n  it('should be possible to modify an image file', () => {\n    return common.post(req, '/testing/changeimagefile', { file: path.join(testDir, testImage) });\n  });\n\n  it('diff on modified file should work', () => {\n    return common.get(req, '/diff', { path: testDir, file: testFile }).then((res) => {\n      expect(res.indexOf('diff --git a/afile.txt b/afile.txt')).to.be.above(-1);\n      expect(res.indexOf('+more')).to.be.above(-1);\n    });\n  });\n\n  it('diff on file commit should work if file is changing', () => {\n    return common\n      .get(req, '/gitlog', { path: testDir })\n      .then((res) => {\n        expect(res.nodes.length).to.be(2);\n        return common.get(req, '/diff', { path: testDir, file: testFile, sha1: res.nodes[1].sha1 });\n      })\n      .then((res) => {\n        expect(res.indexOf('diff --git a/afile.txt b/afile.txt')).to.be.above(-1);\n        expect(res.indexOf('+more')).to.be(-1);\n      });\n  });\n\n  it('getting current image file should work', () => {\n    return common\n      .getPng(req, '/diff/image', { path: testDir, filename: testImage, version: 'current' })\n      .then((res) => expect(res.toString()).to.be('png ~~'));\n  });\n\n  it('getting previous image file should work', () => {\n    return common\n      .getPng(req, '/diff/image', { path: testDir, filename: testImage, version: 'HEAD' })\n      .then((res) => expect(res.toString()).to.be('png'));\n  });\n\n  it('should be possible to rename a modified file', () => {\n    return common.post(req, '/testing/git', {\n      path: testDir,\n      command: ['mv', testFile, testFile2],\n    });\n  });\n\n  it('diff on renamed and modified file should work', () => {\n    return common\n      .get(req, '/diff', { path: testDir, file: testFile2, oldFile: testFile })\n      .then((res) => {\n        expect(res.indexOf('diff --git a/afile.txt b/anotherfile.txt')).to.be.above(-1);\n        expect(res.indexOf('+more')).to.be.above(-1);\n      });\n  });\n\n  it('should be possible to commit the renamed and modified file', () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: 'Move and Change',\n      files: [{ name: testFile2 }],\n    });\n  });\n\n  it('diff on commit with renamed and modified file should work', () => {\n    return common\n      .get(req, '/gitlog', { path: testDir })\n      .then((res) => {\n        expect(res.nodes.length).to.be(3);\n        return common.get(req, '/diff', {\n          path: testDir,\n          file: testFile2,\n          oldFile: testFile,\n          sha1: res.nodes[0].sha1,\n        });\n      })\n      .then((res) => {\n        for (let i = 0; i < content.length; i++) {\n          expect(res.indexOf(content[i])).to.be.above(-1);\n        }\n      });\n  });\n\n  it('removing a test file should work', () => {\n    return common.post(req, '/testing/removefile', { file: path.join(testDir, testFile2) });\n  });\n\n  it('should be possible to commit an image file for removal', () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: 'Init',\n      files: [{ name: testImage }],\n    });\n  });\n\n  it('removing a test image file should work', () => {\n    return common.post(req, '/testing/removefile', { file: path.join(testDir, testImage) });\n  });\n\n  it('diff on removed file should work', () => {\n    return common.get(req, '/diff', { path: testDir, file: testFile2 }).then((res) => {\n      expect(res.indexOf('deleted file')).to.be.above(-1);\n      expect(res.indexOf('@@ -1,6 +0,0 @@')).to.be.above(-1);\n    });\n  });\n\n  it('getting previous image file should work after removal', () => {\n    return common\n      .getPng(req, '/diff/image', { path: testDir, filename: testImage, version: 'HEAD' })\n      .then((res) => expect(res.toString()).to.be('png ~~'));\n  });\n\n  it('diff on bare repository file should work', () => {\n    // first add remote and push all commits\n    return common\n      .post(req, '/remotes/barerepository', { path: testDir, url: testBareDir })\n      .then(() => common.post(req, '/push', { path: testDir, remote: 'barerepository' }))\n      .then(() => common.get(req, '/gitlog', { path: testDir }))\n      .then((res) => {\n        // find a commit which contains the testFile\n        const commit = res.nodes.filter((commit) =>\n          commit.fileLineDiffs.some((lineDiff) => lineDiff.fileName == testFile)\n        )[0];\n        return common.get(req, '/diff', { path: testDir, sha1: commit.sha1, file: testFile });\n      });\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.discardchanges.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nconst req = request(app);\n\ndescribe('git-api discardchanges', () => {\n  after(() => common.post(req, '/testing/cleanup'));\n\n  it('should be able to discard a new file', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile1 = 'test.txt';\n      return common\n        .post(req, '/testing/createfile', { file: path.join(dir, testFile1) })\n        .then(() => common.post(req, '/discardchanges', { path: dir, file: testFile1 }))\n        .then(() => common.get(req, '/status', { path: dir }))\n        .then((res) => expect(Object.keys(res.files).length).to.be(0));\n    });\n  });\n\n  it('should be able to discard a changed file', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile1 = 'test.txt';\n\n      return common\n        .post(req, '/testing/createfile', { file: path.join(dir, testFile1) })\n        .then(() =>\n          common.post(req, '/commit', { path: dir, message: 'lol', files: [{ name: testFile1 }] })\n        )\n        .then(() => common.post(req, '/testing/changefile', { file: path.join(dir, testFile1) }))\n        .then(() => common.post(req, '/discardchanges', { path: dir, file: testFile1 }))\n        .then(() => common.get(req, '/status', { path: dir }))\n        .then((res) => expect(Object.keys(res.files).length).to.be(0));\n    });\n  });\n\n  it('should be able to discard a removed file', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile1 = 'test.txt';\n\n      return common\n        .post(req, '/testing/createfile', { file: path.join(dir, testFile1) })\n        .then(() =>\n          common.post(req, '/commit', { path: dir, message: 'lol', files: [{ name: testFile1 }] })\n        )\n        .then(() => common.post(req, '/testing/removefile', { file: path.join(dir, testFile1) }))\n        .then(() => common.post(req, '/discardchanges', { path: dir, file: testFile1 }))\n        .then(() => common.get(req, '/status', { path: dir }))\n        .then((res) => expect(Object.keys(res.files).length).to.be(0));\n    });\n  });\n\n  it('should be able to discard a new and staged file', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile1 = 'test.txt';\n\n      return common\n        .post(req, '/testing/createfile', { file: path.join(dir, testFile1) })\n        .then(() => common.post(req, '/testing/git', { path: dir, command: ['add', testFile1] }))\n        .then(() => common.post(req, '/discardchanges', { path: dir, file: testFile1 }))\n        .then(() => common.get(req, '/status', { path: dir }))\n        .then((res) => expect(Object.keys(res.files).length).to.be(0));\n    });\n  });\n\n  it('should be able to discard a staged and removed file', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile1 = 'test.txt';\n\n      return common\n        .post(req, '/testing/createfile', { file: path.join(dir, testFile1) })\n        .then(() => common.post(req, '/testing/git', { path: dir, command: ['add', testFile1] }))\n        .then(() => common.post(req, '/testing/removefile', { file: path.join(dir, testFile1) }))\n        .then(() => common.post(req, '/discardchanges', { path: dir, file: testFile1 }))\n        .then(() => common.get(req, '/status', { path: dir }))\n        .then((res) => expect(Object.keys(res.files).length).to.be(0));\n    });\n  });\n\n  it('should be able to discard discard submodule changes', function () {\n    const testFile = 'smalltestfile.txt';\n    const submodulePath = 'subrepo';\n\n    return common\n      .createSmallRepo(req)\n      .then((dir) => {\n        return common.createSmallRepo(req).then((subrepoDir) => {\n          return common\n            .post(req, '/submodules/add', {\n              submoduleUrl: subrepoDir,\n              submodulePath: submodulePath,\n              path: dir,\n            })\n            .then(() => dir);\n        });\n      })\n      .then((dir) => {\n        return common\n          .post(req, '/commit', { path: dir, message: 'lol', files: [{ name: '.gitmodules' }] })\n          .then(() =>\n            common.post(req, '/testing/changefile', {\n              file: path.join(dir, submodulePath, testFile),\n            })\n          )\n          .then(() => common.post(req, '/discardchanges', { path: dir, file: submodulePath }))\n          .then(() => common.get(req, '/status', { path: dir }))\n          .then((res) => expect(Object.keys(res.files).length).to.be(0));\n      });\n  });\n\n  // Need to make discardchanges even more powerful to handle this\n  /*it('should be able to discard a commited, staged and removed file', () => {\n    common.createSmallRepo(req, function(dir) {\n      if (err) return done(err);\n      const testFile1 = 'test.txt';\n\n        () => {common.post(req, '/testing/createfile', { file: path.join(dir, testFile1) });\n        () => {common.post(req, '/commit', { path: dir, message: 'lol', files: [{ name: testFile1 }] });\n        () => {common.post(req, '/testing/changefile', { file: path.join(dir, testFile1) });\n        () => {common.post(req, '/testing/git', { path: dir, command: ['add', testFile1] });\n        () => {common.post(req, '/testing/removefile', { file: path.join(dir, testFile1) });\n        () => {common.post(req, '/discardchanges', { path: dir, file: testFile1 });\n        () => {common.get(req, '/status', { path: dir }).then((res) => {\n          if (err) return done(err);\n          expect(Object.keys(res.files).length).to.be(0);\n          done();\n        }); },\n      ], done);\n    });\n  });*/\n});\n"
  },
  {
    "path": "test/spec.git-api.ignorefile.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst fs = require('fs').promises;\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nconst req = request(app);\n\ndescribe('git-api: test ignorefile call', () => {\n  after(() => common.post(req, '/testing/cleanup'));\n\n  it('Add a file to .gitignore file through api call', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile = 'test.txt';\n\n      // Create .gitignore file prior to append\n      return fs\n        .writeFile(path.join(dir, '.gitignore'), 'test git ignore file...')\n        .then(() => common.post(req, '/testing/createfile', { file: path.join(dir, testFile) }))\n        .then(() => common.post(req, '/ignorefile', { path: dir, file: testFile }))\n        .then(() => {\n          return common.get(req, '/status', { path: dir }).then((res) => {\n            expect(Object.keys(res.files).toString()).to.be('.gitignore');\n          });\n        })\n        .then(() => {\n          return fs.readFile(path.join(dir, '.gitignore'), { encoding: 'utf8' }).then((data) => {\n            if (data.indexOf(testFile) < 0) {\n              throw new Error('Test file is not added to the .gitignore file.');\n            }\n          });\n        });\n    });\n  });\n\n  it('Add a file to .gitignore file through api call when .gitignore is missing', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile = 'test.txt';\n\n      return common\n        .post(req, '/testing/createfile', { file: path.join(dir, testFile) })\n        .then(() => common.post(req, '/ignorefile', { path: dir, file: testFile }))\n        .then(() => {\n          return common.get(req, '/status', { path: dir }).then((res) => {\n            expect(Object.keys(res.files).toString()).to.be('.gitignore');\n          });\n        })\n        .then(() => {\n          return fs.readFile(path.join(dir, '.gitignore'), { encoding: 'utf8' }).then((data) => {\n            if (data.indexOf(testFile) < 0) {\n              throw new Error('Test file is not added to the .gitignore file.');\n            }\n          });\n        });\n    });\n  });\n\n  it('Attempt to add a file where similar name alread exist in .gitignore through api call', () => {\n    return common.createSmallRepo(req).then((dir) => {\n      const testFile = 'test.txt';\n\n      // add part of file name to gitignore\n      return fs\n        .appendFile(path.join(dir, '.gitignore'), testFile.split('.')[0])\n        .then(() => common.post(req, '/testing/createfile', { file: path.join(dir, testFile) }))\n        .then(() => common.post(req, '/ignorefile', { path: dir, file: testFile }))\n        .then(() => {\n          return common.get(req, '/status', { path: dir }).then((res) => {\n            expect(Object.keys(res.files).toString()).to.be('.gitignore');\n          });\n        })\n        .then(() => {\n          return fs.readFile(path.join(dir, '.gitignore'), { encoding: 'utf8' }).then((data) => {\n            if (data.indexOf(testFile) < 0) {\n              throw new Error('Test file is not added to the .gitignore file.');\n            }\n          });\n        });\n    });\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst fs = require('fs').promises;\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\nconst mkdirp = require('mkdirp').mkdirp;\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nlet testDir;\nlet gitConfig;\n\nconst req = request(app);\n\nconst commitMessage = 'test';\nconst testFile = 'somefile';\nconst testFile2 = 'my test.txt';\nconst testSubDir = 'sub';\nconst testFile3 = path.join(testSubDir, 'testy.txt').replace('\\\\', '/');\nconst commitMessage3 = 'commit3';\nconst commitMessage4 = 'Removed some file';\nconst testFile4 = path.join(testSubDir, 'renamed.txt').replace(/\\\\/, '/');\n\ndescribe('git-api', () => {\n  before('creating test dir should work', () => {\n    return common.post(req, '/testing/createtempdir').then((res) => {\n      expect(res.path).to.be.ok();\n      return fs.realpath(res.path).then((dir) => {\n        testDir = dir;\n      });\n    });\n  });\n\n  after(() => common.post(req, '/testing/cleanup'));\n\n  it('gitconfig should return config data', () => {\n    return common.get(req, '/gitconfig', { path: testDir }).then((res) => {\n      expect(res).to.be.an('object');\n      expect(res['user.name']).to.be.ok();\n      expect(res['user.email']).to.be.ok();\n      gitConfig = res;\n    });\n  });\n\n  it('status should fail in uninited directory', (done) => {\n    req\n      .get(`${restGit.pathPrefix}/status`)\n      .query({ path: path.join(testDir, 'nowhere') })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end((err, res) => {\n        expect(res.body.errorCode).to.be('no-such-path');\n        done();\n      });\n  });\n\n  it('quickstatus should say uninited in uninited directory', () => {\n    return common\n      .get(req, '/quickstatus', { path: testDir })\n      .then((res) => expect(res).to.eql({ type: 'uninited', subRepos: [], gitRootPath: testDir }));\n  });\n\n  it('quickstatus should say uninited with sub repos if it has sub repos', () => {\n    let testDirWithSubRepos;\n    let subRepo1, subRepo2;\n\n    return common\n      .post(req, '/testing/createtempdir')\n      .then((res) => {\n        expect(res.path).to.be.ok();\n        return fs.realpath(res.path).then((dir) => {\n          testDirWithSubRepos = dir;\n        });\n      })\n      .then(() => {\n        subRepo1 = path.join(testDirWithSubRepos, 'repo1');\n        return fs.mkdir(subRepo1).then(() => common.post(req, '/init', { path: subRepo1 }));\n      })\n      .then(() => {\n        subRepo2 = path.join(testDirWithSubRepos, 'repo2');\n        return fs.mkdir(subRepo2).then(() => common.post(req, '/init', { path: subRepo2 }));\n      })\n      .then(() => {\n        return common.get(req, '/quickstatus', { path: testDirWithSubRepos }).then((res) =>\n          expect(res).to.eql({\n            type: 'uninited',\n            subRepos: [subRepo1, subRepo2],\n            gitRootPath: testDirWithSubRepos,\n          })\n        );\n      });\n  });\n\n  it('status should fail in non-existing directory', () => {\n    return common\n      .get(req, '/status', { path: testDir })\n      .catch((e) => expect(e.errorCode).to.be('no-such-path'));\n  });\n\n  it('quickstatus should say false in non-existing directory', () => {\n    return common\n      .get(req, '/quickstatus', { path: path.join(testDir, 'nowhere') })\n      .then((res) =>\n        expect(res).to.eql({ type: 'no-such-path', gitRootPath: path.join(testDir, 'nowhere') })\n      );\n  });\n\n  it('init should succeed in uninited directory', () => {\n    return common.post(req, '/init', { path: testDir });\n  });\n\n  it('status should succeed in inited directory', () => {\n    return common.get(req, '/status', { path: testDir });\n  });\n\n  it('quickstatus should say inited in inited directory', () => {\n    return common\n      .get(req, '/quickstatus', { path: testDir })\n      .then((res) => expect(res).to.eql({ type: 'inited', gitRootPath: testDir }));\n  });\n\n  it(\"commit should fail on when there's no files to commit\", (done) => {\n    req\n      .post(`${restGit.pathPrefix}/commit`)\n      .send({ path: testDir, message: 'test', files: [] })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end(done);\n  });\n\n  // testFile\n\n  it('log should be empty before first commit', () => {\n    return common.get(req, '/gitlog', { path: testDir }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(0);\n    });\n  });\n\n  it('head should be empty before first commit', () => {\n    return common.get(req, '/head', { path: testDir }).then((res) => {\n      expect(res).to.be.a('array');\n      expect(res.length).to.be(0);\n    });\n  });\n\n  it('commit should fail on non-existing file', (done) => {\n    req\n      .post(`${restGit.pathPrefix}/commit`)\n      .send({ path: testDir, message: 'test', files: [{ name: testFile }] })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end(done);\n  });\n\n  it('creating test file should work', () => {\n    return common.post(req, '/testing/createfile', { file: path.join(testDir, testFile) });\n  });\n\n  it('status should list untracked file', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile]).to.eql({\n        displayName: testFile,\n        fileName: testFile,\n        oldFileName: testFile,\n        isNew: true,\n        staged: false,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '-',\n        deletions: '-',\n      });\n    });\n  });\n\n  // commitMessage\n\n  it('commit should fail without commit message', (done) => {\n    req\n      .post(`${restGit.pathPrefix}/commit`)\n      .send({ path: testDir, message: undefined, files: [{ name: testFile }] })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(400)\n      .end(done);\n  });\n\n  it(\"commit should succeed when there's files to commit\", () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: commitMessage,\n      files: [{ name: testFile }],\n    });\n  });\n\n  it('log should show latest commit', () => {\n    return common.get(req, '/gitlog', { path: testDir }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(1);\n      expect(res.nodes[0].message.indexOf(commitMessage)).to.be(0);\n      expect(res.nodes[0].authorName).to.be(gitConfig['user.name']);\n      expect(res.nodes[0].authorEmail).to.be(gitConfig['user.email']);\n    });\n  });\n\n  it('head should show latest commit', () => {\n    return common.get(req, '/head', { path: testDir }).then((res) => {\n      expect(res).to.be.a('array');\n      expect(res.length).to.be(1);\n      expect(res[0].message.indexOf(commitMessage)).to.be(0);\n      expect(res[0].authorName).to.be(gitConfig['user.name']);\n      expect(res[0].authorEmail).to.be(gitConfig['user.email']);\n    });\n  });\n\n  it('modifying a test file should work', () => {\n    return common.post(req, '/testing/changefile', { file: path.join(testDir, testFile) });\n  });\n\n  it('modified file should show up in status', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile]).to.eql({\n        displayName: testFile,\n        fileName: testFile,\n        oldFileName: testFile,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '1',\n        deletions: '1',\n      });\n    });\n  });\n\n  it('discarding changes should work', () => {\n    return common.post(req, '/discardchanges', { path: testDir, file: testFile });\n  });\n\n  it('modifying a test file should work part deux', () => {\n    return common.post(req, '/testing/changefile', { file: path.join(testDir, testFile) });\n  });\n\n  it('commit ammend should work', () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: commitMessage,\n      files: [{ name: testFile }],\n      amend: true,\n    });\n  });\n\n  it('amend should not produce additional log-entry', () => {\n    return common\n      .get(req, '/gitlog', { path: testDir })\n      .then((res) => expect(res.nodes.length).to.be(1));\n  });\n\n  // testFile2\n\n  it('creating a multi word test file should work', () => {\n    return common.post(req, '/testing/createfile', { file: path.join(testDir, testFile2) });\n  });\n\n  it('status should list the new file', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile2]).to.eql({\n        displayName: testFile2,\n        fileName: testFile2,\n        oldFileName: testFile2,\n        isNew: true,\n        staged: false,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '-',\n        deletions: '-',\n      });\n    });\n  });\n\n  it('discarding the new file should work', (done) => {\n    req\n      .post(`${restGit.pathPrefix}/discardchanges`)\n      .send({ path: testDir, file: testFile2 })\n      .set('Accept', 'application/json')\n      .expect('Content-Type', /json/)\n      .expect(200)\n      .end(done);\n  });\n\n  // testSubDir\n\n  it('creating test sub dir should work', () => {\n    return common.post(req, '/createdir', { dir: path.join(testDir, testSubDir) });\n  });\n\n  it('creating test multi layer dir should work', () => {\n    return common.post(req, '/createdir', {\n      dir: path.join(testDir, `${testSubDir}test/moretest/andmore`),\n    });\n  });\n\n  // testFile3\n\n  it('creating a test file in sub dir should work', () => {\n    return common.post(req, '/testing/createfile', { file: path.join(testDir, testFile3) });\n  });\n\n  it('status should list the new file once again', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile3]).to.eql({\n        displayName: testFile3,\n        fileName: testFile3,\n        oldFileName: testFile3,\n        isNew: true,\n        staged: false,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '-',\n        deletions: '-',\n      });\n    });\n  });\n\n  // commitMessage3\n\n  it('commit should succeed with file in sub dir', () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: commitMessage3,\n      files: [{ name: testFile3 }],\n    });\n  });\n\n  it('log should show last commit', () => {\n    return common.get(req, '/gitlog', { path: testDir }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(2);\n      const HEAD = res.nodes[0];\n\n      expect(HEAD.message.indexOf(commitMessage3)).to.be(0);\n      expect(HEAD.authorDate).to.be.a('string');\n      expect(HEAD.authorName).to.be(gitConfig['user.name']);\n      expect(HEAD.authorEmail).to.be(gitConfig['user.email']);\n      expect(HEAD.commitDate).to.be.a('string');\n      expect(HEAD.committerName).to.be(gitConfig['user.name']);\n      expect(HEAD.committerEmail).to.be(gitConfig['user.email']);\n      expect(HEAD.sha1).to.be.ok();\n    });\n  });\n\n  it('removing a test file should work', () => {\n    return common.post(req, '/testing/removefile', { file: path.join(testDir, testFile) });\n  });\n\n  it('status should list the removed file', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile]).to.eql({\n        displayName: testFile,\n        fileName: testFile,\n        oldFileName: testFile,\n        isNew: false,\n        staged: false,\n        removed: true,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '0',\n        deletions: '2',\n      });\n    });\n  });\n\n  // commitMessage4\n\n  it('commit on removed file should work', () => {\n    return common.post(req, '/commit', {\n      path: testDir,\n      message: commitMessage4,\n      files: [{ name: testFile }],\n    });\n  });\n\n  it('status should list nothing', () => {\n    return common\n      .get(req, '/status', { path: testDir })\n      .then((res) => expect(Object.keys(res.files).length).to.be(0));\n  });\n\n  // testFile4\n\n  it('renaming a file should work', () => {\n    return common.post(req, '/testing/git', {\n      path: testDir,\n      command: ['mv', testFile3, testFile4],\n    });\n  });\n\n  it('status should list the renamed file', () => {\n    return common.get(req, '/status', { path: testDir }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[testFile4]).to.eql({\n        displayName: `${testFile3} → ${testFile4}`,\n        fileName: testFile4,\n        oldFileName: testFile3,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: false,\n        renamed: true,\n        type: 'text',\n        additions: '0',\n        deletions: '0',\n      });\n    });\n  });\n\n  it('log with limit should only return specified number of items', () => {\n    return common.get(req, '/gitlog', { path: testDir, limit: 1 }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(1);\n    });\n  });\n\n  it('get the baserepopath without base repo should work', (done) => {\n    const baseRepoPathTestDir = path.join(testDir, 'depth1', 'depth2');\n\n    mkdirp(baseRepoPathTestDir).then(() => {\n      return common.get(req, '/baserepopath', { path: baseRepoPathTestDir }).then((res) => {\n        // Some oses uses symlink and path will be different as git will return resolved symlink\n        expect(res.path).to.contain(testDir);\n        done();\n      });\n    });\n  });\n\n  it('test gitignore api endpoint', () => {\n    return common\n      .put(req, '/gitignore', { path: testDir, data: 'abc' })\n      .then(() => common.get(req, '/gitignore', { path: testDir }))\n      .then((res) => expect(res.content).to.be('abc'))\n      .then(() => common.put(req, '/gitignore', { path: testDir, data: '' }))\n      .then(() => common.get(req, '/gitignore', { path: testDir }))\n      .then((res) => expect(res.content).to.be(''));\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.patch.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\nconst md5 = require('blueimp-md5');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nlet testDir;\nconst req = request(app);\n\nconst testPatch = (req, testDir, testFileName, contentsToPatch, files) => {\n  // testDir = '/tmp/testdir';\n  return common\n    .post(req, '/testing/createfile', {\n      file: path.join(testDir, testFileName),\n      content: contentsToPatch[0],\n    })\n    .then(() =>\n      common.post(req, '/commit', {\n        path: testDir,\n        message: `a commit for ${testFileName}`,\n        files: [{ name: testFileName }],\n      })\n    )\n    .then(() =>\n      common.post(req, '/testing/changefile', {\n        file: path.join(testDir, testFileName),\n        content: contentsToPatch[1],\n      })\n    )\n    .then(() =>\n      common.post(req, '/commit', {\n        path: testDir,\n        message: `patched commit ${testFileName}`,\n        files: files,\n      })\n    );\n};\n\nconst getPatchLineList = (size, notSelected) => {\n  const patchLineList = [];\n  for (let n = 0; n < size; n++) {\n    patchLineList.push(false);\n  }\n\n  if (notSelected) {\n    for (let m = 0; m < notSelected.length; m++) {\n      patchLineList[notSelected[m]] = true;\n    }\n  }\n  return patchLineList;\n};\n\nconst getContentsToPatch = (size, toChange) => {\n  let content = '';\n  let changedContent = '';\n\n  for (let n = 0; n < size; n++) {\n    content += n + '\\n';\n    changedContent += n;\n    if (!toChange || toChange.indexOf(n) > -1) {\n      changedContent += '!';\n    }\n    changedContent += '\\n';\n  }\n\n  return [content, changedContent];\n};\n\nconst getContentsToPatchWithAdd = (size, numLinesToAdd) => {\n  let content = '';\n  let changedContent = '';\n  let n = 0;\n\n  while (n < size) {\n    content += n + '\\n';\n    changedContent += n + '\\n';\n    n++;\n  }\n  while (n < size + numLinesToAdd) {\n    changedContent += n + '\\n';\n    n++;\n  }\n\n  return [content, changedContent];\n};\n\nconst getContentsToPatchWithDelete = (size, numLinesToDelete) => {\n  let content = '';\n  let changedContent = '';\n  let n = 0;\n\n  while (n < size) {\n    content += n + '\\n';\n    if (n < size - numLinesToDelete) {\n      changedContent += n + '\\n';\n    }\n    n++;\n  }\n\n  return [content, changedContent];\n};\n\ndescribe('git-api: test patch api', () => {\n  it('creating test dir should work', () => {\n    return common.post(req, '/testing/createtempdir').then((res) => {\n      expect(res.path).to.be.ok();\n      testDir = res.path;\n    });\n  });\n\n  it('init test dir should work', () => {\n    return common.post(req, '/init', { path: testDir, bare: false });\n  });\n\n  ///////////////////////////////////////////////////////\n  // Single diff block diff, (git apply uses diff -U3) //\n  ///////////////////////////////////////////////////////\n\n  it('Create a file with 10 lines, commit, change each 10 lines, and commit patch with all selected.', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const contentsToPatch = getContentsToPatch(testFileSize);\n    const patchLineList = [];\n\n    for (let n = 0; n < testFileSize * 2; n++) {\n      patchLineList.push(true);\n    }\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('Create a file with 10 lines, commit, change each 10 lines, and commit patch with none selected.', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const patchLineList = getPatchLineList(testFileSize * 2);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('10 lines, 10 edit, 0~2 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const patchLineList = getPatchLineList(testFileSize * 2, [0, 1, 2]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('10 lines, 10 edit, 18~19 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const patchLineList = getPatchLineList(testFileSize * 2, [18, 19]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('10 lines, 10 edit, 0~2 and 18~19 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const patchLineList = getPatchLineList(testFileSize * 2, [0, 1, 2, 18, 19]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('10 lines, 10 edit, 5~7 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const patchLineList = getPatchLineList(testFileSize * 2, [5, 6, 7]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 30 edit, 0~2 and 28 ~ 29 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const patchLineList = getPatchLineList(testFileSize * 2, [0, 1, 2, 28, 29]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 30 edit, 0~2, 28~29, 58~59 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const patchLineList = getPatchLineList(testFileSize * 2, [0, 1, 2, 28, 29, 57, 58, 59]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 30 edit, 6~8, 16~18 and 58 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const patchLineList = getPatchLineList(testFileSize * 2, [6, 7, 8, 16, 17, 18, 58]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 30 edit, 12~15 and 17~19 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const patchLineList = getPatchLineList(testFileSize * 2, [12, 13, 14, 15, 17, 18, 19]);\n    const contentsToPatch = getContentsToPatch(testFileSize);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 12~19 edit, 0~7, 10~16 selected ', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const linesToChange = [12, 13, 14, 15, 16, 17, 18, 19];\n    const contentsToPatch = getContentsToPatch(testFileSize, linesToChange);\n    const patchLineList = getPatchLineList(\n      linesToChange.length * 2,\n      [0, 1, 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 16]\n    );\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  //////////////////////////////////////////////////////\n  // Multi diff block diff, (git apply uses diff -U3) //\n  //////////////////////////////////////////////////////\n\n  it('30 lines, 2~4, 12~14, 22~24 edit, all selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const linesToChange = [2, 3, 4, 12, 13, 14, 22, 23, 24];\n    const contentsToPatch = getContentsToPatch(testFileSize, linesToChange);\n    const patchLineList = getPatchLineList(\n      linesToChange.length * 2,\n      [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]\n    );\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 2~4, 12~14, 22~24 edit, 0~5, 12~17 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const linesToChange = [2, 3, 4, 12, 13, 14, 22, 23, 24];\n    const contentsToPatch = getContentsToPatch(testFileSize, linesToChange);\n    const patchLineList = getPatchLineList(\n      linesToChange.length * 2,\n      [0, 1, 2, 3, 4, 5, 12, 13, 14, 15, 16, 17]\n    );\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 2~4, 12~14, 22~24 edit, 6~11 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const linesToChange = [2, 3, 4, 12, 13, 14, 22, 23, 24];\n    const contentsToPatch = getContentsToPatch(testFileSize, linesToChange);\n    const patchLineList = getPatchLineList(linesToChange.length * 2, [6, 7, 8, 9, 10, 11]);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 2~4, 12~14, 22~24 edit, none selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const linesToChange = [2, 3, 4, 12, 13, 14, 22, 23, 24];\n    const contentsToPatch = getContentsToPatch(testFileSize, linesToChange);\n    const patchLineList = getPatchLineList(linesToChange.length * 2);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  it('30 lines, 12~14, 16~18 edit, 6~11 selected', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 30;\n    const linesToChange = [12, 13, 14, 22, 23, 24];\n    const contentsToPatch = getContentsToPatch(testFileSize, linesToChange);\n    const patchLineList = getPatchLineList(linesToChange.length * 2, [6, 7, 8, 9, 10, 11]);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  // added diff only, (git apply uses diff -U3)\n  it('10 lines, add 5 lines, select 0~1, 5', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const linesToAdd = 5;\n    const contentsToPatch = getContentsToPatchWithAdd(testFileSize, linesToAdd);\n    const patchLineList = getPatchLineList(linesToAdd, [0, 1, 5]);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n\n  // deleted diff only, (git apply uses diff -U3)\n  it('10 lines, delete 5 lines, select 0~1, 5', () => {\n    const testFileName = md5(Date.now());\n    const testFileSize = 10;\n    const linesToDelete = 5;\n    const contentsToPatch = getContentsToPatchWithDelete(testFileSize, linesToDelete);\n    const patchLineList = getPatchLineList(linesToDelete, [0, 1, 5]);\n\n    return testPatch(req, testDir, testFileName, contentsToPatch, [\n      { name: testFileName, patchLineList: patchLineList },\n    ]);\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.remote.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nlet testDirLocal1, testDirLocal2, testDirRemote;\n\nconst req = request(app);\n\ndescribe('git-api remote', function () {\n  this.timeout(4000);\n\n  before('creating test dirs should work', () => {\n    return common\n      .post(req, '/testing/createtempdir')\n      .then((dir) => {\n        testDirLocal1 = dir.path;\n      })\n      .then(() => common.post(req, '/testing/createtempdir'))\n      .then((dir) => {\n        testDirLocal2 = dir.path;\n      })\n      .then(() => common.post(req, '/testing/createtempdir'))\n      .then((dir) => {\n        testDirRemote = dir.path;\n      });\n  });\n\n  after(() => common.post(req, '/testing/cleanup'));\n\n  it('init a bare \"remote\" test dir should work', () => {\n    return common.post(req, '/init', { path: testDirRemote, bare: true });\n  });\n\n  it('remotes in no-remotes-repo should be zero', () => {\n    return common\n      .get(req, '/remotes', { path: testDirRemote })\n      .then((res) => expect(res.length).to.be(0));\n  });\n\n  it('cloning \"remote\" to \"local1\" should work', () => {\n    return common.post(req, '/clone', {\n      path: testDirLocal1,\n      url: testDirRemote,\n      destinationDir: '.',\n    });\n  });\n\n  it('remotes in cloned-repo should be one', () => {\n    return common.get(req, '/remotes', { path: testDirLocal1 }).then((res) => {\n      expect(res.length).to.be(1);\n      const remote = res[0];\n      expect(remote.name).to.be('origin');\n      expect(remote.pushUrl).to.be(testDirRemote);\n      expect(remote.fetchUrl).to.be(testDirRemote);\n    });\n  });\n\n  it('remote/origin in cloned-repo should work', () => {\n    return common\n      .get(req, '/remotes/origin', { path: testDirLocal1 })\n      .then((res) => expect(res.address).to.be(testDirRemote));\n  });\n\n  it('creating a commit in \"local1\" repo should work', () => {\n    const testFile = path.join(testDirLocal1, 'testfile1.txt');\n    return common.post(req, '/testing/createfile', { file: testFile }).then(() => {\n      return common.post(req, '/commit', {\n        path: testDirLocal1,\n        message: 'Init',\n        files: [{ name: testFile }],\n      });\n    });\n  });\n\n  it('log in \"local1\" should show the init commit', () => {\n    return common.get(req, '/gitlog', { path: testDirLocal1 }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(1);\n      const init = res.nodes[0];\n      expect(init.message.indexOf('Init')).to.be(0);\n      expect(init.refs).to.contain('HEAD');\n      expect(init.refs).to.contain('refs/heads/master');\n    });\n  });\n\n  it('pushing form \"local1\" to \"remote\" should work', () => {\n    return common.post(req, '/push', { path: testDirLocal1, remote: 'origin' });\n  });\n\n  it('cloning \"remote\" to \"local2\" should work', () => {\n    return common.post(req, '/clone', {\n      path: testDirLocal2,\n      url: testDirRemote,\n      destinationDir: '.',\n      isRecursiveSubmodule: true,\n    });\n  });\n\n  it('log in \"local2\" should show the init commit', () => {\n    return common.get(req, '/gitlog', { path: testDirLocal2 }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(1);\n      const init = res.nodes[0];\n      expect(init.message.indexOf('Init')).to.be(0);\n      expect(init.refs).to.contain('HEAD');\n      expect(init.refs).to.contain('refs/heads/master');\n      expect(init.refs).to.contain('refs/remotes/origin/master');\n      expect(init.refs).to.contain('refs/remotes/origin/HEAD');\n    });\n  });\n\n  it('creating and pushing a commit in \"local1\" repo should work', () => {\n    const testFile = path.join(testDirLocal1, 'testfile2.txt');\n    return common\n      .post(req, '/testing/createfile', { file: testFile })\n      .then(() => new Promise((resolve) => setTimeout(resolve, 500)))\n      .then(() =>\n        common.post(req, '/commit', {\n          path: testDirLocal1,\n          message: 'Commit2',\n          files: [{ name: testFile }],\n        })\n      )\n      .then(() => common.post(req, '/push', { path: testDirLocal1, remote: 'origin' }));\n  });\n\n  it('fetching in \"local2\" should work', () => {\n    return common.get(req, '/fetch', { path: testDirLocal2, remote: 'origin' });\n  });\n\n  it('log in \"local2\" should show the branch as one behind', () => {\n    return common.get(req, '/gitlog', { path: testDirLocal2 }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(2);\n      const init = res.nodes.find((node) => node.message.indexOf('Init') == 0);\n      const commit2 = res.nodes.find((node) => node.message.indexOf('Commit2') == 0);\n      expect(init).to.be.ok();\n      expect(commit2).to.be.ok();\n      expect(init.refs).to.contain('HEAD');\n      expect(init.refs).to.contain('refs/heads/master');\n      expect(commit2.refs).to.contain('refs/remotes/origin/master');\n      expect(commit2.refs).to.contain('refs/remotes/origin/HEAD');\n    });\n  });\n\n  it('rebasing local master onto remote master should work in \"local2\"', () => {\n    return common.post(req, '/rebase', { path: testDirLocal2, onto: 'origin/master' });\n  });\n\n  it('log in \"local2\" should show the branch as in sync after rebase', () => {\n    return common.get(req, '/gitlog', { path: testDirLocal2 }).then((res) => {\n      expect(res.nodes).to.be.a('array');\n      expect(res.nodes.length).to.be(2);\n      const init = res.nodes.find((node) => node.message.indexOf('Init') == 0);\n      const commit2 = res.nodes.find((node) => node.message.indexOf('Commit2') == 0);\n      expect(init).to.be.ok();\n      expect(commit2).to.be.ok();\n      expect(init.refs).to.eql([]);\n      expect(commit2.refs).to.contain('HEAD');\n      expect(commit2.refs).to.contain('refs/heads/master');\n      expect(commit2.refs).to.contain('refs/remotes/origin/master');\n      expect(commit2.refs).to.contain('refs/remotes/origin/HEAD');\n    });\n  });\n\n  it('creating a commit in \"local2\" repo should work', () => {\n    const testFile = path.join(testDirLocal2, 'testfile3.txt');\n    return common\n      .post(req, '/testing/createfile', { file: testFile })\n      .then(() => new Promise((resolve) => setTimeout(resolve, 500)))\n      .then(() =>\n        common.post(req, '/commit', {\n          path: testDirLocal2,\n          message: 'Commit3',\n          files: [{ name: testFile }],\n        })\n      );\n  });\n\n  it('resetting local master to remote master should work in \"local2\"', () => {\n    return common.post(req, '/reset', { path: testDirLocal2, to: 'origin/master', mode: 'hard' });\n  });\n\n  it('log in \"local2\" should show the branch as in sync after reset', () => {\n    return common.get(req, '/gitlog', { path: testDirLocal2 }, (res) => {\n      expect(res.nodes.length).to.be(2);\n      const init = res.nodes.find((node) => node.message.indexOf('Init') == 0);\n      const commit2 = res.nodes.find((node) => node.message.indexOf('Commit2') == 0);\n      expect(init.refs).to.eql([]);\n      expect(commit2.refs).to.contain('HEAD');\n      expect(commit2.refs).to.contain('refs/heads/master');\n      expect(commit2.refs).to.contain('refs/remotes/origin/master');\n      expect(commit2.refs).to.contain('refs/remotes/origin/HEAD');\n    });\n  });\n\n  it('status should show nothing', () => {\n    return common\n      .get(req, '/status', { path: testDirLocal2 })\n      .then((res) => expect(Object.keys(res.files).length).to.be(0));\n  });\n\n  it('should be possible to create a tag in \"local2\"', () => {\n    return common.post(req, '/tags', { path: testDirLocal2, name: 'v1.0' });\n  });\n\n  it('should be possible to push a tag from \"local2\"', () => {\n    return common.post(req, '/push', {\n      path: testDirLocal2,\n      remote: 'origin',\n      refSpec: 'v1.0',\n      remoteBranch: 'v1.0',\n    });\n  });\n\n  it('log in \"local2\" should show the local tag', () => {\n    return common.get(req, '/gitlog', { path: testDirLocal2 }).then((res) => {\n      const commit2 = res.nodes.find((node) => node.message.indexOf('Commit2') == 0);\n      expect(commit2.refs).to.contain('tag: refs/tags/v1.0');\n    });\n  });\n\n  it('remote tags in \"local2\" should show the remote tag', () => {\n    return common\n      .get(req, '/remote/tags', { path: testDirLocal2, remote: 'origin' })\n      .then((res) => expect(res.map((tag) => tag.name)).to.contain('refs/tags/v1.0^{}'));\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.squash.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nlet testDir;\n\nconst req = request(app);\nconst rootBranch = 'root';\n\nconst testFile1 = 'testFile1.txt';\nconst testFile2 = 'testFile2.txt';\n\ndescribe('git-api conflict rebase', function () {\n  before(() => {\n    return common.createSmallRepo(req).then((dir) => {\n      testDir = dir;\n    });\n  });\n\n  after(() => common.post(req, '/testing/cleanup'));\n\n  it('establish root branch', () => {\n    return common.post(req, '/branches', { path: testDir, name: rootBranch, startPoint: 'master' });\n  });\n\n  it('create some commits', () => {\n    return common\n      .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n      .then(() =>\n        common.post(req, '/commit', {\n          path: testDir,\n          message: `a commit for ${testFile1}`,\n          files: [{ name: testFile1 }],\n        })\n      )\n      .then(() => common.post(req, '/testing/createfile', { file: path.join(testDir, testFile2) }))\n      .then(() =>\n        common.post(req, '/commit', {\n          path: testDir,\n          message: `a commit for ${testFile2}`,\n          files: [{ name: testFile2 }],\n        })\n      );\n  });\n\n  it('checkout master', () => {\n    return common.post(req, '/checkout', { path: testDir, name: rootBranch });\n  });\n\n  it('squash 2 commits to 1', () => {\n    return common\n      .post(req, '/squash', { path: testDir, target: 'master' })\n      .then(() => common.get(req, '/status', { path: testDir }))\n      .then((res) => expect(Object.keys(res.files).length).to.be(2));\n  });\n\n  it('discard all', () => {\n    return common\n      .post(req, '/discardchanges', { path: testDir, all: true })\n      .then(() => common.get(req, '/status', { path: testDir }))\n      .then((res) => expect(Object.keys(res.files).length).to.be(0));\n  });\n\n  it('making conflicting commit', () => {\n    return common\n      .post(req, '/testing/createfile', { file: path.join(testDir, testFile1) })\n      .then(() =>\n        common.post(req, '/commit', {\n          path: testDir,\n          message: `a 2nd commit for ${testFile1}`,\n          files: [{ name: testFile1 }],\n        })\n      );\n  });\n\n  it('squash 2 commits to 1 with conflict', () => {\n    return common\n      .post(req, '/squash', { path: testDir, target: 'master' })\n      .then(() => common.get(req, '/status', { path: testDir }))\n      .then((res) => {\n        expect(res.inConflict).to.be(true);\n        expect(Object.keys(res.files).length).to.be(2);\n      });\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.stash.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nlet testDir;\n\nconst req = request(app);\n\ndescribe('git-api conflict rebase', function () {\n  const testFile1 = 'testfile1.txt';\n\n  before(() => {\n    return common\n      .createSmallRepo(req)\n      .then((dir) => {\n        testDir = dir;\n      })\n      .then(() => common.post(req, '/testing/createfile', { file: path.join(testDir, testFile1) }));\n  });\n\n  after(() => common.post(req, '/testing/cleanup'));\n\n  it('should be possible to stash', () => common.post(req, '/stashes', { path: testDir }));\n\n  it('stashes should list the stashed item', () => {\n    return common.get(req, '/stashes', { path: testDir }).then((res) => {\n      expect(res.length).to.be(1);\n      expect(res[0].reflogId).to.be('0');\n      expect(res[0].reflogName).to.be('stash@{0}');\n    });\n  });\n\n  it('should be possible to drop stash', () => {\n    return common.delete(req, '/stashes/0', { path: testDir });\n  });\n});\n"
  },
  {
    "path": "test/spec.git-api.submodule.js",
    "content": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst path = require('path');\nconst restGit = require('../source/git-api');\nconst common = require('./common-es6.js');\n\nconst app = express();\napp.use(require('body-parser').json());\n\nrestGit.registerApi({ app: app, config: { dev: true } });\n\nconst req = request(app);\n\ndescribe('git-api submodule', function () {\n  let testDirMain, testDirSecondary;\n\n  before(() => {\n    return common\n      .createSmallRepo(req)\n      .then((dir) => {\n        testDirMain = dir;\n      })\n      .then(() => common.createSmallRepo(req))\n      .then((dir) => {\n        testDirSecondary = dir;\n      });\n  });\n\n  after(() => common.post(req, '/testing/cleanup'));\n\n  const submodulePath = 'sub';\n\n  it('submodule add should work', () => {\n    return common.post(req, '/submodules/add', {\n      path: testDirMain,\n      submodulePath: submodulePath,\n      submoduleUrl: testDirSecondary,\n    });\n  });\n\n  it('submodule should show up in status', () => {\n    return common.get(req, '/status', { path: testDirMain }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(2);\n      expect(res.files[submodulePath]).to.eql({\n        displayName: submodulePath,\n        fileName: submodulePath,\n        oldFileName: submodulePath,\n        isNew: true,\n        staged: true,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '1',\n        deletions: '0',\n      });\n      expect(res.files['.gitmodules']).to.eql({\n        displayName: '.gitmodules',\n        fileName: '.gitmodules',\n        oldFileName: '.gitmodules',\n        isNew: true,\n        staged: true,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '3',\n        deletions: '0',\n      });\n    });\n  });\n\n  it('commit should succeed', () => {\n    return common.post(req, '/commit', {\n      path: testDirMain,\n      message: 'Add submodule',\n      files: [{ name: submodulePath }, { name: '.gitmodules' }],\n    });\n  });\n\n  it('status should be empty after commit', () => {\n    return common\n      .get(req, '/status', { path: testDirMain })\n      .then((res) => expect(Object.keys(res.files).length).to.be(0));\n  });\n\n  it('creating a test file in sub dir should work', () => {\n    const testFile = path.join(submodulePath, 'testy.txt');\n    return common.post(req, '/testing/createfile', { file: path.join(testDirMain, testFile) });\n  });\n\n  // see https://github.com/FredrikNoren/ungit/issues/1472\n  it.skip(\"submodule should show up in status when it's dirty\", () => {\n    return common.get(req, '/status', { path: testDirMain }).then((res) => {\n      expect(Object.keys(res.files).length).to.be(1);\n      expect(res.files[submodulePath]).to.eql({\n        displayName: submodulePath,\n        fileName: submodulePath,\n        oldFileName: submodulePath,\n        isNew: false,\n        staged: false,\n        removed: false,\n        conflict: false,\n        renamed: false,\n        type: 'text',\n        additions: '0',\n        deletions: '0',\n      });\n    });\n  });\n\n  // see https://github.com/FredrikNoren/ungit/issues/1472\n  it.skip('diff on submodule should work', () => {\n    return common.get(req, '/diff', { path: testDirMain, file: submodulePath }).then((res) => {\n      expect(res.indexOf('-Subproject commit')).to.be.above(-1);\n      expect(res.indexOf('+Subproject commit')).to.be.above(-1);\n    });\n  });\n});\n"
  },
  {
    "path": "test/spec.git-parser.js",
    "content": "const expect = require('expect.js');\nconst path = require('path');\nconst gitParser = require('../source/git-parser');\nconst dedent = require('dedent');\n\ndescribe('git-parser stash show', () => {\n  it('should be possible to parse stashed show', () => {\n    const text = ' New Text Document (2).txt | 5 +++++\\n 1 file changed, 5 insertions(+)\\n';\n    const res = gitParser.parseGitStashShow(text);\n    expect(res).to.be.an('array');\n    expect(res.length).to.be(1);\n    expect(res[0]).to.eql({ filename: 'New Text Document (2).txt' });\n  });\n});\n\ndescribe('git-parser parseDiffResult', () => {\n  it('all diff selected', () => {\n    const gitDiff = dedent`\n      diff --git a/package.json b/package.json\n      index f71e0064..08964575 100644\n      --- a/package.json\n      +++ b/package.json\n      @@ -87,9 +87,10 @@\n        \"grunt-mocha-test\": \"~0.13.3\",\n        \"grunt-plato\": \"~1.4.0\",\n        \"grunt-release\": \"~0.14.0\",\n      - \"istanbul\": \"~0.4.5\",\n      + \"istanbul\": \"^0.4.5\",\n        \"mocha\": \"~5.2.0\",\n        \"nightmare\": \"~3.0.1\",\n      + \"nyc\": \"^13.1.0\",\n        \"supertest\": \"~3.3.0\"\n    `;\n\n    expect(gitParser.parsePatchDiffResult([true, true, true], gitDiff)).to.eql(dedent`\n      diff --git a/package.json b/package.json\n      index f71e0064..08964575 100644\n      --- a/package.json\n      +++ b/package.json\n      @@ -87,9 +87,10 @@\n        \"grunt-mocha-test\": \"~0.13.3\",\n        \"grunt-plato\": \"~1.4.0\",\n        \"grunt-release\": \"~0.14.0\",\n      - \"istanbul\": \"~0.4.5\",\n      + \"istanbul\": \"^0.4.5\",\n        \"mocha\": \"~5.2.0\",\n        \"nightmare\": \"~3.0.1\",\n      + \"nyc\": \"^13.1.0\",\n        \"supertest\": \"~3.3.0\"\n    `);\n  });\n\n  it('no diff selected', () => {\n    const gitDiff = dedent`\n      diff --git a/package.json b/package.json\n      index f71e0064..08964575 100644\n      --- a/package.json\n      +++ b/package.json\n      @@ -87,9 +87,10 @@\n        \"grunt-mocha-test\": \"~0.13.3\",\n        \"grunt-plato\": \"~1.4.0\",\n        \"grunt-release\": \"~0.14.0\",\n      - \"istanbul\": \"~0.4.5\",\n      + \"istanbul\": \"^0.4.5\",\n        \"mocha\": \"~5.2.0\",\n        \"nightmare\": \"~3.0.1\",\n      + \"nyc\": \"^13.1.0\",\n        \"supertest\": \"~3.3.0\"\n    `;\n\n    expect(gitParser.parsePatchDiffResult([false, false, false], gitDiff)).to.eql(null);\n  });\n\n  it('one +- diff selected', () => {\n    const gitDiff = dedent`\n      diff --git a/package.json b/package.json\n      index f71e0064..08964575 100644\n      --- a/package.json\n      +++ b/package.json\n      @@ -87,9 +87,10 @@\n      \t\"grunt-mocha-test\": \"~0.13.3\",\n      \t\"grunt-plato\": \"~1.4.0\",\n      \t\"grunt-release\": \"~0.14.0\",\n      -\t\"istanbul\": \"~0.4.5\",\n      +\t\"istanbul\": \"^0.4.5\",\n      \t\"mocha\": \"~5.2.0\",\n      \t\"nightmare\": \"~3.0.1\",\n      +\t\"nyc\": \"^13.1.0\",\n      \t\"supertest\": \"~3.3.0\"\n    `;\n\n    expect(gitParser.parsePatchDiffResult([true, true, false], gitDiff)).to.eql(dedent`\n      diff --git a/package.json b/package.json\n      index f71e0064..08964575 100644\n      --- a/package.json\n      +++ b/package.json\n      @@ -87,9 +87,9 @@\n      \t\"grunt-mocha-test\": \"~0.13.3\",\n      \t\"grunt-plato\": \"~1.4.0\",\n      \t\"grunt-release\": \"~0.14.0\",\n      -\t\"istanbul\": \"~0.4.5\",\n      +\t\"istanbul\": \"^0.4.5\",\n      \t\"mocha\": \"~5.2.0\",\n      \t\"nightmare\": \"~3.0.1\",\n      \t\"supertest\": \"~3.3.0\"\n    `);\n  });\n\n  it('only one + diff selected', () => {\n    const gitDiff = dedent`\n      diff --git a/package.json b/package.json\n      index f71e0064..08964575 100644\n      --- a/package.json\n      +++ b/package.json\n      @@ -87,9 +87,10 @@\n      \t\"grunt-mocha-test\": \"~0.13.3\",\n      \t\"grunt-plato\": \"~1.4.0\",\n      \t\"grunt-release\": \"~0.14.0\",\n      -\t\"istanbul\": \"~0.4.5\",\n      +\t\"istanbul\": \"^0.4.5\",\n      \t\"mocha\": \"~5.2.0\",\n      \t\"nightmare\": \"~3.0.1\",\n      +\t\"nyc\": \"^13.1.0\",\n      \t\"supertest\": \"~3.3.0\"\n    `;\n\n    expect(gitParser.parsePatchDiffResult([false, false, true], gitDiff)).to.eql(\n      'diff --git a/package.json b/package.json\\nindex f71e0064..08964575 100644\\n--- a/package.json\\n+++ b/package.json\\n@@ -87,9 +87,10 @@\\n\\t\"grunt-mocha-test\": \"~0.13.3\",\\n\\t\"grunt-plato\": \"~1.4.0\",\\n\\t\"grunt-release\": \"~0.14.0\",\\n \\t\"istanbul\": \"~0.4.5\",\\n\\t\"mocha\": \"~5.2.0\",\\n\\t\"nightmare\": \"~3.0.1\",\\n+\\t\"nyc\": \"^13.1.0\",\\n\\t\"supertest\": \"~3.3.0\"'\n    );\n  });\n\n  it('works with multiple diffs', () => {\n    const gitDiff = dedent`\n      diff --git a/README.md b/README.md\n      index 96700c3a..dc141a51 100644\n      --- a/README.md\n      +++ b/README.md\n      @@ -1,4 +1,3 @@\n      -ungit\n      ======\n      [![NPM version](https://badge.fury.io/js/ungit.svg)](http://badge.fury.io/js/ungit)\n      [![Build Status](https://travis-ci.org/FredrikNoren/ungit.svg)](https://travis-ci.org/FredrikNoren/ungit)\n      @@ -133,7 +132,6 @@ Changelog\n      See [CHANGELOG.md](CHANGELOG.md).\n\n      -License (MIT)\n      See [LICENSE.md](LICENSE.md). To read about the Faircode experiment go to [#974](https://github.com/FredrikNoren/ungit/issues/974). Ungit is now once again MIT.\n    `;\n\n    expect(gitParser.parsePatchDiffResult([true, false], gitDiff)).to.eql(\n      'diff --git a/README.md b/README.md\\nindex 96700c3a..dc141a51 100644\\n--- a/README.md\\n+++ b/README.md\\n@@ -1,4 +1,3 @@\\n-ungit\\n======\\n[![NPM version](https://badge.fury.io/js/ungit.svg)](http://badge.fury.io/js/ungit)\\n[![Build Status](https://travis-ci.org/FredrikNoren/ungit.svg)](https://travis-ci.org/FredrikNoren/ungit)\\n@@ -133,7 +132,7 @@ Changelog\\nSee [CHANGELOG.md](CHANGELOG.md).\\n\\n License (MIT)\\nSee [LICENSE.md](LICENSE.md). To read about the Faircode experiment go to [#974](https://github.com/FredrikNoren/ungit/issues/974). Ungit is now once again MIT.'\n    );\n  });\n\n  it('works with empty diff', () => {\n    expect(gitParser.parsePatchDiffResult([], null)).to.eql(null);\n  });\n});\n\ndescribe('git-parser parseGitLog', () => {\n  it('should work with branch name with ()', () => {\n    const refs = gitParser.parseGitLog('commit AAA BBB (HEAD, (test), fw(4rw), 5), ((, ()')[0].refs;\n    expect(refs.length).to.be(6);\n  });\n\n  it('should work with no branch name', () => {\n    const refs = gitParser.parseGitLog('commit AAA BBB')[0].refs;\n    expect(refs.length).to.be(0);\n  });\n\n  it('should work with empty lines', () => {\n    expect(gitParser.parseGitLog('')).to.eql([]);\n  });\n\n  it('parses authors without emails', () => {\n    const gitLog = dedent`\n      commit 37d1154434b70854ed243967e0d7e37aa3564551 d58c8e117fc257520d90b099fd2c6acd7c1e8861 (HEAD -> refs/heads/git-parser-specs)\n      Author:     Test ungit\n      Commit:     Test ungit\n    `;\n\n    expect(gitParser.parseGitLog(gitLog)[0]).to.eql({\n      authorName: 'Test ungit',\n      committerName: 'Test ungit',\n      additions: 0,\n      deletions: 0,\n      fileLineDiffs: [],\n      isHead: true,\n      message: '',\n      parents: ['d58c8e117fc257520d90b099fd2c6acd7c1e8861'],\n      refs: ['HEAD', 'refs/heads/git-parser-specs'],\n      sha1: '37d1154434b70854ed243967e0d7e37aa3564551',\n    });\n  });\n\n  it('parses multiple commits in a row', () => {\n    const gitLog = dedent(`\n      commit 5867e2766b0a0f81ad59ce9e9895d9b1a3523aa4 37d1154434b70854ed243967e0d7e37aa3564551 (HEAD -> refs/heads/git-parser-specs)\n      Author:     Test ungit <test@example.com>\n      AuthorDate: Fri Jan 4 14:54:06 2019 +0100\n      Commit:     Test ungit <test@example.com>\n      CommitDate: Fri Jan 4 14:54:06 2019 +0100\n\n        parseGitLog + gix reflox parsing\n\n      1\t1\tsource/git-parser.js\\x00175\t0\ttest/spec.git-parser.js\\x00\\x00commit 37d1154434b70854ed243967e0d7e37aa3564551 d58c8e117fc257520d90b099fd2c6acd7c1e8861\n      Author:     Test ungit <test@example.com>\n      AuthorDate: Fri Jan 4 14:03:56 2019 +0100\n      Commit:     Test ungit <test@example.com>\n      CommitDate: Fri Jan 4 14:03:56 2019 +0100\n\n        submodules parser\n\n      32\t0\ttest/spec.git-parser.js\\x00\\x00commit 02efa0da7b1eccb1e0f1c2ff0433ce7387738f60 985617e19e30e9abe0a5711bf455f0dc10f97dff\n      Author:     Test ungit <test@example.com>\n      AuthorDate: Fri Jan 4 14:02:56 2019 +0100\n      Commit:     Test ungit <test@example.com>\n      CommitDate: Fri Jan 4 14:02:56 2019 +0100\n      \n          empty commit\n      \\x00commit 621a04f931ea9007ac826c04a1a02832e20aa470 4e5d611fdad85bcad44abf65936c95f748abef4e e2dc3ef6e2cbf6ab0acb456c0837257dc01baafd\n      Merge: 4e5d611f e2dc3ef6\n      Author:     Test ungit <test@example.com>\n      AuthorDate: Fri Jan 4 14:01:56 2019 +0100\n      Commit:     Test ungit <test@example.com>\n      CommitDate: Fri Jan 4 14:01:56 2019 +0100\n      \n          Merge pull request #1268 from campersau/prepare_152\n          \n          Prepare version 1.5.2\n      \\x004\t1\tCHANGELOG.md\\x001\t1\tpackage-lock.json\\x001\t1\tpackage.json\\x008\t6\tsource/git-parser.js\\x00\\x00`);\n\n    const res = gitParser.parseGitLog(gitLog);\n    expect(res[0]).to.eql({\n      authorDate: 'Fri Jan 4 14:54:06 2019 +0100',\n      authorEmail: 'test@example.com',\n      authorName: 'Test ungit',\n      commitDate: 'Fri Jan 4 14:54:06 2019 +0100',\n      committerEmail: 'test@example.com',\n      committerName: 'Test ungit',\n      additions: 176,\n      deletions: 1,\n      fileLineDiffs: [\n        {\n          additions: 1,\n          deletions: 1,\n          displayName: 'source/git-parser.js',\n          fileName: 'source/git-parser.js',\n          oldFileName: 'source/git-parser.js',\n          type: 'text',\n        },\n        {\n          additions: 175,\n          deletions: 0,\n          displayName: 'test/spec.git-parser.js',\n          fileName: 'test/spec.git-parser.js',\n          oldFileName: 'test/spec.git-parser.js',\n          type: 'text',\n        },\n      ],\n      isHead: true,\n      message: 'parseGitLog + gix reflox parsing',\n      parents: ['37d1154434b70854ed243967e0d7e37aa3564551'],\n      refs: ['HEAD', 'refs/heads/git-parser-specs'],\n      sha1: '5867e2766b0a0f81ad59ce9e9895d9b1a3523aa4',\n    });\n    expect(res[1]).to.eql({\n      authorDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      authorEmail: 'test@example.com',\n      authorName: 'Test ungit',\n      commitDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      committerEmail: 'test@example.com',\n      committerName: 'Test ungit',\n      additions: 32,\n      deletions: 0,\n      fileLineDiffs: [\n        {\n          additions: 32,\n          deletions: 0,\n          displayName: 'test/spec.git-parser.js',\n          fileName: 'test/spec.git-parser.js',\n          oldFileName: 'test/spec.git-parser.js',\n          type: 'text',\n        },\n      ],\n      isHead: false,\n      message: 'submodules parser',\n      parents: ['d58c8e117fc257520d90b099fd2c6acd7c1e8861'],\n      refs: [],\n      sha1: '37d1154434b70854ed243967e0d7e37aa3564551',\n    });\n    // empty commit\n    expect(res[2]).to.eql({\n      authorDate: 'Fri Jan 4 14:02:56 2019 +0100',\n      authorEmail: 'test@example.com',\n      authorName: 'Test ungit',\n      commitDate: 'Fri Jan 4 14:02:56 2019 +0100',\n      committerEmail: 'test@example.com',\n      committerName: 'Test ungit',\n      additions: 0,\n      deletions: 0,\n      fileLineDiffs: [],\n      isHead: false,\n      message: 'empty commit',\n      parents: ['985617e19e30e9abe0a5711bf455f0dc10f97dff'],\n      refs: [],\n      sha1: '02efa0da7b1eccb1e0f1c2ff0433ce7387738f60',\n    });\n    // merge commit\n    expect(res[3]).to.eql({\n      authorDate: 'Fri Jan 4 14:01:56 2019 +0100',\n      authorEmail: 'test@example.com',\n      authorName: 'Test ungit',\n      commitDate: 'Fri Jan 4 14:01:56 2019 +0100',\n      committerEmail: 'test@example.com',\n      committerName: 'Test ungit',\n      additions: 14,\n      deletions: 9,\n      fileLineDiffs: [\n        {\n          additions: 4,\n          deletions: 1,\n          displayName: 'CHANGELOG.md',\n          fileName: 'CHANGELOG.md',\n          oldFileName: 'CHANGELOG.md',\n          type: 'text',\n        },\n        {\n          additions: 1,\n          deletions: 1,\n          displayName: 'package-lock.json',\n          fileName: 'package-lock.json',\n          oldFileName: 'package-lock.json',\n          type: 'text',\n        },\n        {\n          additions: 1,\n          deletions: 1,\n          displayName: 'package.json',\n          fileName: 'package.json',\n          oldFileName: 'package.json',\n          type: 'text',\n        },\n        {\n          additions: 8,\n          deletions: 6,\n          displayName: 'source/git-parser.js',\n          fileName: 'source/git-parser.js',\n          oldFileName: 'source/git-parser.js',\n          type: 'text',\n        },\n      ],\n      isHead: false,\n      message: 'Merge pull request #1268 from campersau/prepare_152\\n\\nPrepare version 1.5.2',\n      parents: [\n        '4e5d611fdad85bcad44abf65936c95f748abef4e',\n        'e2dc3ef6e2cbf6ab0acb456c0837257dc01baafd',\n      ],\n      refs: [],\n      sha1: '621a04f931ea9007ac826c04a1a02832e20aa470',\n    });\n  });\n\n  it('parses multiple commits in a row multiple nul separators', () => {\n    const gitLog = dedent(`\n      commit ad4c559f05796e78095a51679324cefd9afca879 47185090d5096033db0d5c0bbf883d9295ca084e b360295026ae6afac3525b89145aa22d61e818ff (HEAD -> refs/heads/dev)\n      Merge: 4718509 b360295\n      Author:     Ungit Commiter <ungit.commiter@example.com>\n      AuthorDate: Sat May 22 22:21:04 2021 +0200\n      Commit:     Ungit Commiter <ungit.commiter@example.com>\n      CommitDate: Sat May 22 22:21:04 2021 +0200\n\n          Merge branch 'a' into dev\n      \\x00\\x00commit 7d7a4d7d9fc625aff46a0ff4d7e95f86d01d25c7 47185090d5096033db0d5c0bbf883d9295ca084e (refs/heads/b)\n      Author:     Ungit Commiter <ungit.commiter@example.com>\n      AuthorDate: Sat May 22 22:20:28 2021 +0200\n      Commit:     Ungit Commiter <ungit.commiter@example.com>\n      CommitDate: Sat May 22 22:20:28 2021 +0200\n\n          b\n      \\x00commit b360295026ae6afac3525b89145aa22d61e818ff 47185090d5096033db0d5c0bbf883d9295ca084e (refs/heads/a)\n      Author:     Ungit Commiter <ungit.commiter@example.com>\n      AuthorDate: Sat May 22 22:20:23 2021 +0200\n      Commit:     Ungit Commiter <ungit.commiter@example.com>\n      CommitDate: Sat May 22 22:20:23 2021 +0200\n\n          a\n      \\x00commit 47185090d5096033db0d5c0bbf883d9295ca084e (refs/heads/master)\n      Author:     Ungit Commiter <ungit.commiter@example.com>\n      AuthorDate: Sat May 22 22:19:31 2021 +0200\n      Commit:     Ungit Commiter <ungit.commiter@example.com>\n      CommitDate: Sat May 22 22:19:31 2021 +0200\n\n          Initial commit`);\n\n    const res = gitParser.parseGitLog(gitLog);\n    expect(res.length).to.eql(4);\n    expect(res[0].message).to.eql(\"Merge branch 'a' into dev\");\n    expect(res[1].message).to.eql('b');\n    expect(res[2].message).to.eql('a');\n    expect(res[3].message).to.eql('Initial commit');\n  });\n\n  it('parses reflog commits without email', () => {\n    const gitLog = dedent(`\n      commit 37d11544 d58c8e11 (HEAD -> refs/heads/git-parser-specs)\n      Reflog: git-parser-specs@{Fri Jan 4 14:03:56 2019 +0100} (Test ungit)\n      Reflog message: commit: submodules parser\n      Author:     Test ungit <test@example.com>\n      AuthorDate: Fri Jan 4 14:03:56 2019 +0100\n      Commit:     Test ungit <test@example.com>\n      CommitDate: Fri Jan 4 14:03:56 2019 +0100\n\n          submodules parser\n\n      32\t0\ttest/spec.git-parser.js\\x00\\x00`);\n\n    expect(gitParser.parseGitLog(gitLog)[0]).to.eql({\n      authorDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      authorEmail: 'test@example.com',\n      authorName: 'Test ungit',\n      commitDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      committerEmail: 'test@example.com',\n      committerName: 'Test ungit',\n      additions: 32,\n      deletions: 0,\n      fileLineDiffs: [\n        {\n          additions: 32,\n          deletions: 0,\n          displayName: 'test/spec.git-parser.js',\n          fileName: 'test/spec.git-parser.js',\n          oldFileName: 'test/spec.git-parser.js',\n          type: 'text',\n        },\n      ],\n      isHead: true,\n      message: 'submodules parser',\n      parents: ['d58c8e11'],\n      reflogAuthorName: 'Test ungit',\n      reflogId: 'Fri Jan 4 14:03:56 2019 +0100',\n      reflogName: 'git-parser-specs@{Fri',\n      refs: ['HEAD', 'refs/heads/git-parser-specs'],\n      sha1: '37d11544',\n    });\n  });\n\n  it('parses reflog commits', () => {\n    const gitLog = dedent(`\n      commit 37d11544 d58c8e11 (HEAD -> refs/heads/git-parser-specs)\n      Reflog: git-parser-specs@{Fri Jan 4 14:03:56 2019 +0100} (Test ungit <test@example.com>)\n      Reflog message: commit: submodules parser\n      Author:     Test ungit <test@example.com>\n      AuthorDate: Fri Jan 4 14:03:56 2019 +0100\n      Commit:     Test ungit <test@example.com>\n      CommitDate: Fri Jan 4 14:03:56 2019 +0100\n\n          submodules parser\n\n      32\t0\ttest/spec.git-parser.js\\x00\\x00`);\n\n    expect(gitParser.parseGitLog(gitLog)[0]).to.eql({\n      authorDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      authorEmail: 'test@example.com',\n      authorName: 'Test ungit',\n      commitDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      committerEmail: 'test@example.com',\n      committerName: 'Test ungit',\n      additions: 32,\n      deletions: 0,\n      fileLineDiffs: [\n        {\n          additions: 32,\n          deletions: 0,\n          displayName: 'test/spec.git-parser.js',\n          fileName: 'test/spec.git-parser.js',\n          oldFileName: 'test/spec.git-parser.js',\n          type: 'text',\n        },\n      ],\n      isHead: true,\n      message: 'submodules parser',\n      parents: ['d58c8e11'],\n      reflogAuthorEmail: 'test@example.com',\n      reflogAuthorName: 'Test ungit',\n      reflogId: 'Fri Jan 4 14:03:56 2019 +0100',\n      reflogName: 'git-parser-specs@{Fri',\n      refs: ['HEAD', 'refs/heads/git-parser-specs'],\n      sha1: '37d11544',\n    });\n  });\n\n  it('parses wrongly signed commits', () => {\n    const gitLog = dedent`\n      commit 37d1154434b70854ed243967e0d7e37aa3564551 d58c8e117fc257520d90b099fd2c6acd7c1e8861 (HEAD -> refs/heads/git-parser-specs)\n      gpg: Signature made Wed Jun  4 19:49:17 2014 PDT using RSA key ID 0AAAAAAA\n      gpg: Can't check signature: public key not found\n      Author: Test Ungit <test@example.com>\n      Date:   Wed Jun 4 19:49:17 2014 -0700\n      signed commit\n    `;\n\n    expect(gitParser.parseGitLog(gitLog)[0]).to.eql({\n      authorEmail: 'test@example.com',\n      authorName: 'Test Ungit',\n      additions: 0,\n      deletions: 0,\n      fileLineDiffs: [],\n      isHead: true,\n      message: '',\n      parents: ['d58c8e117fc257520d90b099fd2c6acd7c1e8861'],\n      refs: ['HEAD', 'refs/heads/git-parser-specs'],\n      sha1: '37d1154434b70854ed243967e0d7e37aa3564551',\n    });\n  });\n\n  it('parses signed commits', () => {\n    const gitLog = dedent`\n      commit 37d1154434b70854ed243967e0d7e37aa3564551 d58c8e117fc257520d90b099fd2c6acd7c1e8861 (HEAD -> refs/heads/git-parser-specs)\n      gpg: Signature made Wed Jun  4 19:49:17 2014 PDT using RSA key ID 0AAAAAAA\n      gpg: Good signature from \"Test ungit (Git signing key) <test@example.com>\"\n      Author: Test Ungit <test@example.com>\n      Date:   Wed Jun 4 19:49:17 2014 -0700\n      signed commit\n    `;\n\n    expect(gitParser.parseGitLog(gitLog)[0]).to.eql({\n      authorEmail: 'test@example.com',\n      authorName: 'Test Ungit',\n      additions: 0,\n      deletions: 0,\n      fileLineDiffs: [],\n      isHead: true,\n      message: '',\n      parents: ['d58c8e117fc257520d90b099fd2c6acd7c1e8861'],\n      refs: ['HEAD', 'refs/heads/git-parser-specs'],\n      sha1: '37d1154434b70854ed243967e0d7e37aa3564551',\n      signatureDate: 'Wed Jun  4 19:49:17 2014 PDT using RSA key ID 0AAAAAAA',\n      signatureMade: '\"Test ungit (Git signing key) <test@example.com>\"',\n    });\n  });\n\n  it('parses the git log', () => {\n    const gitLog = dedent(`\n      commit 37d1154434b70854ed243967e0d7e37aa3564551 d58c8e117fc257520d90b099fd2c6acd7c1e8861 (HEAD -> refs/heads/git-parser-specs)\n      Author:     Test ungit <test@example.com>\n      AuthorDate: Fri Jan 4 14:03:56 2019 +0100\n      Commit:     Test ungit <test@example.com>\n      CommitDate: Fri Jan 4 14:03:56 2019 +0100\n\n          submodules parser\n\n      32\t0\ttest/spec.git-parser.js\\x00\\x00`);\n\n    expect(gitParser.parseGitLog(gitLog)[0]).to.eql({\n      refs: ['HEAD', 'refs/heads/git-parser-specs'],\n      additions: 32,\n      deletions: 0,\n      fileLineDiffs: [\n        {\n          additions: 32,\n          deletions: 0,\n          displayName: 'test/spec.git-parser.js',\n          fileName: 'test/spec.git-parser.js',\n          oldFileName: 'test/spec.git-parser.js',\n          type: 'text',\n        },\n      ],\n      sha1: '37d1154434b70854ed243967e0d7e37aa3564551',\n      parents: ['d58c8e117fc257520d90b099fd2c6acd7c1e8861'],\n      isHead: true,\n      authorName: 'Test ungit',\n      authorEmail: 'test@example.com',\n      authorDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      committerName: 'Test ungit',\n      committerEmail: 'test@example.com',\n      commitDate: 'Fri Jan 4 14:03:56 2019 +0100',\n      message: 'submodules parser',\n    });\n  });\n});\n\ndescribe('git-parser submodule', () => {\n  it('should work with empty string', () => {\n    const gitmodules = '';\n    const submodules = gitParser.parseGitSubmodule(gitmodules);\n    expect(submodules).to.eql([]);\n  });\n\n  it('should work with name, path and url', () => {\n    const gitmodules = '[submodule \"test1\"]\\npath = /path/to/sub1\\nurl = http://example1.com';\n    const submodules = gitParser.parseGitSubmodule(gitmodules);\n    expect(submodules.length).to.be(1);\n    expect(submodules[0]).to.eql({\n      name: 'test1',\n      path: path.join('/path', 'to', 'sub1'),\n      rawUrl: 'http://example1.com',\n      url: 'http://example1.com',\n    });\n  });\n\n  it('should work with multiple name, path and url', () => {\n    const gitmodules = [\n      '[submodule \"test1\"]\\npath = /path/to/sub1\\nurl = http://example1.com',\n      '[submodule \"test2\"]\\npath = /path/to/sub2\\nurl = http://example2.com',\n    ].join('\\n');\n    const submodules = gitParser.parseGitSubmodule(gitmodules);\n    expect(submodules.length).to.be(2);\n    expect(submodules[0]).to.eql({\n      name: 'test1',\n      path: path.join('/path', 'to', 'sub1'),\n      rawUrl: 'http://example1.com',\n      url: 'http://example1.com',\n    });\n    expect(submodules[1]).to.eql({\n      name: 'test2',\n      path: path.join('/path', 'to', 'sub2'),\n      rawUrl: 'http://example2.com',\n      url: 'http://example2.com',\n    });\n  });\n\n  it('should work with multiple name, path, url, update, branch, fetchRecurseSubmodules and ignore', () => {\n    const gitmodules = [\n      '[submodule \"test1\"]\\npath = /path/to/sub1\\nurl = http://example1.com\\nupdate = checkout\\nbranch = master\\nfetchRecurseSubmodules = true\\nignore = all',\n      '[submodule  \"test2\"]\\n\\npath   = /path/to/sub2\\nurl= git://example2.com',\n    ].join('\\n');\n    const submodules = gitParser.parseGitSubmodule(gitmodules);\n    expect(submodules.length).to.be(2);\n    expect(submodules[0]).to.eql({\n      branch: 'master',\n      fetchRecurseSubmodules: 'true',\n      ignore: 'all',\n      name: 'test1',\n      path: path.join('/path', 'to', 'sub1'),\n      rawUrl: 'http://example1.com',\n      update: 'checkout',\n      url: 'http://example1.com',\n    });\n    expect(submodules[1]).to.eql({\n      name: 'test2',\n      path: path.join('/path', 'to', 'sub2'),\n      rawUrl: 'git://example2.com',\n      url: 'http://example2.com',\n    });\n  });\n\n  it('should work with git submodules', () => {\n    const gitmodules = dedent`\n      [submodule \"test1\"]\n      path = /path/to/sub1\n      url = git://example1.com\n      update = checkout\n      branch = master\n      fetchRecurseSubmodules = true\n      ignore = all\n    `;\n\n    expect(gitParser.parseGitSubmodule(gitmodules)).to.eql([\n      {\n        name: 'test1',\n        path: path.join('/path', 'to', 'sub1'),\n        rawUrl: 'git://example1.com',\n        url: 'http://example1.com',\n        update: 'checkout',\n        branch: 'master',\n        fetchRecurseSubmodules: 'true',\n        ignore: 'all',\n      },\n    ]);\n  });\n\n  it('should work with ssh submodules', () => {\n    const gitmodules = dedent`\n      [submodule \"test1\"]\n      path = /path/to/sub1\n      url = ssh://login@server.com:12345\n      update = checkout\n      branch = master\n      fetchRecurseSubmodules = true\n      ignore = all\n    `;\n\n    expect(gitParser.parseGitSubmodule(gitmodules)).to.eql([\n      {\n        name: 'test1',\n        path: path.join('/path', 'to', 'sub1'),\n        rawUrl: 'ssh://login@server.com:12345',\n        url: 'http://server.com/12345',\n        update: 'checkout',\n        branch: 'master',\n        fetchRecurseSubmodules: 'true',\n        ignore: 'all',\n      },\n    ]);\n  });\n});\n\ndescribe('parseGitConfig', () => {\n  it('parses the git config', () => {\n    const gitConfig = dedent`\n      user.email=test@example.com\n      user.name=Ungit Test\n      core.repositoryformatversion=0\n      core.filemode=true\n      core.bare=false\n      core.logallrefupdates=true\n      remote.origin.url=git@github.com:ungit/ungit.git\n      branch.master.remote=origin\n      branch.master.merge=refs/heads/master\n    `;\n\n    expect(gitParser.parseGitConfig(gitConfig)).to.eql({\n      'user.email': 'test@example.com',\n      'user.name': 'Ungit Test',\n      'core.repositoryformatversion': '0',\n      'core.filemode': 'true',\n      'core.bare': 'false',\n      'core.logallrefupdates': 'true',\n      'remote.origin.url': 'git@github.com:ungit/ungit.git',\n      'branch.master.remote': 'origin',\n      'branch.master.merge': 'refs/heads/master',\n    });\n  });\n});\n\ndescribe('parseGitBranches', () => {\n  it('parses the branches', () => {\n    const gitBranches = dedent`\n      * dev\n        master\n        testbuild\n    `;\n\n    expect(gitParser.parseGitBranches(gitBranches)).to.eql([\n      { name: 'dev', current: true },\n      { name: 'master' },\n      { name: 'testbuild' },\n    ]);\n  });\n});\n\ndescribe('parseGitTags', () => {\n  it('parses the tags', () => {\n    const gitTags = dedent`\n      0.1.0\n      0.1.1\n      0.1.2\n    `;\n\n    expect(gitParser.parseGitTags(gitTags)).to.eql(['0.1.0', '0.1.1', '0.1.2']);\n  });\n});\n\ndescribe('parseGitRemotes', () => {\n  it('parses the remotes', () => {\n    const gitRemotes = dedent`\n      origin\n      upstream\n    `;\n\n    expect(gitParser.parseGitRemotes(gitRemotes)).to.eql([\n      { name: 'origin' },\n      { name: 'upstream' },\n    ]);\n  });\n\n  it('parses the remotes with fetch and push url', () => {\n    const gitRemotes = dedent`\n      origin\thttp://example1.com\n      upstream\thttp://example2.com (fetch)\n      upstream\thttp://example3.com (push)\n    `;\n\n    expect(gitParser.parseGitRemotes(gitRemotes)).to.eql([\n      { name: 'origin', url: 'http://example1.com' },\n      { name: 'upstream', fetchUrl: 'http://example2.com', pushUrl: 'http://example3.com' },\n    ]);\n  });\n});\n\ndescribe('parseGitLsRemote', () => {\n  it('parses the ls remote', () => {\n    const gitLsRemote = dedent`\n      86bec6415fa7ec0d7550a62389de86adb493d546\trefs/tags/0.1.0\n      668ab7beae996c5a7b36da0be64b98e45ba2aa0b\trefs/tags/0.1.0^{}\n      d3ec9678acf285637ef11c7cba897d697820de07\trefs/tags/0.1.1\n      ad00b6c8b7b0cbdd0bd92d44dece559b874a4ae6\trefs/tags/0.1.1^{}\n    `;\n\n    expect(gitParser.parseGitLsRemote(gitLsRemote)).to.eql([\n      { sha1: '86bec6415fa7ec0d7550a62389de86adb493d546', name: 'refs/tags/0.1.0' },\n      { sha1: '668ab7beae996c5a7b36da0be64b98e45ba2aa0b', name: 'refs/tags/0.1.0^{}' },\n      { sha1: 'd3ec9678acf285637ef11c7cba897d697820de07', name: 'refs/tags/0.1.1' },\n      { sha1: 'ad00b6c8b7b0cbdd0bd92d44dece559b874a4ae6', name: 'refs/tags/0.1.1^{}' },\n    ]);\n  });\n});\n\ndescribe('parseGitStatusNumstat', () => {\n  it('parses the git status numstat', () => {\n    const gitStatusNumstat =\n      '1459\t202\tpackage-lock.json\\x002\t1\tpackage.json\\x0013\t0\ttest/spec.git-parser.js\\x00';\n\n    expect(gitParser.parseGitStatusNumstat(gitStatusNumstat)).to.eql({\n      'package-lock.json': { additions: '1459', deletions: '202' },\n      'package.json': { additions: '2', deletions: '1' },\n      'test/spec.git-parser.js': { additions: '13', deletions: '0' },\n    });\n  });\n\n  it('skips empty lines', () => {\n    const gitStatusNumstat = dedent(`\n      1459\t202\tpackage-lock.json\\x00\n\n\n      2\t1\tpackage.json\\x0013\t0\ttest/spec.git-parser.js\\x00\n    `);\n\n    expect(gitParser.parseGitStatusNumstat(gitStatusNumstat)).to.eql({\n      'package-lock.json': { additions: '1459', deletions: '202' },\n      'package.json': { additions: '2', deletions: '1' },\n      'test/spec.git-parser.js': { additions: '13', deletions: '0' },\n    });\n  });\n});\n\ndescribe('parseGitStatus', () => {\n  it('parses git status', () => {\n    const gitStatus =\n      '## git-parser-specs\\x00' +\n      'A  file1.js\\x00' +\n      'M  file2.js\\x00' +\n      'D  file3.js\\x00' +\n      ' D file4.js\\x00' +\n      ' U file5.js\\x00' +\n      'U  file6.js\\x00' +\n      'AA file7.js\\x00' +\n      '?  file8.js\\x00' +\n      'A  file9.js\\x00' +\n      '?D file10.js\\x00' +\n      'AD file11.js\\x00' +\n      ' M file12.js\\x00' +\n      '?? file13.js\\x00' +\n      'R  ../source/sys.js\\x00../source/sysinfo.js\\x00';\n\n    expect(gitParser.parseGitStatus(gitStatus)).to.eql({\n      branch: 'git-parser-specs',\n      files: {\n        '../source/sys.js': {\n          conflict: false,\n          displayName: '../source/sysinfo.js → ../source/sys.js',\n          fileName: '../source/sys.js',\n          oldFileName: '../source/sysinfo.js',\n          isNew: false,\n          removed: false,\n          renamed: true,\n          staged: false,\n          type: 'text',\n        },\n        'file1.js': {\n          conflict: false,\n          displayName: 'file1.js',\n          fileName: 'file1.js',\n          oldFileName: 'file1.js',\n          isNew: true,\n          removed: false,\n          renamed: false,\n          staged: true,\n          type: 'text',\n        },\n        'file2.js': {\n          conflict: false,\n          displayName: 'file2.js',\n          fileName: 'file2.js',\n          oldFileName: 'file2.js',\n          isNew: false,\n          removed: false,\n          renamed: false,\n          staged: true,\n          type: 'text',\n        },\n        'file3.js': {\n          conflict: false,\n          displayName: 'file3.js',\n          fileName: 'file3.js',\n          oldFileName: 'file3.js',\n          isNew: false,\n          removed: true,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n        'file4.js': {\n          conflict: false,\n          displayName: 'file4.js',\n          fileName: 'file4.js',\n          oldFileName: 'file4.js',\n          isNew: false,\n          removed: true,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n        'file5.js': {\n          conflict: true,\n          displayName: 'file5.js',\n          fileName: 'file5.js',\n          oldFileName: 'file5.js',\n          isNew: false,\n          removed: false,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n        'file6.js': {\n          conflict: true,\n          displayName: 'file6.js',\n          fileName: 'file6.js',\n          oldFileName: 'file6.js',\n          isNew: false,\n          removed: false,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n        'file7.js': {\n          conflict: true,\n          displayName: 'file7.js',\n          fileName: 'file7.js',\n          oldFileName: 'file7.js',\n          isNew: true,\n          removed: false,\n          renamed: false,\n          staged: true,\n          type: 'text',\n        },\n        'file8.js': {\n          conflict: false,\n          displayName: 'file8.js',\n          fileName: 'file8.js',\n          oldFileName: 'file8.js',\n          isNew: true,\n          removed: false,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n        'file9.js': {\n          conflict: false,\n          displayName: 'file9.js',\n          fileName: 'file9.js',\n          oldFileName: 'file9.js',\n          isNew: true,\n          removed: false,\n          renamed: false,\n          staged: true,\n          type: 'text',\n        },\n        'file10.js': {\n          conflict: false,\n          displayName: 'file10.js',\n          fileName: 'file10.js',\n          oldFileName: 'file10.js',\n          isNew: false,\n          removed: true,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n        'file11.js': {\n          conflict: false,\n          displayName: 'file11.js',\n          fileName: 'file11.js',\n          oldFileName: 'file11.js',\n          isNew: false,\n          removed: true,\n          renamed: false,\n          staged: true,\n          type: 'text',\n        },\n        'file12.js': {\n          conflict: false,\n          displayName: 'file12.js',\n          fileName: 'file12.js',\n          oldFileName: 'file12.js',\n          isNew: false,\n          removed: false,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n        'file13.js': {\n          conflict: false,\n          displayName: 'file13.js',\n          fileName: 'file13.js',\n          oldFileName: 'file13.js',\n          isNew: true,\n          removed: false,\n          renamed: false,\n          staged: false,\n          type: 'text',\n        },\n      },\n    });\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"forceConsistentCasingInFileNames\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"resolveJsonModule\": true,\n    \"downlevelIteration\": true,\n    \"skipLibCheck\": true,\n    \"module\": \"commonjs\",\n    // Check JS files too\n    \"allowJs\": true,\n    \"checkJs\": false,\n    // Used for temp builds\n    \"outDir\": \"build\",\n    \"paths\": {\n      \"mina\": [\"./node_modules/snapsvg/src/mina\"],\n      \"octicons\": [\"./node_modules/@primer/octicons\"],\n      \"ungit-address-parser\": [\"./source/address-parser\"],\n      \"ungit-components\": [\"./public/source/components\"],\n      \"ungit-main\": [\"./public/source/main\"],\n      \"ungit-navigation\": [\"./public/source/navigation\"],\n      \"ungit-program-events\": [\"./public/source/program-events\"],\n      \"ungit-storage\": [\"./public/source/storage\"]\n    }\n  },\n  \"exclude\": [\"node_modules\", \"build\", \"coverage\", \"public/js\", \"**/*.min.js\", \"**/*.bundle.js\", \"clicktests\", \"test\"]\n}\n"
  }
]