Full Code of FredrikNoren/ungit for AI

master 823076b5795c cached
175 files
664.9 KB
187.5k tokens
404 symbols
1 requests
Download .txt
Showing preview only (709K chars total). Download the full file or copy to clipboard to get everything.
Repository: FredrikNoren/ungit
Branch: master
Commit: 823076b5795c
Files: 175
Total size: 664.9 KB

Directory structure:
gitextract_do455as5/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── bump.yml
│       └── ci.yml
├── .gitignore
├── .mochaclicktest.json
├── .mochatest.json
├── .npmignore
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── MERGETOOL.md
├── PLUGINS.md
├── README.md
├── appveyor.yml
├── bin/
│   ├── credentials-helper
│   └── ungit
├── clicktests/
│   ├── environment.js
│   ├── spec.authentication.js
│   ├── spec.bare.js
│   ├── spec.branches.js
│   ├── spec.commands.js
│   ├── spec.discard.js
│   ├── spec.generic.js
│   ├── spec.load-ahead.js
│   ├── spec.no-header.js
│   ├── spec.remotes.js
│   ├── spec.screens.js
│   ├── spec.stash.js
│   └── spec.submodules.js
├── components/
│   ├── ComponentRoot.ts
│   ├── app/
│   │   ├── app.html
│   │   ├── app.js
│   │   ├── app.less
│   │   └── ungit-plugin.json
│   ├── branches/
│   │   ├── branches.html
│   │   ├── branches.js
│   │   ├── branches.less
│   │   └── ungit-plugin.json
│   ├── commit/
│   │   ├── commit.html
│   │   ├── commit.js
│   │   ├── commit.less
│   │   └── ungit-plugin.json
│   ├── commitdiff/
│   │   ├── commitdiff.html
│   │   ├── commitdiff.js
│   │   ├── commitdiff.less
│   │   ├── commitlinediff.js
│   │   └── ungit-plugin.json
│   ├── crash/
│   │   ├── crash.html
│   │   ├── crash.js
│   │   ├── crash.less
│   │   └── ungit-plugin.json
│   ├── gitErrors/
│   │   ├── gitErrors.html
│   │   ├── gitErrors.js
│   │   └── ungit-plugin.json
│   ├── graph/
│   │   ├── animateable.js
│   │   ├── edge.js
│   │   ├── git-graph-actions.js
│   │   ├── git-node.js
│   │   ├── git-ref.js
│   │   ├── graph-graphics.html
│   │   ├── graph.html
│   │   ├── graph.js
│   │   ├── graph.less
│   │   ├── hover-actions.js
│   │   ├── selectable.js
│   │   └── ungit-plugin.json
│   ├── header/
│   │   ├── header.html
│   │   ├── header.js
│   │   ├── header.less
│   │   └── ungit-plugin.json
│   ├── home/
│   │   ├── home.html
│   │   ├── home.js
│   │   ├── home.less
│   │   └── ungit-plugin.json
│   ├── imagediff/
│   │   ├── imagediff.html
│   │   ├── imagediff.js
│   │   ├── imagediff.less
│   │   └── ungit-plugin.json
│   ├── login/
│   │   ├── login.html
│   │   ├── login.js
│   │   └── ungit-plugin.json
│   ├── modals/
│   │   ├── formModal.html
│   │   ├── forms.ts
│   │   ├── modalBase.ts
│   │   ├── modals.ts
│   │   ├── promptModal.html
│   │   ├── prompts.ts
│   │   └── ungit-plugin.json
│   ├── path/
│   │   ├── path.html
│   │   ├── path.js
│   │   ├── path.less
│   │   └── ungit-plugin.json
│   ├── refreshbutton/
│   │   ├── refreshbutton.html
│   │   ├── refreshbutton.js
│   │   ├── refreshbutton.less
│   │   └── ungit-plugin.json
│   ├── remotes/
│   │   ├── remotes.html
│   │   ├── remotes.js
│   │   └── ungit-plugin.json
│   ├── repository/
│   │   ├── repository.html
│   │   ├── repository.js
│   │   ├── repository.less
│   │   └── ungit-plugin.json
│   ├── staging/
│   │   ├── staging.html
│   │   ├── staging.js
│   │   ├── staging.less
│   │   └── ungit-plugin.json
│   ├── stash/
│   │   ├── stash.html
│   │   ├── stash.js
│   │   ├── stash.less
│   │   └── ungit-plugin.json
│   ├── submodules/
│   │   ├── submodules.html
│   │   ├── submodules.js
│   │   └── ungit-plugin.json
│   └── textdiff/
│       ├── textdiff.html
│       ├── textdiff.js
│       ├── textdiff.less
│       └── ungit-plugin.json
├── eslint.config.mjs
├── package.json
├── public/
│   ├── images/
│   │   └── icon.icns
│   ├── index.html
│   ├── less/
│   │   ├── bootstrap.less
│   │   ├── d2h.less
│   │   ├── styles.less
│   │   └── variables.less
│   ├── main.js
│   ├── source/
│   │   ├── bootstrap.js
│   │   ├── components.js
│   │   ├── jquery-ui.js
│   │   ├── knockout-bindings.js
│   │   ├── main.js
│   │   ├── navigation.js
│   │   ├── program-events.js
│   │   ├── server.js
│   │   └── storage.js
│   └── vendor/
│       └── css/
│           └── animate.css
├── scripts/
│   ├── build.js
│   ├── electronpackage.js
│   ├── electronzip.js
│   ├── npmpublish.js
│   └── teststabilitytester.js
├── source/
│   ├── address-parser.js
│   ├── bugtracker.js
│   ├── config.js
│   ├── git-api.js
│   ├── git-parser.js
│   ├── git-promise.js
│   ├── server.js
│   ├── sysinfo.js
│   ├── ungit-plugin.js
│   └── utils/
│       ├── cache.js
│       ├── file-type.js
│       └── logger.js
├── test/
│   ├── common-es6.js
│   ├── spec.address-parser.js
│   ├── spec.cache.js
│   ├── spec.credentials-helper.js
│   ├── spec.file-type.js
│   ├── spec.git-api.branching.js
│   ├── spec.git-api.conflict-no-auto-stash.js
│   ├── spec.git-api.conflict.js
│   ├── spec.git-api.diff.js
│   ├── spec.git-api.discardchanges.js
│   ├── spec.git-api.ignorefile.js
│   ├── spec.git-api.js
│   ├── spec.git-api.patch.js
│   ├── spec.git-api.remote.js
│   ├── spec.git-api.squash.js
│   ├── spec.git-api.stash.js
│   ├── spec.git-api.submodule.js
│   └── spec.git-parser.js
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
/bin/credentials-helper eol=lf
/bin/ungit eol=lf


================================================
FILE: .github/workflows/bump.yml
================================================
name: Bump Dependencies

on:
  push:
    branches:
      - master
  schedule:
    - cron: '0 0 * * *'
  workflow_dispatch:

jobs:
  bump:
    if: github.event.repository.fork == false
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v6

      - name: Use Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '18'

      - run: npm ci
      - run: |
          body="$(npm run bumpdependencies)"
          body="${body#"${body%%[![:space:]]*}"}"
          body="${body%"${body##*[![:space:]]}"}"
          echo "$body"
          echo "body<<EOF" >> $GITHUB_OUTPUT
          echo "$body" >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT
        id: bumpdependencies
      - run: npm install

      - name: Create Pull Request
        uses: peter-evans/create-pull-request@v8
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          author: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
          commit-message: |
            Bump Dependencies

            ${{ steps.bumpdependencies.outputs.body }}
          title: Bump Dependencies
          body: |
            ```
            ${{ steps.bumpdependencies.outputs.body }}
            ```
          labels: dependencies
          branch: bumpdependencies


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on: [push, pull_request]

permissions:
  id-token: write   # Required for npm OIDC provenance publishing
  contents: write

jobs:
  test:
    if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
    strategy:
      fail-fast: false
      matrix:
        node-version: ['20', '22', '*']
        os: [ubuntu-latest, macos-latest, windows-latest]
    runs-on: ${{ matrix.os }}
#    env:
      # electron-packager (win32 ia32 on macos) https://github.com/electron/electron-packager/pull/449#issuecomment-240508298
#      WINEDLLOVERRIDES: 'mscoree,mshtml='

    steps:
      - uses: actions/checkout@v6

      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v6
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org'

      # linux dependencies
      # https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces
      - run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
        if: matrix.os == 'ubuntu-latest'
      - run: sudo apt update && sudo apt install -y wine64 && sudo ln -sf /usr/bin/wine /usr/bin/wine64
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
      - run: wine64 --version
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
      - run: sudo add-apt-repository ppa:git-core/ppa -y && sudo apt-get update -q && sudo apt-get install -y git
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '*'
      # macos dependencies
# required for electron-packager
#      - run: brew update && brew cask install xquartz wine-stable
#        if: matrix.os == 'macos-latest' && matrix.node-version == '20'
#      - run: wine64 --version
#        if: matrix.os == 'macos-latest' && matrix.node-version == '20'
      - run: brew reinstall git
        if: matrix.os == 'macos-latest' && matrix.node-version == '*'
      # windows dependencies
      # https://github.community/t5/GitHub-Actions/TEMP-is-broken-on-Windows/td-p/30432
      - run: echo "TEMP=$env:USERPROFILE\AppData\Local\Temp" >> $env:GITHUB_ENV
        if: matrix.os == 'windows-latest'
      - run: choco upgrade git
        if: matrix.os == 'windows-latest' && matrix.node-version == '*'

      - run: git --version
      - run: git config --global user.email "test@testy.com"
      - run: git config --global user.name "Test testy"
      - run: git config --global protocol.file.allow always # tests use file based submodules see #1539

      - run: npm ci
      - run: npm run lint
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
      - run: npm run build
      - run: npm test

      # publish artifacts
      - run: npm pack
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
      - run: npm run electronpackage -- --all
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
      - run: npm run electronzip
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'

      - name: Upload npm pack
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit
          path: ungit-*.tgz
          archive: false
          retention-days: 7

      - name: Upload ungit-darwin-arm64
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-darwin-arm64
          path: dist/ungit-darwin-arm64.zip
          archive: false
          retention-days: 7

      - name: Upload ungit-darwin-x64
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-darwin-x64
          path: dist/ungit-darwin-x64.zip
          archive: false
          retention-days: 7

      - name: Upload ungit-linux-arm64
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-linux-arm64
          path: dist/ungit-linux-arm64.zip
          archive: false
          retention-days: 7

      - name: Upload ungit-linux-armv7l
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-linux-armv7l
          path: dist/ungit-linux-armv7l.zip
          archive: false
          retention-days: 7

      - name: Upload ungit-linux-x64
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-linux-x64
          path: dist/ungit-linux-x64.zip
          archive: false
          retention-days: 7

      - name: Upload ungit-win32-arm64
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-win32-arm64
          path: dist/ungit-win32-arm64.zip
          archive: false
          retention-days: 7

      - name: Upload ungit-win32-ia32
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-win32-ia32
          path: dist/ungit-win32-ia32.zip
          archive: false
          retention-days: 7

      - name: Upload ungit-win32-x64
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20'
        uses: actions/upload-artifact@v7
        with:
          name: ungit-win32-x64
          path: dist/ungit-win32-x64.zip
          archive: false
          retention-days: 7

      - name: Upgrade npm for trusted publishing
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20' && github.repository == 'FredrikNoren/ungit' && github.ref == 'refs/heads/master'
        run: npm install -g npm@latest

      - name: npm publish
        if: matrix.os == 'ubuntu-latest' && matrix.node-version == '20' && github.repository == 'FredrikNoren/ungit' && github.ref == 'refs/heads/master'
        uses: actions/github-script@v8
        with:
          script: |
            const script = require('./scripts/npmpublish.js')
            await script({github, context, core, exec})


================================================
FILE: .gitignore
================================================
# Ignore generated folders
.nyc_output/
build/
coverage/
dist/
node_modules/
public/css/
public/fonts/glyphicons-*
public/js/

# Ignore compiled files
components/**/*.bundle.js
components/**/*.bundle.js.map
components/**/*.css
components/**/*.css.map

# Ignore potential files
.vscode/
.ungitrc
ungit-*.tgz


================================================
FILE: .mochaclicktest.json
================================================
{
  "spec": "clicktests/spec.*.js",
  "file": "./source/utils/logger.js",
  "timeout": 20000,
  "bail": true,
  "exit": true
}

================================================
FILE: .mochatest.json
================================================
{
  "spec": "test/spec.*.js",
  "file": "./source/utils/logger.js",
  "timeout": 12000,
  "exit": true
}

================================================
FILE: .npmignore
================================================
# Ignore whole folders
.github/
.nyc_output/
assets/
build/
clicktests/
coverage/
dist/
scripts/
test/

# Ignore non-compiled sources
components/**/*.less
components/**/*.js
!components/**/*.bundle.js
public/images/icon.icns
public/images/icon.ico
public/js/raven.min.js.map
public/less/
public/source/
public/vendor/

# Ignore dot-files
.gitattributes
.mochaclicktest.json
.mochatest.json
.prettierignore
.prettierrc
appveyor.yml
eslint.config.mjs
tsconfig.json

# Ignore docs
CONTRIBUTING.md
gpg_save_screenshot.png
MERGETOOL.md
PLUGINS.md
screenshot.png
xkcd.png

# Ignore potential files
.vscode/
.ungitrc
ungit-*.tgz


================================================
FILE: .prettierignore
================================================
# Generated folders
.nyc_output/
build/
coverage/

# Third-party files
public/css/
public/js
public/vendor/

# Browserify bundles
**/*.bundle.js
# All css files are autogenerated
**/*.css
# We don't like the prettier Markdown formatting
**/*.md

# Don't interfere with npm
package-lock.json
package.json


================================================
FILE: .prettierrc
================================================
{
  "endOfLine": "lf",
  "trailingComma": "es5",
  "jsdocUseInlineCommentForASingleTagBlock": true,
  "plugins": ["@homer0/prettier-plugin-jsdoc"],
  "printWidth": 100,
  "singleQuote": true
}


================================================
FILE: CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](https://semver.org/).
We are following the [Keep a Changelog](https://keepachangelog.com/) format.

## [Unreleased](https://github.com/FredrikNoren/ungit/compare/v1.5.29...master)

## [1.5.29](https://github.com/FredrikNoren/ungit/compare/v1.5.28...v1.5.29)

### Changed
- Update README.md [#1615](https://github.com/FredrikNoren/ungit/pull/1615)
- 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)
- Migrate to eslint flat config [#1607](https://github.com/FredrikNoren/ungit/pull/1607)
- Bump formidable from 3.5.2 to 3.5.4 [#1617](https://github.com/FredrikNoren/ungit/pull/1617)
- show remote fetch / push url in title [#1618](https://github.com/FredrikNoren/ungit/pull/1618)
- Bump qs from 6.14.1 to 6.14.2 [#1644](https://github.com/FredrikNoren/ungit/pull/1644)
- Bump actions/upload-artifact to 7 [#1649](https://github.com/FredrikNoren/ungit/pull/1649)

## [1.5.28](https://github.com/FredrikNoren/ungit/compare/v1.5.27...v1.5.28)

### Fixes
- Enable git fetch again [#1604](https://github.com/FredrikNoren/ungit/pull/1604)

### Changed
- 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
- CI Fixes https://github.com/FredrikNoren/ungit/commit/c02db509c2c334e7541e8f3dea4f615c7a614ccb, https://github.com/FredrikNoren/ungit/commit/a32668649836d7fd382574615333807ef3eed370, https://github.com/FredrikNoren/ungit/commit/7349b941a58f95117801fa36029fd44769dee8f8, 
- Replace node 21 with 22 https://github.com/FredrikNoren/ungit/commit/571d03f5fd9f4dca073152b123f117b5fb9bde8b

## [1.5.27](https://github.com/FredrikNoren/ungit/compare/v1.5.26...v1.5.27)

### Added
- Show parent commit ids and try to select them on click [#1581](https://github.com/FredrikNoren/ungit/issues/1581)

### Changed
- Use monospace font-family in commit body [#1598](https://github.com/FredrikNoren/ungit/pull/1598)
- Change watcher to properly filter ignored directories [#1597](https://github.com/FredrikNoren/ungit/pull/1597)

## [1.5.26](https://github.com/FredrikNoren/ungit/compare/v1.5.25...v1.5.26)

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

## [1.5.25](https://github.com/FredrikNoren/ungit/compare/v1.5.24...v1.5.25)

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

## [1.5.24](https://github.com/FredrikNoren/ungit/compare/v1.5.23...v1.5.24)

### Fixes
- Fallback for font if bundled font dont support glyphs [#1564](https://github.com/FredrikNoren/ungit/pull/1564)

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

## [1.5.23](https://github.com/FredrikNoren/ungit/compare/v1.5.22...v1.5.23)

### Fixes
- Ungit returns 0 when wrong arguments are used [#1548](https://github.com/FredrikNoren/ungit/issues/1548)
- Server process keeps running when parent gets killed [#1552](https://github.com/FredrikNoren/ungit/issues/1552)

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

## [1.5.22](https://github.com/FredrikNoren/ungit/compare/v1.5.21...v1.5.22)

### Fixes
- build: fix memory use by building serially [#1529](https://github.com/FredrikNoren/ungit/pull/1529)
- Small fixes and cleanups [#1530](https://github.com/FredrikNoren/ungit/pull/1530)

### Changed
- Update README.md [#1526](https://github.com/FredrikNoren/ungit/pull/1526)
- Update git version dependency to 2.34.x [#1536](https://github.com/FredrikNoren/ungit/pull/1536)
- Use fork to spawn a new node process [#1537](https://github.com/FredrikNoren/ungit/pull/1537)
- CI: git allow file protocol which is used in submodule tests [#1540](https://github.com/FredrikNoren/ungit/pull/1540)
- 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)

## [1.5.21](https://github.com/FredrikNoren/ungit/compare/v1.5.20...v1.5.21)

### Fixes
- fix patch checkbox html [#1517](https://github.com/FredrikNoren/ungit/pull/1517)

### Changed
- Bump Dependencies [#1512](https://github.com/FredrikNoren/ungit/pull/1512), [#1518](https://github.com/FredrikNoren/ungit/pull/1518)

### Removed
- Remove node 12 from build matrix [#1516](https://github.com/FredrikNoren/ungit/pull/1516)

## [1.5.20](https://github.com/FredrikNoren/ungit/compare/v1.5.19...v1.5.20)

### Fixes
- Fix potential remote code exec [#1510](https://github.com/FredrikNoren/ungit/pull/1510)
- Fix intermittent test failures [#1495](https://github.com/FredrikNoren/ungit/issues/1495)
- lint: small bugs + jsdoc [#1504](https://github.com/FredrikNoren/ungit/pull/1504)

### Changed
- Bump Dependencies [#1503](https://github.com/FredrikNoren/ungit/pull/1503)

## [1.5.19](https://github.com/FredrikNoren/ungit/compare/v1.5.18...v1.5.19)

### Added
- Add Tab Size Configuration [#1499](https://github.com/FredrikNoren/ungit/pull/1499)
- Types: add support for VSCode IntelliSense [#1466](https://github.com/FredrikNoren/ungit/pull/1466)

### Changed
- Directory view [#1491](https://github.com/FredrikNoren/ungit/pull/1491)
- Node watch [#1465](https://github.com/FredrikNoren/ungit/pull/1465)
- Bump cached-path-relative from 1.0.2 to 1.1.0 [#1501](https://github.com/FredrikNoren/ungit/pull/1501)
- 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)

## [1.5.18](https://github.com/FredrikNoren/ungit/compare/v1.5.17...v1.5.18)

### Fixes
- simple git flow breaks ungit [#1460](https://github.com/FredrikNoren/ungit/issues/1460)

### Changed
- Bump Dependencies [#1479](https://github.com/FredrikNoren/ungit/pull/1479)

## [1.5.17](https://github.com/FredrikNoren/ungit/compare/v1.5.16...v1.5.17)

### Changed
- node 16 [#1476](https://github.com/FredrikNoren/ungit/pull/1476)
- Bump Dependencies [#1475](https://github.com/FredrikNoren/ungit/pull/1475)

## [1.5.16](https://github.com/FredrikNoren/ungit/compare/v1.5.15...v1.5.16)

### Added
- Add clipboard button on commit [#1462](https://github.com/FredrikNoren/ungit/pull/1462)
- Encode URI paths with slashes [#1378](https://github.com/FredrikNoren/ungit/pull/1378)

### Changed
- Bump Dependencies [#1456](https://github.com/FredrikNoren/ungit/pull/1456), [#1464](https://github.com/FredrikNoren/ungit/pull/1464)
- Bump elliptic from 6.5.3 to 6.5.4 [#1468](https://github.com/FredrikNoren/ungit/pull/1468)
- Bump y18n from 4.0.0 to 4.0.1 [#1471](https://github.com/FredrikNoren/ungit/pull/1471)
- git 2.3x changes break unittests [#1472](https://github.com/FredrikNoren/ungit/issues/1472)

## [1.5.15](https://github.com/FredrikNoren/ungit/compare/v1.5.14...v1.5.15)

### Changed
- Bump Dependencies [#1451](https://github.com/FredrikNoren/ungit/pull/1451)

## [1.5.14](https://github.com/FredrikNoren/ungit/compare/v1.5.13...v1.5.14)

### Changed
- Update socket.io to version 3.0.0 [#1443](https://github.com/FredrikNoren/ungit/pull/1443)
- 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)

## [1.5.13](https://github.com/FredrikNoren/ungit/compare/v1.5.12...v1.5.13)

### Fixed
- Unhandled rejection ERR_FEATURE_NOT_AVAILABLE_ON_PLATFORM (recursive watch) [#1389](https://github.com/FredrikNoren/ungit/issues/1389)

### Changed
- Bump Dependencies [#1438](https://github.com/FredrikNoren/ungit/pull/1438)

## [1.5.12](https://github.com/FredrikNoren/ungit/compare/v1.5.11...v1.5.12)

### Fixed
- branches - can't re-enable disabled groups [#1434](https://github.com/FredrikNoren/ungit/issues/1434)
- Support git 2.29 sha256 [#1436](https://github.com/FredrikNoren/ungit/pull/1436)

### Changed
- Bump Dependencies [#1427](https://github.com/FredrikNoren/ungit/pull/1427)

## [1.5.11](https://github.com/FredrikNoren/ungit/compare/v1.5.10...v1.5.11)

### Added
- Doubleclick to checkout [#190](https://github.com/FredrikNoren/ungit/issues/190)

### Changed
- Use page.waitForTimeout API in tests [#1422](https://github.com/FredrikNoren/ungit/pull/1422)
- Bump Dependencies [#1417](https://github.com/FredrikNoren/ungit/pull/1417), [#1423](https://github.com/FredrikNoren/ungit/pull/1423)

## [1.5.10](https://github.com/FredrikNoren/ungit/compare/v1.5.9...v1.5.10)

### Fixed
- Add copyright to electron executable [#1411](https://github.com/FredrikNoren/ungit/issues/1411)

### Changed
- Generate and extract source maps [#1394](https://github.com/FredrikNoren/ungit/pull/1394)
- Import Bootstrap from npm and upgrade to latest 3.x [#1395](https://github.com/FredrikNoren/ungit/pull/1395)
- 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)

## [1.5.9](https://github.com/FredrikNoren/ungit/compare/v1.5.8...v1.5.9)

### Fixed
- Fix git ignore settings [#1393](https://github.com/FredrikNoren/ungit/pull/1393)

## [1.5.8](https://github.com/FredrikNoren/ungit/compare/v1.5.7...v1.5.8)

### Fixed
- Clear git-promise timeout when git command was successful [#1357](https://github.com/FredrikNoren/ungit/pull/1357)
- When autoFetch=false don't make remote repo calls automatically [#1381](https://github.com/FredrikNoren/ungit/pull/1381)
- Prevent commit message <textarea> from resizing horizontally [#1390](https://github.com/FredrikNoren/ungit/pull/1390)
- Diff out is not properly escaping [#1387](https://github.com/FredrikNoren/ungit/issues/1387)

### Changed
- Migrate clicktests from nightmare to puppeteer [#1336](https://github.com/FredrikNoren/ungit/pull/1336)
- Prettify code with prettier [#1316](https://github.com/FredrikNoren/ungit/pull/1316)
- Switch from JSHint to ESLint [#1360](https://github.com/FredrikNoren/ungit/pull/1360)
- Bump Dependencies [#1355](https://github.com/FredrikNoren/ungit/pull/1355), [#1385](https://github.com/FredrikNoren/ungit/pull/1385)

### Removed
- Remove bluebird dependency [#1350](https://github.com/FredrikNoren/ungit/pull/1350)
- Remove grunt [#895](https://github.com/FredrikNoren/ungit/issues/895)

## [1.5.7](https://github.com/FredrikNoren/ungit/compare/v1.5.6...v1.5.7)

### Fixed
- Init tooltips from the app start [#1343](https://github.com/FredrikNoren/ungit/pull/1343)
- Fixing some accessibility issues [#1318](https://github.com/FredrikNoren/ungit/pull/1318)
- Flatten total-lines-changed object [#1330](https://github.com/FredrikNoren/ungit/pull/1330)
- Set electron window icon explicitly so it works during debug and on linux [#1347](https://github.com/FredrikNoren/ungit/pull/1347)

### Changed
- 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)
- 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)
- Style autocompletes like dropdowns [#1327](https://github.com/FredrikNoren/ungit/pull/1327)
- Optimizes ref-search autocomplete initialization [#1326](https://github.com/FredrikNoren/ungit/pull/1326)
- Reduce jQuery UI imports and use Bootstrap tooltips [#1340](https://github.com/FredrikNoren/ungit/pull/1340)
- Image cleanup [#1345](https://github.com/FredrikNoren/ungit/pull/1345)
- Bump Dependencies [#1309](https://github.com/FredrikNoren/ungit/pull/1309)

### Removed
- Remove unused color dependency [#1341](https://github.com/FredrikNoren/ungit/pull/1341)
- Remove image embed [#1346](https://github.com/FredrikNoren/ungit/pull/1346)
- Remove unused tracker.js [#1344](https://github.com/FredrikNoren/ungit/pull/1344)

## [1.5.6](https://github.com/FredrikNoren/ungit/compare/v1.5.5...v1.5.6)

### Fixed
- Continue rebase fails with git 2.26 [#1301](https://github.com/FredrikNoren/ungit/issues/1301)
- 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)
- ignore nmclicktests and ci files in npm package [#1306](https://github.com/FredrikNoren/ungit/pull/1306)

### Added
- GitHub Action CI [#1298](https://github.com/FredrikNoren/ungit/pull/1298)
- GitHub Action dependency bump [#1296](https://github.com/FredrikNoren/ungit/pull/1296)

## [1.5.5](https://github.com/FredrikNoren/ungit/compare/v1.5.4...v1.5.5)

### Fixed
- Bump dependencies [#1283](https://github.com/FredrikNoren/ungit/pull/1283)
- Running npm scripts on macOS [#1287](https://github.com/FredrikNoren/ungit/pull/1287)
- Reduce CPU and Memory consumption in textdiff. Addresses part of [#1091](https://github.com/FredrikNoren/ungit/issues/1091)
- Better focus handling when creating branches and tags [#1288](https://github.com/FredrikNoren/ungit/pull/1288)
- Don't show error page when reloading the page [#1289](https://github.com/FredrikNoren/ungit/issues/1289)
- Periodically update author date of commits again [#1286](https://github.com/FredrikNoren/ungit/pull/1286)

## [1.5.4](https://github.com/FredrikNoren/ungit/compare/v1.5.3...v1.5.4)

### Fixed
- forcedLaunchPath of null fails to work [#1281](https://github.com/FredrikNoren/ungit/issues/1281)

### Changed
- Update diff2html to version 3 [#1273](https://github.com/FredrikNoren/ungit/pull/1273)

### Removed
- Remove dependency on npm [#1269](https://github.com/FredrikNoren/ungit/pull/1269)

## [1.5.3](https://github.com/FredrikNoren/ungit/compare/v1.5.2...v1.5.3)

### Fixed
- Git log for merge / empty commits does not work correctly [#1270](https://github.com/FredrikNoren/ungit/issues/1270)

## [1.5.2](https://github.com/FredrikNoren/ungit/compare/v1.5.1...v1.5.2)

### Fixed
- Diff does not work for first commit [#1124](https://github.com/FredrikNoren/ungit/issues/1124)
- `--no-launchBrowser` is ignored when ungit already running [#1259](https://github.com/FredrikNoren/ungit/issues/1259)
- Bare repositories don't work with git 2.25 [#1265](https://github.com/FredrikNoren/ungit/issues/1265)
- ungit crashes if current directory is deleted [#1266](https://github.com/FredrikNoren/ungit/issues/1266)
- Make clicktests more reliable [#1263](https://github.com/FredrikNoren/ungit/pull/1263)
- Rename + changes only show rename [#1175](https://github.com/FredrikNoren/ungit/issues/1175)

### Removed
- Remove Node 8 from build matrix [#1256](https://github.com/FredrikNoren/ungit/pull/1256)

## [1.5.1](https://github.com/FredrikNoren/ungit/compare/v1.5.0...v1.5.1)

### Fixed
- Fix copy and paste in electron on macOS [#1251](https://github.com/FredrikNoren/ungit/issues/1251)

## [1.5.0](https://github.com/FredrikNoren/ungit/compare/v1.4.48...v1.5.0)

### Added
- Include file diff in merge commits [#1242](https://github.com/FredrikNoren/ungit/pull/1242)
- Hide diff buttons on hover [#1225](https://github.com/FredrikNoren/ungit/pull/1225)
- Publish electron build [#1241](https://github.com/FredrikNoren/ungit/pull/1241)

### Fixed
- 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)
- Fix stash tooltips [#1227](https://github.com/FredrikNoren/ungit/pull/1227)
- Improve git-init experience [#1228](https://github.com/FredrikNoren/ungit/pull/1228)
- Fix inconsistent diff options [#1229](https://github.com/FredrikNoren/ungit/issues/1229)
- Fix clearing .gitignore [#1236](https://github.com/FredrikNoren/ungit/pull/1236)
- Fix electron package [#1240](https://github.com/FredrikNoren/ungit/pull/1240), [#1248](https://github.com/FredrikNoren/ungit/pull/1248)
- 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)

## [1.4.48](https://github.com/FredrikNoren/ungit/compare/v1.4.47...v1.4.48)

### Fixed
- fix the width value of the header logo [#1221](https://github.com/FredrikNoren/ungit/pull/1221)

## [1.4.47](https://github.com/FredrikNoren/ungit/compare/v1.4.46...v1.4.47)

### Fixed
- make diff2html line numbers and +/- prefixes unselectable [#1214](https://github.com/FredrikNoren/ungit/issues/1214), [#1215](https://github.com/FredrikNoren/ungit/pull/1215)

## [1.4.46](https://github.com/FredrikNoren/ungit/compare/v1.4.45...v1.4.46)

### Fixed
- force git out put to be in English within ungit [#1208](https://github.com/FredrikNoren/ungit/pull/1208)

## [1.4.45](https://github.com/FredrikNoren/ungit/compare/v1.4.44...v1.4.45)

### Fixed
- Improve styling of .gitignore edit dialog [#1205](https://github.com/FredrikNoren/ungit/pull/1205)

## [1.4.44](https://github.com/FredrikNoren/ungit/compare/v1.4.43...v1.4.44)

### Added
- add config to disable numstat in staged diff to better performance [#1193](https://github.com/FredrikNoren/ungit/issues/1193)

## [1.4.43](https://github.com/FredrikNoren/ungit/compare/v1.4.42...v1.4.43)

### Fixed
- fix gitignore manual edit not being saved [#644](https://github.com/FredrikNoren/ungit/issues/644)
- fix issue with detached git processes on some OS and timeout not being enforced.
- simplify `maxSearchIteration` enforcement for git.log()
- change `alwaysLoadActiveBranch` boolean config to `maxActiveBranchSearchIteration` numeric config
- bumped node engine requirement to [10.14 Dubnium](https://github.com/nodejs/node/blob/master/doc/changelogs/CHANGELOG_V10.md#10.14.2)

## [1.4.42](https://github.com/FredrikNoren/ungit/compare/v1.4.41...v1.4.42)

### Fixed
- Add "Ignore white space" config [#1185](https://github.com/FredrikNoren/ungit/pull/1185)

## [1.4.41](https://github.com/FredrikNoren/ungit/compare/v1.4.40...v1.4.41)

### Removed
- Remove Google Analytics [#1182](https://github.com/FredrikNoren/ungit/pull/1182)

## [1.4.40](https://github.com/FredrikNoren/ungit/compare/v1.4.39...v1.4.40)

### Removed
- Remove Keen.io [#1180](https://github.com/FredrikNoren/ungit/pull/1180)

## [1.4.39](https://github.com/FredrikNoren/ungit/compare/v1.4.38...v1.4.39)

### Fixed
- Add git bin path config. [#1151](https://github.com/FredrikNoren/ungit/issues/1151)

## [1.4.38](https://github.com/FredrikNoren/ungit/compare/v1.4.37...v1.4.38)

### Fixed
- Fix: Highlight current branch in submodules

## [1.4.37](https://github.com/FredrikNoren/ungit/compare/v1.4.36...v1.4.37)

### Fixed
- Sort modules by names

## [1.4.36](https://github.com/FredrikNoren/ungit/compare/v1.4.35...v1.4.36)

### Fixed
- fix changing remotes in remotes dropdown [#1158](https://github.com/FredrikNoren/ungit/pull/1158)

## [1.4.35](https://github.com/FredrikNoren/ungit/compare/v1.4.34...v1.4.35)

### Fixed
- allow disabling of nprogress bar [#1143](https://github.com/FredrikNoren/ungit/issues/1143)
- set `ungitVersionCheckOverride` as boolean in config [#1102](https://github.com/FredrikNoren/ungit/issues/1102)

## [1.4.34](https://github.com/FredrikNoren/ungit/compare/v1.4.33...v1.4.34)

### Fixed
- fix issues when remote tags doesn't show [#1139](https://github.com/FredrikNoren/ungit/issues/1139)

## [1.4.33](https://github.com/FredrikNoren/ungit/compare/v1.4.32...v1.4.33)

### Fixed
- Bump getmac version [#1130](https://github.com/FredrikNoren/ungit/issues/1130)
- Add config to disable animation [#1136](https://github.com/FredrikNoren/ungit/issues/1136)
- dependency bumps
- Remove node6. Add node8 and node9 explicitly.

## [1.4.32](https://github.com/FredrikNoren/ungit/compare/v1.4.31...v1.4.32)

### Fixed
- Handle crashes with better logs
- Wrap localStorage to support environments without access to it

## [1.4.31](https://github.com/FredrikNoren/ungit/compare/v1.4.30...v1.4.31)

### Fixed
- Add error logging for npm publish

## [1.4.30](https://github.com/FredrikNoren/ungit/compare/v1.4.29...v1.4.30)

### Fixed
- Add `ungitBindIp` config to allow default binding in some cases [#1112](https://github.com/FredrikNoren/ungit/issues/1112)

## [1.4.29](https://github.com/FredrikNoren/ungit/compare/v1.4.28...v1.4.29)

### Fixed
- Add `--no-optional-locks` if git version is appropriate [#1105](https://github.com/FredrikNoren/ungit/issues/1105)
- Ensure ungit server to bind to `127.0.0.1` [#988](https://github.com/FredrikNoren/ungit/issues/988)
- Add node highlight on mouse hover on relationship path [#1093](https://github.com/FredrikNoren/ungit/issues/1093)

## [1.4.28](https://github.com/FredrikNoren/ungit/compare/v1.4.27...v1.4.28)

### Fixed
- adding raven locally for offline access. [#1107](https://github.com/FredrikNoren/ungit/pull/1107)

## [1.4.27](https://github.com/FredrikNoren/ungit/compare/v1.4.26...v1.4.27)

### Fixed
- logic change for the merge conflict resolution

## [1.4.26](https://github.com/FredrikNoren/ungit/compare/v1.4.25...v1.4.26)

### Added
- add a way to preconfigure repo lists [#1106](https://github.com/FredrikNoren/ungit/issues/1106)

## [1.4.25](https://github.com/FredrikNoren/ungit/compare/v1.4.24...v1.4.25)

### Added
- add git pgp signing docs and code [#740](https://github.com/FredrikNoren/ungit/issues/740)

## [1.4.24](https://github.com/FredrikNoren/ungit/compare/v1.4.23...v1.4.24)

### Fixed
- change `/api/log` -> `/api/gitlog` as some ad blockers really hates This
- Fix excessive error messaging when disconnected from internet
- Fix Raven initialization error when disconnected from internet

## [1.4.23](https://github.com/FredrikNoren/ungit/compare/v1.4.22...v1.4.23)

### Fixed
- add feature to do `--recurse-submodules` for git clone [#1080](https://www.gnupg.org/documentation/manpage.html
- increase debounce 250->500 wait and 1000->2000 sec so UI can pick up server changes more accurately

## [1.4.22](https://github.com/FredrikNoren/ungit/compare/v1.4.21...v1.4.22)

### Fixed
- Fix missing jQuery and jQuery UI references [#1086](https://github.com/FredrikNoren/ungit/issues/1086)

## [1.4.21](https://github.com/FredrikNoren/ungit/compare/v1.4.20...v1.4.21)

### Fixed
- Treat remote fetch fail as an warning rather than error [#1081](https://github.com/FredrikNoren/ungit/issues/1081)

## [1.4.20](https://github.com/FredrikNoren/ungit/compare/v1.4.19...v1.4.20)

### Fixed
- deleted checked in 3rd party codes and manage by npm.
- remove dependencies on async lib

## [1.4.19](https://github.com/FredrikNoren/ungit/compare/v1.4.18...v1.4.19)

### Fixed
- fix credential helper not fetching all the authentication data [#1078](https://github.com/FredrikNoren/ungit/pull/1078)

## [1.4.18](https://github.com/FredrikNoren/ungit/compare/v1.4.17...v1.4.18)

### Fixed
- fix inaccurate git state issue when new branch name conflict and `autoCheckoutOnBranchCreate` is enabled.
- Add content refresh on .gitignore file change
- fix reference filtering

## [1.4.17](https://github.com/FredrikNoren/ungit/compare/v1.4.16...v1.4.17)

### Fixed
- fix textarea with in dialog when editing .gitignore [#1068](https://github.com/FredrikNoren/ungit/pull/1068)

## [1.4.16](https://github.com/FredrikNoren/ungit/compare/v1.4.15...v1.4.16)

### Fixed
- Move version number to below logo. [#1069](https://github.com/FredrikNoren/ungit/pull/1069)

## [1.4.15](https://github.com/FredrikNoren/ungit/compare/v1.4.14...v1.4.15)

### Fixed
- fix not setting `pathToNavigateTo` properly when `launchBrowser` is false and `launchCommand` is set [#1065](https://github.com/FredrikNoren/ungit/issues/1065)

## [1.4.14](https://github.com/FredrikNoren/ungit/compare/v1.4.13...v1.4.14)

### Fixed
- fix credential helper when ungit is used with rootpath [#1060](https://github.com/FredrikNoren/ungit/issues/1060)

## [1.4.13](https://github.com/FredrikNoren/ungit/compare/v1.4.12...v1.4.13)

### Fixed
- Change raven web client source to CDN rather than local copy [#972](https://github.com/FredrikNoren/ungit/issues/972)
- dependency bump

## [1.4.12](https://github.com/FredrikNoren/ungit/compare/v1.4.11...v1.4.12)

### Fixed
- Adding internet disconnected state handling [#1014](https://github.com/FredrikNoren/ungit/issues/1014)
- Allow editing .gitignore via ungit [#976](https://github.com/FredrikNoren/ungit/issues/1014)

## [1.4.11](https://github.com/FredrikNoren/ungit/compare/v1.4.10...v1.4.11)

### Added
- add cancel button for empty commits and amends [#1029](https://github.com/FredrikNoren/ungit/issues/1029)

### Fixed
- differentiate remote vs local tag. [#1016](https://github.com/FredrikNoren/ungit/issues/1016)
- fix push not throwing giterror
- fix remote tag push not creating remote tag
- change ref refresh logic
- show error on incorrect credentials [#1042](https://github.com/FredrikNoren/ungit/pull/1042)
- allow credential handling for remotes [#1039](https://github.com/FredrikNoren/ungit/issues/1039)
- cleanup clicktest output [#1035](https://github.com/FredrikNoren/ungit/pull/1035)

## [1.4.10](https://github.com/FredrikNoren/ungit/compare/v1.4.9...v1.4.10)

### Added
- add commit & push option [#1038](https://github.com/FredrikNoren/ungit/issues/1038)

### Fixed
- hide / disable push option if there is no remote [#1050](https://github.com/FredrikNoren/ungit/issues/1050)

## [1.4.9](https://github.com/FredrikNoren/ungit/compare/v1.4.8...v1.4.9)

### Fixed
- handle failed promises [#1017](https://github.com/FredrikNoren/ungit/issues/1017)
- empty commit [#1028](https://github.com/FredrikNoren/ungit/issues/1028)
- fix commit detail layout while hovering over commit node [#1025](https://github.com/FredrikNoren/ungit/issues/1025)

## [1.4.8](https://github.com/FredrikNoren/ungit/compare/v1.4.7...v1.4.8)

### Fixed
- fix remote branches display name and delete action [#1032](https://github.com/FredrikNoren/ungit/issues/1032), [#1031](https://github.com/FredrikNoren/ungit/issues/1031)

## [1.4.7](https://github.com/FredrikNoren/ungit/compare/v1.4.6...v1.4.7)

### Added
- add remote branches to the branch list. [#966](https://github.com/FredrikNoren/ungit/issues/966)

## [1.4.6](https://github.com/FredrikNoren/ungit/compare/v1.4.5...v1.4.6)

### Fixed
- dependency bump to fix dependency's security problem.
- Add emphasis if remote branch delete for confirmation dialog. [#947](https://github.com/FredrikNoren/ungit/issues/947)

## [1.4.5](https://github.com/FredrikNoren/ungit/compare/v1.4.4...v1.4.5)

### Fixed
- fix a bug where no diff wasn't properly showing [#969](https://github.com/FredrikNoren/ungit/issues/969)

## [1.4.4](https://github.com/FredrikNoren/ungit/compare/v1.4.3...v1.4.4)

### Fixed
- fix a bug where fetch is disabled after page load
- make `forceLaunchPath` to supersede `launchBrowser` [#1006](https://github.com/FredrikNoren/ungit/issues/1006)

## [1.4.3](https://github.com/FredrikNoren/ungit/compare/v1.4.2...v1.4.3)

### Fixed
- changing to path navigation to `nprogress` bar. [#1001](https://github.com/FredrikNoren/ungit/issues/1001)

## [1.4.2](https://github.com/FredrikNoren/ungit/compare/v1.4.1...v1.4.2)

### Fixed
- fix navigation redirection on git clone and adding xkcd image
- dependency bump

## [1.4.1](https://github.com/FredrikNoren/ungit/compare/v1.4.0...v1.4.1)

### Fixed
- fix the issue where browser opens before ungit start. [#994](https://github.com/FredrikNoren/ungit/issues/994)
- including xkcd art back [#999](https://github.com/FredrikNoren/ungit/issues/999)

## [1.4.0](https://github.com/FredrikNoren/ungit/compare/v1.3.3...v1.4.0)

### Fixed
- Revert to MIT [#947](https://github.com/FredrikNoren/ungit/issues/974)

## [1.3.3](https://github.com/FredrikNoren/ungit/compare/v1.3.2...v1.3.3)

### Fixed
- fix `tagsToDisplay` clearing issue. [#973](https://github.com/FredrikNoren/ungit/issues/973)

## [1.3.2](https://github.com/FredrikNoren/ungit/compare/v1.3.1...v1.3.2)

### Added
- Adding in ref search box and limit num of ref display [#973](https://github.com/FredrikNoren/ungit/issues/973)

## [1.3.1](https://github.com/FredrikNoren/ungit/compare/v1.3.0...v1.3.1)

### Added
- Add link to plans & license in header [#947](https://github.com/FredrikNoren/ungit/issues/974)

## [1.3.0](https://github.com/FredrikNoren/ungit/compare/v1.2.3...v1.3.0)

### Fixed
- Switch to Faircode paywall instead of license popup [#947](https://github.com/FredrikNoren/ungit/issues/974)

## [1.2.3](https://github.com/FredrikNoren/ungit/compare/v1.2.2...v1.2.3)

### Fixed
- Bump license text to v0.2.1 (fixes typo). [Faircode License changelog](https://github.com/faircodeio/faircode-license/blob/master/CHANGELOG.md)

## [1.2.2](https://github.com/FredrikNoren/ungit/compare/v1.2.1...v1.2.2)

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

## [1.2.1](https://github.com/FredrikNoren/ungit/compare/v1.2.0...v1.2.1)

### Fixed
- fix for not launching browser when executed at the git repo [#986](https://github.com/FredrikNoren/ungit/issues/986)

## [1.2.0](https://github.com/FredrikNoren/ungit/compare/v1.1.33...v1.2.0)

### Fixed
- Show license notification on first start (license changed in 1.1.32) [#947](https://github.com/FredrikNoren/ungit/issues/974)
- fix potential memory leak with `express-session`[#977](https://github.com/FredrikNoren/ungit/issues/977)
- Fix document title on windows [#983](https://github.com/FredrikNoren/ungit/pull/983)
- parse local storage as json instead of regex [#981](https://github.com/FredrikNoren/ungit/pull/981)
- resolve path keywords such as `~` at server side [#980](https://github.com/FredrikNoren/ungit/issues/975)

## [1.1.33](https://github.com/FredrikNoren/ungit/compare/v1.1.32...v1.1.33)

### Fixed
- Make Logo and favicon HiDpi [#589](https://github.com/FredrikNoren/ungit/issues/589)
- Remove forever-monitor [#961](https://github.com/FredrikNoren/ungit/issues/961)

## [1.1.32](https://github.com/FredrikNoren/ungit/compare/v1.1.31...v1.1.32)

### Fixed
- Update license [#974](https://github.com/FredrikNoren/ungit/issues/974)

## [1.1.31](https://github.com/FredrikNoren/ungit/compare/v1.1.30...v1.1.31)

### Fixed
- Bump dependencies

## [1.1.30](https://github.com/FredrikNoren/ungit/compare/v1.1.29...v1.1.30)

### Fixed
- move unit tests to es6
- Add squash feature [#129](https://github.com/FredrikNoren/ungit/issues/129)

## [1.1.29](https://github.com/FredrikNoren/ungit/compare/v1.1.28...v1.1.29)

### Fixed
- move `Gruntfile.js` to es6

## [1.1.28](https://github.com/FredrikNoren/ungit/compare/v1.1.27...v1.1.28)

### Fixed
- Refactoring to remove static data-ta tags from tests
- `grunt nmclicktest` -> `grunt clicktest`
- Stabilize ungit open test of clicktest via using a tag that is guaranteed to be generated
- Add click test bailout on tes failure
- Add parallel click test `grunt clickParallel`
- Remove deps to fix config init bug for the `credentials-helper`. [#838](https://github.com/FredrikNoren/ungit/issues/838)

## [1.1.27](https://github.com/FredrikNoren/ungit/compare/v1.1.26...v1.1.27)

### Fixed
- Add alert when moving back in time. [#914](https://github.com/FredrikNoren/ungit/issues/914)

## [1.1.26](https://github.com/FredrikNoren/ungit/compare/v1.1.25...v1.1.26)

### Fixed
- fix invalid path input for autocomplete causing front end crash [#942](https://github.com/FredrikNoren/ungit/issues/942)
- bump and checking in package-lock.json

## [1.1.25](https://github.com/FredrikNoren/ungit/compare/v1.1.24...v1.1.25)

### Fixed
- Change stash pop operation to stash apply [#919](https://github.com/FredrikNoren/ungit/issues/919)

## [1.1.24](https://github.com/FredrikNoren/ungit/compare/v1.1.23...v1.1.24)

### Fixed
- fix some commands not properly reporting git error [#933](https://github.com/FredrikNoren/ungit/issues/933)

## [1.1.23](https://github.com/FredrikNoren/ungit/compare/v1.1.22...v1.1.23)

### Fixed
- finalize nightmare click test

## [1.1.22](https://github.com/FredrikNoren/ungit/compare/v1.1.21...v1.1.22)

### Fixed
- Add a config setting to allow setting the default diff type. [#929](https://github.com/FredrikNoren/ungit/issues/929)

## [1.1.21](https://github.com/FredrikNoren/ungit/compare/v1.1.20...v1.1.21)

### Fixed
- Initial refactoring of click test using nightmare and mocha
- **Dropping support for node 4.x and 5.x!, 6.x and later is now supported.**

## [1.1.20](https://github.com/FredrikNoren/ungit/compare/v1.1.19...v1.1.20)

### Fixed
- Hide credentials in remote urls at home repo list

## [1.1.19](https://github.com/FredrikNoren/ungit/compare/v1.1.18...v1.1.19)

### Fixed
- Ask before deleting a stash

## [1.1.18](https://github.com/FredrikNoren/ungit/compare/v1.1.17...v1.1.18)

### Fixed
- Fix checking out remote refs (again)

## [1.1.17](https://github.com/FredrikNoren/ungit/compare/v1.1.16...v1.1.17)

### Fixed
- Fix checking out remote refs

## [1.1.16](https://github.com/FredrikNoren/ungit/compare/v1.1.15...v1.1.16)

### Fixed
- clicktests logging correction and using wait for within tests.
- Refactor filewatch and using normalized test path
- throttle parallel test's parellelization limit
- dependency bump
- Fix context issue for `gitSetUserConfig` [#912](https://github.com/FredrikNoren/ungit/issues/912)

## [1.1.15](https://github.com/FredrikNoren/ungit/compare/v1.1.14...v1.1.15)

### Fixed
- Updating crash page with instructions and adblock detection

## [1.1.14](https://github.com/FredrikNoren/ungit/compare/v1.1.13...v1.1.14)

### Fixed
- Disable strict mode for startup params and config [#890](https://github.com/FredrikNoren/ungit/issues/890)

## [1.1.13](https://github.com/FredrikNoren/ungit/compare/v1.1.12...v1.1.13)

### Fixed
- Fix startup args bug: [#896](https://github.com/FredrikNoren/ungit/issues/896)

## [1.1.12](https://github.com/FredrikNoren/ungit/compare/v1.1.11...v1.1.12)

### Fixed
- Retain commit messages when commit fails [#882](https://github.com/FredrikNoren/ungit/pull/882)
- Fix rare edge case where remote node is gone during reset op.
- rescursively resolve all promises before caching them. [#878](https://github.com/FredrikNoren/ungit/pull/878)

## [1.1.11](https://github.com/FredrikNoren/ungit/compare/v1.1.10...v1.1.11)

### Fixed
- Fix cli arguments [#871](https://github.com/FredrikNoren/ungit/pull/871)
- Stop if ~/.ungitrc contains syntax error
- Removed official support ini format of ~/.ungitrc, because internal API supports only JSON

## [1.1.10](https://github.com/FredrikNoren/ungit/compare/v1.1.9...v1.1.10)

### Fixed
- Fix broken diff out in some cases when diff contains table. [#881](https://github.com/FredrikNoren/ungit/pull/881)

## [1.1.9](https://github.com/FredrikNoren/ungit/compare/v1.1.8...v1.1.9)

### Fixed
- Fix around ubuntu's inability to cache promises. [#877](https://github.com/FredrikNoren/ungit/pull/878)

## [1.1.8](https://github.com/FredrikNoren/ungit/compare/v1.1.7...v1.1.8)

### Fixed
- Realtime text diff via invalidate diff on directory change [#867](https://github.com/FredrikNoren/ungit/pull/867)
- Promisify `./source/utils/cache.js` [#870](https://github.com/FredrikNoren/ungit/pull/870)
- Fix load more text diff button. [#876](https://github.com/FredrikNoren/ungit/pull/876)

## [1.1.7](https://github.com/FredrikNoren/ungit/compare/v1.1.6...v1.1.7)

### Fixed
- Fix diff flickering issue and optimization [#865](https://github.com/FredrikNoren/ungit/pull/865)
- Fix credential dialog issue [#864](https://github.com/FredrikNoren/ungit/pull/864)
- Fix HEAD branch order when redraw [#858](https://github.com/FredrikNoren/ungit/issues/858)

## [1.1.6](https://github.com/FredrikNoren/ungit/compare/v1.1.5...v1.1.6)

### Fixed
- Fix path auto complete [#861](https://github.com/FredrikNoren/ungit/issues/861)

## [1.1.5](https://github.com/FredrikNoren/ungit/compare/v1.1.4...v1.1.5)

### Fixed
- Update "Toggle all" button after commit or changing selected files [#859](https://github.com/FredrikNoren/ungit/issues/859)

## [1.1.4](https://github.com/FredrikNoren/ungit/compare/v1.1.3...v1.1.4)

### Fixed
- [patch] Promise refactoring

## [1.1.3](https://github.com/FredrikNoren/ungit/compare/v1.1.2...v1.1.3)

### Fixed
- [patch] Fix submodule navigation on windows [#577](https://github.com/FredrikNoren/ungit/issues/577)

## [1.1.2](https://github.com/FredrikNoren/ungit/compare/v1.1.1...v1.1.2)

### Fixed
- Fix a bug that prevented the new version dialog from being dismissed

## [1.1.1](https://github.com/FredrikNoren/ungit/compare/v1.1.0...v1.1.1)

### Fixed
- [patch] Fixed small spelling error for ignore whitespace feature [#853](https://github.com/FredrikNoren/ungit/pull/853)

## [1.1.0](https://github.com/FredrikNoren/ungit/compare/v1.0.1...v1.1.0)

### Added
- Added option to ignore ungit version checks [#851](https://github.com/FredrikNoren/ungit/issues/851)

## [1.0.1](https://github.com/FredrikNoren/ungit/compare/v1.0.0...v1.0.1)

### Fixed
- [patch] Fixed gravatar avatar fetch if email have different cases applied. [#847](https://github.com/FredrikNoren/ungit/issues/847)

## [1.0.0](https://github.com/FredrikNoren/ungit/compare/v0.10.3...v1.0.0)

### Added
- Added search by git folder name in the search bar. [#793](https://github.com/FredrikNoren/ungit/issues/793)
- New configuration option `logLevel` allows you to assign the level of logging you want to see in the servers output console.
- 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)
- Whitespace ignore option for text diffs [#777](https://github.com/FredrikNoren/ungit/issues/777)
- Fix for favorites linking in case rootPath is used @sebastianmay [#609](https://github.com/FredrikNoren/ungit/issues/609) and image diffing
- Limit commit title to 72 characters, the rest is truncated and shown when inspecting the commit
- Updated file watch logic to closely follow git commands in another process [#283](https://github.com/FredrikNoren/ungit/issues/283)
- Introduced Continuous delivery. [#823](https://github.com/FredrikNoren/ungit/issues/823)

### Fixed
- File diff firing increasing number of events longer it survives.
- Fix missing ungit logo. [#812](https://github.com/FredrikNoren/ungit/issues/812)
- Fix when stash output is empty [#818](https://github.com/FredrikNoren/ungit/issues/818)
- Fix minor display error for wide git repo [#830](https://github.com/FredrikNoren/ungit/pull/830)
- Persist commit messages during merge operation [#779](https://github.com/FredrikNoren/ungit/issues/779)
- Refresh `staging.files` object for cleaner refresh such as refresh pached line list, diff and etc.
- Fixed an issue where patching on some key word file names such as "test".
- Fix missing commit message body if commit was committed with Visual Studio or Visual Studio Code [#826](https://github.com/FredrikNoren/ungit/pull/826)
- Fix initial page load when loaded node does not fits in screen. [#832](https://github.com/FredrikNoren/ungit/issues/832)

## [0.10.3](https://github.com/FredrikNoren/ungit/compare/v0.10.2...v0.10.3)

### Added
- Show diffs for stashed changes [#444](https://github.com/FredrikNoren/ungit/issues/444)
- Active node focused git log result [#420](https://github.com/FredrikNoren/ungit/issues/420)

### Fixed
- Missing npm as a normal dependency [#766](https://github.com/FredrikNoren/ungit/issues/766)

## [0.10.2](https://github.com/FredrikNoren/ungit/compare/v0.10.1...v0.10.2)

### Fixed
- Handle SIGTERM and SIGINT [#763](https://github.com/FredrikNoren/ungit/issues/763)

### Added
- Added bare repo support [#177](https://github.com/FredrikNoren/ungit/issues/177) [#728](https://github.com/FredrikNoren/ungit/issues/728)
- Added support for cherry-pick conflict[#701](https://github.com/FredrikNoren/ungit/issues/701)
- Added wordwrap support for diffs [#721](https://github.com/FredrikNoren/ungit/issues/721)
- Support for Node6 [#745](https://github.com/FredrikNoren/ungit/pull/745/files)
- Added "autoCheckoutOnBranchCreate" option [#752](https://github.com/FredrikNoren/ungit/pull/752/files)

### Fixed
- Fix maxConcurrentGitOperations not limiting git processes [#707](https://github.com/FredrikNoren/ungit/issues/707)
- Fix ".lock" file conflicts in parallelized git operations [#515](https://github.com/FredrikNoren/ungit/issues/515)
- Allow Ungit to function under sub dir of a git dir [#734](https://github.com/FredrikNoren/ungit/issues/734)
- Removed deprecated npmconf package [#746](https://github.com/FredrikNoren/ungit/issues/746)
- More helpful warning messages [#749](https://github.com/FredrikNoren/ungit/pull/749/files)
- Deleting already deleted remote tag [#748](https://github.com/FredrikNoren/ungit/pull/748)
- Fix to handle revert merge commit [#757](https://github.com/FredrikNoren/ungit/pull/757)

### Changed
- Cleaner rebase conflict message display [#708](https://github.com/FredrikNoren/ungit/pull/708)
- ES6 [#672](https://github.com/FredrikNoren/ungit/pull/672)
- Dropped support for Node 0.10 and 0.12 [#745](https://github.com/FredrikNoren/ungit/pull/745/files)

## [0.10.1](https://github.com/FredrikNoren/ungit/compare/v0.10.0...v0.10.1)

### Added
- Introduced change log! [#687](https://github.com/FredrikNoren/ungit/issues/687)
- Improved server and client error logging [#695](https://github.com/FredrikNoren/ungit/pull/695)

### Fixed
- Fix crashes due to submodule parsing [#690](https://github.com/FredrikNoren/ungit/issues/690) [#689](https://github.com/FredrikNoren/ungit/issues/689)
- Fix duplicate remote tag issues [#685](https://github.com/FredrikNoren/ungit/issues/685)
- Fix scrolling issue in safari [#686](https://github.com/FredrikNoren/ungit/issues/686)
- Fix git hooks failing on non-ascii files [#676](https://github.com/FredrikNoren/ungit/issues/676)

### Removed
- Reverted on hover button effects [#688](https://github.com/FredrikNoren/ungit/issues/688)

### Changed
- Upgrade keen.io client code [#679](https://github.com/FredrikNoren/ungit/issues/679)


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing Guidelines

These 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.

## Posting issues

Just 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).

## Pull requests

All PRs are automatically published to NPM once merged (see [#823](https://github.com/FredrikNoren/ungit/issues/823)).
There are two things you have to do for all PRs:

- Make sure to include a note in CHANGELOG.md about the change as part of the PR.
- If it's a code change: Bump the version in `package.json` and `package-lock.json`.
  - Does the change fundamentally change how people use Ungit: Bump the major version.
  - Does the change introduce new features: Bump the minor version.
  - Otherwise (bug fixes, tweaks, and refactoring): Bump patch version.
  - If the change doesn't affect the product (e.g. you change the README): No need to bump the version.

## Writing plugins

See [PLUGINS.md](PLUGINS.md)

## Developing for Ungit proper

I 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.

### What you need to get started

You'll need the same as for running Ungit; node, npm, and git.

### Getting started

To get started developing on Ungit:

 1. Make sure you have [node.js](https://nodejs.org/), [npm](https://www.npmjs.com/) and [git](https://git-scm.com/) installed.
 2. Clone the repository to a local directory.
 3. Run `npm install` to install dependencies.
 4. Run `npm run build` to build (compile templates, CSS and JS).
 5. Type `npm start` to start ungit, or `npm test` to run tests.
 6. (Optional). Run `npm run watch` to automatically rebuild stuff when you change files.
 7. Run `npm run lint` to verify your changes conform to the formatting.
 8. (Optional). Run `npm run format` to automatically fix formatting and (some) linting issues.

### Run ungit as a standalone application

To provide easier access to launch ungit a standalone application container using [electron](https://electronjs.org/) is available.

#### To get started

 1. Follow steps in 'Getting started' to get a development environment ready.
 2. Run `npm run electronpackage`. This will create a standalone application package under `build/`

#### Known limitations

 1. The current standalone application does not allow you to execute more than one instance.
 2. There is no installer package neither automatic update mechanism for standalone application in place yet.

#### Additional notes

 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.

### Architecture overview

Ungit 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.

### Folders

- `assets/` Raw assets used for development.
- `bin/` "Binary" files, the ungit launcher script and the credentials-helper, which is invoked by git to acquire credentials when using http authentication.
- `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.
- `components/` This directory contains all view components for Ungit, each of them exposed as an Ungit plugin.
- `public/` The UI web-app.
- `public/css/` CSS generated by the npm build script.
- `public/fonts/` and `public/images/` Assets which are served directly.
- `public/js/` An ungit.js file generated by the npm build script, as well as raven files which handle exception logging.
- `public/less/` Less files, which are the "source" used to generate the CSS.
- `public/source/` JavaScript source code, which is turned into the `public/js/ungit.js` file by the npm build script.
- `public/vendor/` Various 3rd-party libs.
- `source/` Server and shared (i.e. used by both server and UI) source code.
- `test/` Unit tests and REST interface tests.

### Running tests

`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`.

### Things to consider when developing

- 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.
- Write tests. The most important tests to write are usually the click tests since they will cover the most code (both UI and backend).


================================================
FILE: LICENSE.md
================================================
MIT License

Copyright (c) 2013-2026 Fredrik Norén

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: MERGETOOL.md
================================================
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:  


1. Configuring git
------------------

The 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:

```ini
[mergetool "extMerge"]
	cmd = extMergeTool "$BASE" "$LOCAL" "$REMOTE" "$MERGED"
	trustExitCode = false
```

* `"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.
* For best results, refer to the documentation of your merge tool, as it may require different command arguments.
* 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.
* `"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).
* Additionally, you can also provide the following if you want to identify your merge tool as the default:

```ini
[merge]
	tool = extMerge
```

If 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:
`> git mergetool --tool extMerge`
This should invoke your merge tool and cycle through each conflicted file.


2. Configuring ungit
--------------------

Add 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:

```json
{
  "mergeTool": "extMerge"
}
```

3. Use ungit's interface
------------------------

Start 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`.

Once 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.


4. Known Issues and Troubleshooting
-----------------------------------

* 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.
* 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.
* If for any reason git does not recognize that your merge tool has resolved the file, `trustExitCode` may need to be set to `false`.
* 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.


5. Merge Tool Suggestions
-------------------------
* Mac OS X:
  * Meld: [meldmerge.org](https://meldmerge.org)
  * Kaleidoscope: [kaleidoscopeapp.com](https://www.kaleidoscopeapp.com)
  * Araxis Merge: [araxis.com](https://araxis.com/merge)
  * DeltaWalker: [deltopia.com](https://deltopia.com)
* Windows:
  * Beyond Compare: [scootersoftware.com](https://scootersoftware.com/)
  * Araxis Merge: [araxis.com](https://araxis.com/merge)
  * P4Merge: [perforce.com](https://perforce.com/products/helix-core-apps/merge-diff-tool-p4merge)


================================================
FILE: PLUGINS.md
================================================
Writing Ungit plugins
=====================

It's super easy to write an Ungit plugin. Here's how to write a completely new (though super simple) git log ui:

### 1. Create a new folder for your plugin.
Create a folder at `~/.ungit/plugins/MY_FANCY_PLUGIN`, then add a file called `ungit-plugin.json` with the following content:
```JSON
{
  "exports": {
    "javascript": "example.js"
  }
}
```

### 2. Add some code
Create an `example.js` file and add this:

```JavaScript
var components = require('ungit-components');

// We're overriding the graph component here
components.register('graph', function(args) {
  return {
    // This method creates and returns the DOM node that represents this component.
    updateNode: function() {
      var node = document.createElement('div');
      // Request all log entries from the backend
      args.server.get('/log', { path: args.repoPath, limit: 50 }, function(err, log) {
        // Add all log entries to the parent node
        log.forEach(function(entry) {
          var entryNode = document.createElement('div');
          entryNode.innerHTML = entry.message;
          node.appendChild(entryNode);
        });
      });
      return node;
    }
  };
});
```

### 3. Done!
Just 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.

### Ungit Plugin API version
The 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`.

### Components

Each 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.

An [example](https://github.com/FredrikNoren/ungit/tree/master/components/staging) of ungit component with view can be seen below.

```JSON
{
  "exports": {
    "knockoutTemplates": {
      "staging": "staging.html"
    },
    "javascript": "staging.bundle.js",
    "css": "staging.css"
  }
}
```

* Views(html) for Component

   Each component can have multiple views as exampled [here](https://github.com/FredrikNoren/ungit/tree/master/components/dialogs).

* CSS for Component
   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.

* JS for Component

   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).


================================================
FILE: README.md
================================================
ungit
======
[![Release](https://img.shields.io/github/v/release/FredrikNoren/ungit)](https://github.com/FredrikNoren/ungit/releases)
[![CI](https://github.com/FredrikNoren/ungit/actions/workflows/ci.yml/badge.svg)](https://github.com/FredrikNoren/ungit/actions/workflows/ci.yml)
[![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)

The easiest way to use git. On any platform. Anywhere.

[![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/)

Git 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.

Ungit brings user friendliness to git without sacrificing the versatility of git.

 * Clean and intuitive UI that makes it easy to _understand_ git.
 * Runs on any platform that node.js & git supports.
 * 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).
 * Works well with GitHub.
 * [Gerrit](https://code.google.com/p/gerrit/) integration through plugin: https://github.com/FredrikNoren/ungit-gerrit

[Follow @ungitui on twitter](https://twitter.com/ungitui)

Quick intro to ungit: [https://youtu.be/hkBVAi3oKvo](https://youtu.be/hkBVAi3oKvo)

[![Screenshot](screenshot.png)](https://youtu.be/hkBVAi3oKvo)

Installing
----------
Requires [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:

	npm install -g ungit

NOTE: If your system requires root access to install global npm packages, make sure you use the -H flag:

	sudo -H npm install -g ungit

Prebuilt [electron](https://electronjs.org/) packages are available [here](https://github.com/FredrikNoren/ungit/releases) (git is still required).

Using
-----
Anywhere you want to start, just type:

	ungit

This will launch the server and open up a browser with the ui.

Configuring
-----------
Put 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.

You 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`.

Example of `~/.ungitrc` configuration file to change default port and enable bugtracking:

```json
{
	"port": 8080,
	"bugtracking": true
}
```

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

- `git config --global commit.gpgsign true` (or without `--global` at the repo)
- Add `isForceGPGSign: true` to `ungit.rc` file

Currently, 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.  

Therefore, password-less gpg authentication or 3rd party gpg password must be configured when using Ungit to commit with gpg.
Below are several way to enable password-less gpg authentication for various OSs.

- [Cache GnuPG passphrase](https://superuser.com/questions/624343/keep-gnupg-credentials-cached-for-entire-user-session)
- gpg-agent with pinentry-mac
  1. `brew install gnupg gpg-agent pinentry-mac`
  2. `echo "test" | gpg --clearsign` # See gpg authentication prompt when gpg is accessed.
  3. Optionally you can save it to keychain. ![gpg_save_screenshot](gpg_save_screenshot.png)

I understand this is not convenient, but security is hard. And I'd much rather have bit of inconvenience than Ungit having security exposure.


External Merge Tools
--------------------
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. See [MERGETOOL.md](MERGETOOL.md).

Auto Refresh
------------
Ungit 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`.

Text Editor Integrations
-------------------

* [atom-ungit](https://github.com/codingtwinky/atom-ungit) for [Atom.io](https://atom.io/) by [@codingtwinky](https://github.com/codingtwinky)

![atom-ungit Screenshot](https://raw.githubusercontent.com/codingtwinky/atom-ungit/master/screenshot.png)

* [brackets-ungit](https://github.com/Hirse/brackets-ungit) for [Brackets.io](http://brackets.io/) by [@Hirse](https://github.com/Hirse)

![brackets-ungit Screenshot](https://raw.githubusercontent.com/Hirse/brackets-ungit/master/images/viewer.png)

* [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)

![VSCode-Ungit screenshot](https://raw.githubusercontent.com/hirse/vscode-ungit/master/screenshots/ungit.gif)


Developing
----------

See [CONTRIBUTING.md](CONTRIBUTING.md).

Maintainers
-----------

* [FredrikNoren](https://github.com/FredrikNoren) [Fredrik's Patreon page for donations](https://www.patreon.com/fredriknoren)
* [Jung-Kim](https://github.com/jung-kim) [JK's (codingtwinky) Patreon page for donations](https://www.patreon.com/jungkim)
* [campersau](https://github.com/campersau)

Known issues
------------

* 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.
* 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.
* 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).
* 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)
* 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)

Changelog
---------
See [CHANGELOG.md](CHANGELOG.md).

License (MIT)
-------------
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.

[![Dependency Status](https://david-dm.org/FredrikNoren/ungit.svg)](https://david-dm.org/FredrikNoren/ungit)
[![devDependency Status](https://david-dm.org/FredrikNoren/ungit/dev-status.svg)](https://david-dm.org/FredrikNoren/ungit#info=devDependencies)


================================================
FILE: appveyor.yml
================================================
skip_branch_with_pr: true
image: Visual Studio 2022

environment:
  matrix:
    - nodejs_version: '' # latest
    - nodejs_version: '22.12'
    - nodejs_version: '20.19'

branches:
  only:
    - master

install:
  - ps: |
      try {
        Install-Product node $env:nodejs_version x64
      } catch {
        Update-NodeJsInstallation (Get-NodeJsLatestBuild $env:nodejs_version) x64
      }
  - node --version
  - npm --version
  - npm ci

build_script:
  - npm run lint
  - npm run build

before_test:
  - git config --global user.email "test@testy.com"
  - git config --global user.name "Test testy"
  - git config --global protocol.file.allow always # tests use file based submodules see #1539
  - git --version

test_script:
  - npm test


================================================
FILE: bin/credentials-helper
================================================
#!/usr/bin/env node
const http = require('http');
const socketId = process.argv[2];
const portAndRootPath = process.argv[3];
const remote = process.argv[4];
const action = process.argv[5];

if (action == 'get') {
  http
    .get(
      `http://localhost:${portAndRootPath}/api/credentials?socketId=${socketId}&remote=${encodeURIComponent(
        remote
      )}`,
      (res) => {
        let rawData = '';
        res.on('data', (chunk) => {
          rawData += chunk;
        });
        res.on('end', () => {
          const data = JSON.parse(rawData);
          console.log(`username=${data.username}`);
          console.log(`password=${data.password ? data.password : ''}`);
        });
      }
    )
    .on('error', (err) => {
      console.error("Error getting credentials, couldn't query server", err);
    });
} else {
  console.info(`Unhandled action: ${action}`);
}


================================================
FILE: bin/ungit
================================================
#!/usr/bin/env node

const startLaunchTime = Date.now();

const config = require('../source/config');
const openPromise = import('open');
const path = require('path');
const child_process = require('child_process');
const { encodePath } = require('../source/address-parser');

const BugTracker = require('../source/bugtracker');
const bugtracker = new BugTracker('launcher');
// Fastest way to find out if a port is used or not/i.e. if ungit is running
const net = require('net');
const server = net.createServer();
let child;
const cleanExit = () => {
  if (child) {
    child.kill('SIGINT');
  }
  process.exit();
};

process.on('SIGINT', cleanExit); // catch ctrl-c
process.on('SIGTERM', cleanExit); // catch kill
process.on('uncaughtException', (err) => {
  console.error(err.stack.toString());

  bugtracker.notify(err, 'ungit-launcher');
  cleanExit();
});

const openUngitBrowser = (pathToNavigateTo) => {
  console.log(`Navigate to ${pathToNavigateTo}`);
  return openPromise
    .then((open) => {
      return open.default(pathToNavigateTo);
    })
    .catch((err) => console.log(`failed to navigate to ${pathToNavigateTo}`, err));
};

const navigate = () => {
  let url = config.urlBase + ':' + config.port;
  if (config.forcedLaunchPath === undefined) {
    url += '/#/repository?path=' + encodePath(process.cwd());
  } else if (config.forcedLaunchPath !== null && config.forcedLaunchPath !== '') {
    url += '/#/repository?path=' + encodePath(config.forcedLaunchPath);
  }

  if (config.launchCommand) {
    const command = config.launchCommand.replace(/%U/g, url);
    console.log(`Running custom launch command: ${command}`);
    child_process.exec(command, (err, stdout, stderr) => {
      if (err) {
        console.log('Failed to exec custom launchCommand', err, stderr);
        return;
      }
      if (config.launchBrowser) {
        openUngitBrowser(url);
      }
    });
  } else if (config.launchBrowser) {
    openUngitBrowser(url);
  }
};

const launch = () => {
  child = child_process.fork(
    path.join(__dirname, '..', 'source', 'server.js'),
    process.argv.slice(2),
    { cwd: path.join(process.cwd(), '..'), silent: true }
  );

  child.on('exit', (res) => {
    console.log('Stopped keeping ungit alive');
  });

  const startupListener = (data) => {
    if (data.toString().indexOf('## Ungit started ##') >= 0) {
      child.removeListener('stdout', startupListener);
      child.stdout.on('data', (data) => console.log(data.toString().trim()));
      const launchTime = Date.now() - startLaunchTime;
      console.log(data.toString());
      console.log(`Took ${launchTime}ms to start server.`);
      navigate();
    }
  };

  child.stdout.on('data', startupListener);
  child.stderr.on('data', (data) => console.log(`stderr: ${data.toString().trim()}`));
};

server.listen({ port: config.port, host: config.ungitBindIp }, (err) => {
  server.close(launch);
});
server.on('error', (e) => {
  if (e.code == 'EADDRINUSE') {
    console.log('Ungit server already running');
    navigate();
  } else {
    console.error('Failed to run server: ', e);
    process.exit(1);
  }
});


================================================
FILE: clicktests/environment.js
================================================
'use strict';
const logger = require('../source/utils/logger');
const child_process = require('child_process');
const puppeteer = require('puppeteer');
const request = require('superagent');
const mkdirp = require('mkdirp').mkdirp;
const rimraf = require('rimraf').rimraf;
const { encodePath } = require('../source/address-parser');
const portfinder = require('portfinder');
const portrange = 45032;

module.exports = (config) => new Environment(config);

const prependLines = (pre, text) => {
  return text
    .split('\n')
    .filter((l) => l)
    .map((line) => pre + line)
    .join('\n');
};

// Environment provides
class Environment {
  constructor(config) {
    this.config = config || {};
    this.config.rootPath = typeof this.config.rootPath === 'string' ? this.config.rootPath : '';
    this.config.serverTimeout = this.config.serverTimeout || 35000;
    this.config.headless = this.config.headless === undefined ? true : this.config.headless;
    this.config.viewWidth = 1920;
    this.config.viewHeight = 1080;
    this.config.serverStartupOptions = this.config.serverStartupOptions || [];
    this.shuttinDown = false;
  }

  getRootUrl() {
    return this.rootUrl;
  }

  async init() {
    try {
      this.browser = await puppeteer.launch({
        headless: this.config.headless,
        defaultViewport: {
          width: this.config.viewWidth,
          height: this.config.viewHeight,
        },
      });
      await this.startServer();
      await new Promise((resolve) => setTimeout(resolve, 1000));
    } catch (err) {
      logger.error(err);
      throw new Error('Cannot confirm ungit start!!', { cause: err });
    }
  }

  async startServer() {
    this.port = await portfinder.getPortPromise({ port: portrange });
    this.rootUrl = `http://127.0.0.1:${this.port}${this.config.rootPath}`;
    logger.info(`Starting ungit server:${this.port} with ${this.config.serverStartupOptions}`);

    this.hasStarted = false;
    const options = [
      'bin/ungit',
      '--cliconfigonly',
      `--port=${this.port}`,
      `--rootPath=${this.config.rootPath}`,
      '--no-launchBrowser',
      '--dev',
      '--no-bugtracking',
      `--autoShutdownTimeout=${this.config.serverTimeout}`,
      '--logLevel=debug',
      '--maxNAutoRestartOnCrash=0',
      '--no-autoCheckoutOnBranchCreate',
      '--alwaysLoadActiveBranch',
      `--numRefsToShow=${this.config.numRefsToShow || 5}`,
    ].concat(this.config.serverStartupOptions);

    const ungitServer = (this.ungitServerProcess = child_process.spawn('node', options));

    return new Promise((resolve, reject) => {
      ungitServer.stdout.on('data', (stdout) => {
        const stdoutStr = stdout.toString();
        console.log(prependLines('[server] ', stdoutStr));

        if (stdoutStr.indexOf('Ungit server already running') >= 0) {
          logger.info('server-already-running');
        }

        if (stdoutStr.indexOf('## Ungit started ##') >= 0) {
          if (this.hasStarted) {
            reject(new Error('Ungit started twice, probably crashed.'));
          } else {
            this.hasStarted = true;
            logger.info('Ungit server started.');
            resolve();
          }
        }
      });
      ungitServer.stderr.on('data', (stderr) => {
        const stderrStr = stderr.toString();
        logger.error(prependLines('[server ERROR] ', stderrStr));
        if (stderrStr.indexOf('EADDRINUSE') > -1) {
          logger.info('retrying with different port');
          ungitServer.kill('SIGINT');
          reject(new Error('EADDRINUSE'));
        }
      });
      ungitServer.on('exit', () => logger.info('UNGIT SERVER EXITED'));
    });
  }

  async shutdown() {
    this.shuttinDown = true;

    await this.backgroundAction('POST', '/api/testing/cleanup');

    if (this.ungitServerProcess) {
      this.ungitServerProcess.kill('SIGINT');
      this.ungitServerProcess = null;
    }

    if (this.browser) {
      await this.browser.close();
      this.browser = null;
      this.page = null;
    }
  }

  // server helpers

  async backgroundAction(method, url, body) {
    url = this.getRootUrl() + url;

    let req;
    if (method === 'GET') {
      req = request.get(url).withCredentials().query(body);
    } else if (method === 'POST') {
      req = request.post(url).send(body);
    } else if (method === 'DELETE') {
      req = request.delete(url).send(body);
    }

    req.set({ encoding: 'utf8', 'cache-control': 'no-cache', 'Content-Type': 'application/json' });

    const response = await req;
    return response.body;
  }

  async createRepos(testRepoPaths, config) {
    for (let i = 0; i < config.length; i++) {
      const conf = config[i];
      conf.bare = !!conf.bare;
      await this.initRepo(conf);
      await this.createCommits(conf, conf.initCommits);
      testRepoPaths.push(conf.path);
    }
  }

  async initRepo(options) {
    if (options.path) {
      await rimraf(options.path);
      await mkdirp(options.path);
    } else {
      logger.info('Creating temp folder');
      options.path = await this.createTempFolder();
    }
    await this.backgroundAction('POST', '/api/init', options);
  }

  async createTempFolder() {
    const res = await this.backgroundAction('POST', '/api/testing/createtempdir');
    return res.path;
  }

  async createCommits(config, limit, x) {
    x = x || 0;
    if (!limit || limit < 0 || x === limit) return;

    await this.createTestFile(`${config.path}/testy${x}`);
    await this.backgroundAction('POST', '/api/commit', {
      path: config.path,
      message: `Init Commit ${x}`,
      files: [{ name: `testy${x}` }],
    });
    // `createCommits()` is used at create repo `this.page` may not be inited
    await this.createCommits(config, limit, x + 1);
  }

  async createTestFile(filename, repoPath) {
    await this.backgroundAction('POST', '/api/testing/createfile', {
      file: filename,
      path: repoPath,
    });
  }

  // browser helpers

  async goto(url) {
    logger.info('Go to page: ' + url);

    if (!this.page) {
      const pages = await this.browser.pages();
      this.page = pages[0];
      this.page.on('console', (message) => {
        const text = `[ui ${message.type()}] ${message.text()}`;

        if (message.type() === 'error' && !this.shuttinDown) {
          const stackTraceString = message
            .stackTrace()
            .map((trace) => `\t${trace.lineNumber}: ${trace.url}`)
            .join('\n');
          logger.error(text, stackTraceString);
        } else {
          // text already has timestamp and etc as it is generated by logger as well.
          console.log(text);
        }
      });
    }

    await this.page.goto(url);
  }

  async openUngit(tempDirPath) {
    await this.goto(`${this.getRootUrl()}/#/repository?path=${encodePath(tempDirPath)}`);
    await this.waitForElementVisible('.repository-actions');
    await this.page.waitForNetworkIdle();
  }

  waitForElementVisible(selector, timeout) {
    logger.debug(`Waiting for visible: "${selector}"`);
    return this.page.waitForSelector(selector, { visible: true, timeout: timeout || 6000 });
  }
  waitForElementHidden(selector, timeout) {
    logger.debug(`Waiting for hidden: "${selector}"`);
    return this.page.waitForSelector(selector, { hidden: true, timeout: timeout || 6000 });
  }
  wait(duration) {
    return new Promise((resolve) => setTimeout(resolve, duration));
  }

  type(text) {
    return this.page.keyboard.type(text);
  }
  async insert(selector, text) {
    await this.waitForElementVisible(selector);
    await this.page.$eval(selector, (ele) => (ele.value = ''));
    await this.page.focus(selector);
    await this.type(text);
  }
  press(key) {
    return this.page.keyboard.press(key);
  }

  async click(selector, clickCount) {
    logger.info(`clicking "${selector}"`);

    for (let i = 0; i < 3; i++) {
      try {
        const toClick = await this.waitForElementVisible(selector);
        await this.wait(200);
        await toClick.click({ delay: 100, clickCount: clickCount });
        break;
      } catch (err) {
        logger.error('error while clicking', err);
      }
    }
    logger.info(`clicked "${selector}`);
  }

  waitForBranch(branchName) {
    const currentBranch = 'document.querySelector(".ref.branch.current")';
    return this.page.waitForFunction(
      `${currentBranch} && ${currentBranch}.innerText && ${currentBranch}.innerText.trim() === "${branchName}"`,
      { polling: 250 }
    );
  }

  async commit(commitMessage) {
    await this.waitForElementVisible('.files .file .btn-default');
    await this.insert('.staging input.form-control', commitMessage);
    const postCommitProm = this.setApiListener('/commit', 'POST');
    await this.click('.commit-btn');
    await postCommitProm;
    await this.waitForElementHidden('.files .file .btn-default');
  }

  async _createRef(type, name) {
    await this.click('.current ~ .new-ref button.showBranchingForm');
    await this.insert('.ref-icons.new-ref.editing input', name);
    await this.wait(500);
    const createRefProm =
      type === 'branch'
        ? this.setApiListener('/branches', 'POST')
        : this.setApiListener('/tags', 'POST');
    await this.click(`.new-ref ${type === 'branch' ? '.btn-primary' : '.btn-default'}`);
    await createRefProm;
    await this.waitForElementVisible(`.ref.${type}[data-ta-name="${name}"]`);
    await this.ensureRedraw();
  }
  createTag(name) {
    return this._createRef('tag', name);
  }
  createBranch(name) {
    return this._createRef('branch', name);
  }

  async _verifyRefAction(action) {
    try {
      await this.page.waitForSelector('.modal-dialog .btn-primary', {
        visible: true,
        timeout: 2000,
      }); // not all ref actions opens dialog, this line may throw exception.
      await this.awaitAndClick('.modal-dialog .btn-primary');
    } catch {
      /* ignore */
    }
    await this.waitForElementHidden(`[data-ta-action="${action}"]:not([style*="display: none"])`);
    await this.ensureRedraw();
  }

  async _refAction(ref, local, action, validateFunc) {
    if (!this[`_${action}ResponseWatcher`]) {
      this.page.on('response', async (response) => {
        const url = response.url();
        const method = response.request().method();

        if (validateFunc(url, method)) {
          this.page.evaluate(`ungit._${action}Response = true`);
        }
      });
      this[`_${action}ResponseWatcher`] = true;
    }
    await this.clickOnNode(`.branch[data-ta-name="${ref}"][data-ta-local="${local}"]`);
    await this.click(`[data-ta-action="${action}"]:not([style*="display: none"]) .dropmask`);
    await this._verifyRefAction(action);
    await this.page.waitForFunction(`ungit._${action}Response`, { polling: 250 });
    await this.page.evaluate(`ungit._${action}Response = undefined`);
  }

  async pushRefAction(ref, local) {
    await this._refAction(ref, local, 'push', (url, method) => {
      if (method !== 'POST') {
        return false;
      }
      if (
        url.indexOf('/push') === -1 &&
        url.indexOf('/tags') === -1 &&
        url.indexOf('/branches') === -1
      ) {
        return false;
      }
      return true;
    });
  }

  async rebaseRefAction(ref, local) {
    await this._refAction(ref, local, 'rebase', (url, method) => {
      return method === 'POST' && url.indexOf('/rebase') >= -1;
    });
  }

  async mergeRefAction(ref, local) {
    await this._refAction(ref, local, 'merge', (url, method) => {
      return method === 'POST' && url.indexOf('/merge') >= -1;
    });
  }

  async moveRef(ref, targetNodeCommitTitle) {
    await this.clickOnNode(`.branch[data-ta-name="${ref}"]`);
    if (!this._isMoveResponseWatcherSet) {
      this.page.on('response', async (response) => {
        const url = response.url();
        if (response.request().method() !== 'POST') {
          return;
        }
        if (
          url.indexOf('/reset') === -1 &&
          url.indexOf('/tags') === -1 &&
          url.indexOf('/branches') === -1
        ) {
          return;
        }
        this.page.evaluate('ungit._moveEventResponded = true');
      });
      this._isMoveResponseWatcherSet = true;
    }
    await this.click(
      `[data-ta-node-title="${targetNodeCommitTitle}"] [data-ta-action="move"]:not([style*="display: none"]) .dropmask`
    );
    await this._verifyRefAction('move');
    await this.page.waitForFunction('ungit._moveEventResponded', { polling: 250 });
    await this.page.evaluate('ungit._moveEventResponded = undefined');
  }

  // Explicitly trigger two program events.
  // Usually these events are triggered by mouse movements, or api calls
  // and etc.  This function is to help mimic those movements.
  triggerProgramEvents() {
    return this.page.evaluate(() => {
      const isActive = ungit.programEvents.active;
      if (!isActive) {
        ungit.programEvents.active = true;
      }
      ungit.programEvents.dispatch({ event: 'working-tree-changed' });
      if (!isActive) {
        ungit.programEvents.active = false;
      }
    });
  }

  async ensureRedraw() {
    logger.debug('ensureRedraw triggered');
    if (!this._gitlogResposneWatcher) {
      this.page.on('response', async (response) => {
        if (response.url().indexOf('/gitlog') > 0 && response.request().method() === 'GET') {
          this.page.evaluate('ungit._gitlogResponse = true');
        }
      });
      this._gitlogResposneWatcher = true;
    }
    await this.page.evaluate('ungit._gitlogResponse = undefined');
    await this.triggerProgramEvents();
    await this.page.waitForFunction('ungit._gitlogResponse', { polling: 250 });
    await this.page.waitForFunction(
      'ungit.__app.content().repository().graph._isLoadNodesFromApiRunning === false',
      { polling: 250 }
    );
    logger.debug('ensureRedraw finished');
  }

  async awaitAndClick(selector, time = 1000) {
    await this.wait(time);
    await this.click(selector);
  }

  // After a click on `git-node` or `git-ref`, ensure `currentActionContext` is set
  async clickOnNode(nodeSelector) {
    await this.awaitAndClick(nodeSelector);
    await this.page.waitForFunction(
      () => {
        const app = ungit.__app;
        if (!app) {
          return;
        }
        const path = app.content();
        if (!path || path.constructor.name !== 'PathViewModel') {
          return;
        }
        const repository = path.repository();
        if (!repository) {
          return;
        }
        const graph = repository.graph;
        if (!graph) {
          return;
        }
        return graph.currentActionContext();
      },
      { polling: 250 }
    );
    logger.debug(`clickOnNode ${nodeSelector} finished`);
  }

  // If an api call matches `apiPart` and `method` is called, set the `globalVarName`
  // to true. Use for detect if an API call was made and responded.
  setApiListener(apiPart, method, bodyMatcher = () => true) {
    const randomVariable = `ungit._${Math.floor(Math.random() * 500000)}`;
    this.page.on(
      'response',
      async (response) => {
        if (response.url().indexOf(apiPart) > -1 && response.request().method() === method) {
          if (bodyMatcher(await response.json())) {
            // reponse body matcher is matched, set the value to true
            this.page.evaluate(`${randomVariable} = true`);
          }
        }
      },
      { polling: 250 }
    );
    return this.page
      .waitForFunction(`${randomVariable} === true`, { polling: 250 })
      .then(() => this.page.evaluate(`${randomVariable} = undefined`));
  }
}


================================================
FILE: clicktests/spec.authentication.js
================================================
'use strict';
const testuser = { username: 'testuser', password: 'testpassword' };
const environment = require('./environment')({
  serverStartupOptions: ['--authentication', `--users.${testuser.username}=${testuser.password}`],
  showServerOutput: true,
});

describe('[AUTHENTICATION]', () => {
  before('Environment init without temp folder', () => environment.init());

  after('Environment stop', () => environment.shutdown());

  it('Open home screen should show authentication dialog', async function () {
    this.retries(3);
    await environment.goto(environment.getRootUrl());
    await environment.waitForElementVisible('.login');
  });

  it('Filling out the authentication with wrong details should result in an error', async function () {
    this.retries(3);
    await environment.insert('.login #inputUsername', testuser.username);
    await environment.insert('.login #inputPassword', 'notthepassword');
    await environment.click('.login button');
    await environment.waitForElementVisible('.login .loginError');
  });

  it('Filling out the authentication should bring you to the home screen', async function () {
    this.retries(3);
    await environment.insert('.login #inputUsername', testuser.username);
    await environment.insert('.login #inputPassword', testuser.password);
    await environment.click('.login button');
    await environment.waitForElementVisible('.container.home');
  });
});


================================================
FILE: clicktests/spec.bare.js
================================================
'use strict';
const environment = require('./environment')();
const testRepoPaths = [];

describe('[BARE]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: true }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[0]);
  });

  it('update branches button without branches', async () => {
    const apiResponseProm = environment.setApiListener('/branches?', 'GET');
    const refResponseProm = environment.setApiListener('/refs?', 'GET');
    await environment.click('.btn-group.branch .btn-main');
    await apiResponseProm;
    await refResponseProm;
  });
});


================================================
FILE: clicktests/spec.branches.js
================================================
'use strict';
const environment = require('./environment')();
const testRepoPaths = [];
const _ = require('lodash');

describe('[BRANCHES]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[0]);
  });

  it('add a commit', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.commit('commit-1');
  });

  // < branch search test >
  it('add branches', async () => {
    await environment.createBranch('search-1');
    await environment.createBranch('search-2');
    await environment.createBranch('search-3');
    await environment.createBranch('search-4');
    await environment.waitForElementVisible('[data-ta-name="search-4"]');
  });

  it('add tag should make one of the branch disappear', async () => {
    const branchesResponse = environment.setApiListener('/tags', 'POST');
    await environment.createTag('tag-1');
    await branchesResponse;
    await environment.waitForElementHidden('[data-ta-name="search-4"]');
  });

  it('search for the hidden branch', async () => {
    await environment.awaitAndClick('.showSearchForm');
    await environment.wait(500);
    await environment.type('-4');
    await environment.waitForElementVisible('.branch-search');
    await environment.page.waitForFunction(
      'document.querySelectorAll(".ui-menu-item-wrapper").length > 0 && document.querySelectorAll(".ui-menu-item-wrapper")[0].text.trim() === "search-4"',
      { polling: 250 }
    );
    await environment.press('ArrowDown');
    await environment.press('Enter');

    await environment.waitForElementVisible('[data-ta-name="search-4"]', 10000);
  });

  it('updateBranches button without branches', async () => {
    const branchesResponse = environment.setApiListener('/branches?', 'GET', (body) => {
      return _.isEqual(body, [
        { name: 'master', current: true },
        { name: 'search-1' },
        { name: 'search-2' },
        { name: 'search-3' },
        { name: 'search-4' },
      ]);
    });
    const refsResponse = environment.setApiListener('/refs?', 'GET', (body) => {
      body.forEach((ref) => delete ref.sha1);
      return _.isEqual(body, [
        {
          name: 'refs/heads/master',
        },
        {
          name: 'refs/heads/search-1',
        },
        {
          name: 'refs/heads/search-2',
        },
        {
          name: 'refs/heads/search-3',
        },
        {
          name: 'refs/heads/search-4',
        },
        {
          name: 'refs/tags/tag-1',
        },
      ]);
    });
    await environment.click('.btn-group.branch .btn-main');
    await branchesResponse;
    await refsResponse;
  });

  it('add a branch', () => {
    return environment.createBranch('branch-1');
  });

  it('updateBranches button with one branch', async () => {
    const branchesResponse = environment.setApiListener('/branches?', 'GET', (body) => {
      return _.isEqual(body, [
        { name: 'branch-1' },
        { name: 'master', current: true },
        { name: 'search-1' },
        { name: 'search-2' },
        { name: 'search-3' },
        { name: 'search-4' },
      ]);
    });
    const refsResponse = environment.setApiListener('/refs?', 'GET', (body) => {
      body.forEach((ref) => delete ref.sha1);
      return _.isEqual(body, [
        { name: 'refs/heads/branch-1' },
        {
          name: 'refs/heads/master',
        },
        {
          name: 'refs/heads/search-1',
        },
        {
          name: 'refs/heads/search-2',
        },
        {
          name: 'refs/heads/search-3',
        },
        {
          name: 'refs/heads/search-4',
        },
        {
          name: 'refs/tags/tag-1',
        },
      ]);
    });
    await environment.click('.btn-group.branch .btn-main');
    await branchesResponse;
    await refsResponse;
  });

  it('add second branch', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);
    await environment.commit('commit-2');

    await environment.createBranch('branch-2');
    await environment.createBranch('branch-3');
  });

  it('Check out a branch via selection', async () => {
    await environment.click('.branch .dropdown-toggle');
    await environment.click('[data-ta-clickable="checkoutrefs/heads/branch-2"]');
    await environment.waitForElementVisible('[data-ta-name="branch-2"].current');
  });

  it('Delete a branch via selection', async () => {
    const branchDeleteResponse = environment.setApiListener('/branches?', 'DELETE');
    await environment.click('.branch .dropdown-toggle');
    await environment.click('[data-ta-clickable="refs/heads/branch-3-remove"]');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await branchDeleteResponse;
  });

  it('add another commit', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);
    await environment.commit('commit-3');
    await environment.ensureRedraw();
  });

  it('checkout cherrypick base', async () => {
    const checkoutResponse = environment.setApiListener('/checkout', 'POST');
    await environment.click('.branch .dropdown-toggle');
    await environment.click('[data-ta-clickable="checkoutrefs/heads/branch-1"]');
    await checkoutResponse;
    await environment.ensureRedraw();
    await environment.waitForElementVisible('[data-ta-name="branch-1"].current');
  });

  it('cherrypick abort case', async () => {
    await environment.wait(1000);
    await environment.clickOnNode('[data-ta-clickable="node-clickable-0"]');
    await environment.awaitAndClick(
      '[data-ta-action="cherry-pick"]:not([style*="display: none"]) .dropmask'
    );
    await environment.click('.staging .btn-stg-abort');
    await environment.awaitAndClick('.modal-dialog .btn-primary', 2000);
    const gitlogResponse = environment.setApiListener('/gitlog', 'GET', (body) => {
      return _.isEqual(
        body.nodes.map((node) => node.message),
        ['commit-3', 'commit-2', 'commit-1']
      );
    });
    await environment.ensureRedraw();
    await gitlogResponse;
  });

  it('cherrypick success case', async () => {
    const cherrypickPostResponed = environment.setApiListener('/cherrypick', 'POST');
    await environment.clickOnNode('[data-ta-clickable="node-clickable-1"]');
    await environment.click(
      '[data-ta-action="cherry-pick"]:not([style*="display: none"]) .dropmask'
    );
    await cherrypickPostResponed;
    const cherrypickGitlogResponse = environment.setApiListener('/gitlog', 'GET', (body) => {
      return _.isEqual(
        body.nodes.map((node) => node.message),
        ['commit-2', 'commit-3', 'commit-2', 'commit-1']
      );
    });
    await environment.ensureRedraw();
    await cherrypickGitlogResponse;
    await environment.waitForElementVisible('[data-ta-node-title="commit-2"] .ref.branch.current');
  });

  it('test backward squash from own lineage', async () => {
    await environment.wait(1000);
    await environment.waitForBranch('branch-1');
    await environment.clickOnNode('.ref.branch.current');
    await environment.click('[data-ta-node-title="commit-1"] .squash .dropmask');
    await environment.waitForElementVisible('.staging .files .file');
    await environment.click('.files button.discard');
    await environment.awaitAndClick('.modal-dialog .btn-primary', 2000);
    await environment.ensureRedraw();
    await environment.waitForElementHidden('.staging .files .file');
  });

  it('test forward squash from different lineage', async () => {
    await environment.clickOnNode('.ref.branch.current');
    await environment.click('[data-ta-node-title="commit-3"] .squash .dropmask');
    await environment.ensureRedraw();
    await environment.waitForElementVisible('.staging .files .file');
  });

  it('Auto checkout on branch creation.', async () => {
    await environment.page.evaluate(() => (ungit.config.autoCheckoutOnBranchCreate = true));
    await environment.createBranch('autoCheckout');
    await environment.waitForElementVisible('[data-ta-name="autoCheckout"].current');
  });
});


================================================
FILE: clicktests/spec.commands.js
================================================
'use strict';
const environment = require('./environment')();
const testRepoPaths = [];
const _ = require('lodash');

const gitCommand = (options) => {
  return environment.backgroundAction('POST', '/api/testing/git', options);
};
const testForBranchMove = async (branch, command) => {
  const branchTagLoc = await environment.page.$eval(branch, (element) =>
    JSON.stringify(element.getBoundingClientRect())
  );

  await gitCommand({ command: command, path: testRepoPaths[0] });

  await environment.page.waitForFunction(
    (branch, oldLoc) => {
      const newLoc = document.querySelector(branch).getBoundingClientRect();
      return newLoc.top !== oldLoc.top || newLoc.left !== oldLoc.left;
    },
    { timeout: 6000, polling: 250 },
    branch,
    JSON.parse(branchTagLoc)
  );
};

describe('[COMMANDS]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[0]);
  });

  it('add a branch-1', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.commit('commit-1');
    await environment.createBranch('branch-1');
  });

  it('add a branch-2', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.commit('commit-1');
    await environment.createBranch('branch-2');
  });

  it('test branch create from command line', async () => {
    await gitCommand({ command: ['branch', 'gitCommandBranch'], path: testRepoPaths[0] });
    await environment.waitForElementVisible('[data-ta-name="gitCommandBranch"]');
  });

  it('test branch move from command line', () => {
    return testForBranchMove('[data-ta-name="gitCommandBranch"]', [
      'branch',
      '-f',
      'gitCommandBranch',
      'branch-1',
    ]);
  });

  it('test branch delete from command line', async () => {
    const brachesResponseProm = environment.setApiListener('/branches?', 'GET', (body) => {
      return _.isEqual(body, [
        { name: 'branch-1' },
        { name: 'branch-2' },
        { name: 'master', current: true },
      ]);
    });
    await gitCommand({ command: ['branch', '-D', 'gitCommandBranch'], path: testRepoPaths[0] });
    await brachesResponseProm;
    await environment.waitForElementHidden('[data-ta-name="gitCommandBranch"]', 10000);
  });

  it('test tag create from command line', async () => {
    const refsResponseProm = environment.setApiListener('/refs?', 'GET', (body) => {
      body.forEach((ref) => delete ref.sha1);
      return _.isEqual(body, [
        { name: 'refs/heads/branch-1' },
        { name: 'refs/heads/branch-2' },
        { name: 'refs/heads/master' },
        { name: 'refs/tags/tag1' },
      ]);
    });
    await gitCommand({ command: ['tag', 'tag1'], path: testRepoPaths[0] });
    await refsResponseProm;
    await environment.waitForElementVisible('[data-ta-name="tag1"]', 10000);
  });

  it('test tag delete from command line', async () => {
    const refDeleteResponseProm = environment.setApiListener('/refs?', 'GET', (body) => {
      body.forEach((ref) => delete ref.sha1);
      return _.isEqual(body, [
        { name: 'refs/heads/branch-1' },
        { name: 'refs/heads/branch-2' },
        { name: 'refs/heads/master' },
      ]);
    });
    await gitCommand({ command: ['tag', '-d', 'tag1'], path: testRepoPaths[0] });
    await refDeleteResponseProm;
    await environment.waitForElementHidden('[data-ta-name="tag1"]', 10000);
  });

  it('test reset from command line', () => {
    return testForBranchMove('[data-ta-name="branch-1"]', ['reset', 'branch-1']);
  });
});


================================================
FILE: clicktests/spec.discard.js
================================================
'use strict';

const muteGraceTimeDuration = 5000;
const createAndDiscard = async (env, testRepoPath, dialogButtonToClick) => {
  await env.createTestFile(testRepoPath + '/testfile2.txt', testRepoPath);
  await env.ensureRedraw();
  await env.waitForElementVisible('.files .file .btn-default');

  await env.click('.files button.discard');
  if (dialogButtonToClick === 'yes') {
    await env.awaitAndClick('.modal-dialog [data-ta-action="yes"]');
  } else if (dialogButtonToClick === 'mute') {
    await env.awaitAndClick('.modal-dialog [data-ta-action="mute"]');
  } else if (dialogButtonToClick === 'no') {
    await env.awaitAndClick('.modal-dialog [data-ta-action="no"]');
  } else {
    await env.waitForElementHidden('.modal-dialog [data-ta-action="yes"]');
  }
  if (dialogButtonToClick !== 'no') {
    await env.ensureRedraw();
    await env.waitForElementHidden('.files .file .btn-default');
  } else {
    await env.waitForElementVisible('.files .file .btn-default');
  }
};

describe('[DISCARD - noWarn]', () => {
  const environment = require('./environment')({
    serverStartupOptions: ['--disableDiscardWarning'],
  });
  const testRepoPaths = [];

  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[0]);
  });

  it('Should be possible to discard a created file without warning message', () => {
    return createAndDiscard(environment, testRepoPaths[0]);
  });
});

describe('[DISCARD - withWarn]', () => {
  const environment = require('./environment')({
    serverStartupOptions: [
      '--no-disableDiscardWarning',
      '--disableDiscardMuteTime=' + muteGraceTimeDuration,
    ],
  });
  const testRepoPaths = [];

  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[0]);
  });

  it('Should be possible to select no from discard', () => {
    return createAndDiscard(environment, testRepoPaths[0], 'no');
  });

  it('Should be possible to discard a created file', () => {
    return createAndDiscard(environment, testRepoPaths[0], 'yes');
  });

  it('Should be possible to discard a created file and disable warn for awhile', async () => {
    await createAndDiscard(environment, testRepoPaths[0], 'mute');
    const start = new Date().getTime(); // this is when the "mute" timestamp is stamped
    await createAndDiscard(environment, testRepoPaths[0]);
    // ensure, at least 2 seconds has passed since mute timestamp is stamped
    const end = new Date().getTime();
    const diff = muteGraceTimeDuration + 500 - (end - start);
    if (diff > 0) {
      await environment.wait(diff);
    }
    await createAndDiscard(environment, testRepoPaths[0], 'yes');
  });
});


================================================
FILE: clicktests/spec.generic.js
================================================
'use strict';
const environment = require('./environment')({
  serverStartupOptions: ['--no-disableDiscardWarning'],
  rootPath: '/deep/root/path/to/app',
});
const mkdirp = require('mkdirp').mkdirp;
const rimraf = require('rimraf').rimraf;
const testRepoPaths = [];

const changeTestFile = async (filename, repoPath) => {
  await environment.backgroundAction('POST', '/api/testing/changefile', {
    file: filename,
    path: repoPath,
  });
  await environment.ensureRedraw();
};
const amendCommit = async () => {
  try {
    await environment.page.waitForSelector('.amend-button', { visible: true, timeout: 2000 });
    await environment.click('.amend-button');
  } catch {
    await environment.click('.amend-link');
  }
  await environment.ensureRedraw();
  await environment.click('.commit-btn');
  await environment.ensureRedraw();
  await environment.waitForElementHidden('.files .file .btn-default');
};

describe('[GENERIC]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false }]);

    // create a sub dir and change working dir to sub dir to prove functionality within subdir
    testRepoPaths.push(`${testRepoPaths[0]}/asubdir`);
    await rimraf(testRepoPaths[1]);
    await mkdirp(testRepoPaths[1]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open repo screen', () => {
    return environment.openUngit(testRepoPaths[1]);
  });

  it('Check for refresh button', () => {
    return environment.click('.refresh-button');
  });

  it('Should be possible to create and commit a file', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.commit('Init');
    await environment.waitForElementVisible('.commit');
  });

  it('Should be possible to amend a file', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.waitForElementVisible('.files .file .btn-default');
    await amendCommit();
    await environment.waitForElementVisible('.commit');
  });

  it('Should be possible to cancel amend a file', async () => {
    await environment.click('.amend-link');
    await environment.click('.btn-stg-cancel');
    await environment.waitForElementVisible('.empty-commit-link');
  });

  it('Should be able to add a new file to .gitignore', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/addMeToIgnore.txt`, testRepoPaths[0]);
    await environment.waitForElementVisible('.files .file .btn-default');
    await environment.page.waitForFunction(
      'document.querySelectorAll(".files .file .btn-default").length === 1',
      { polling: 250 }
    );
    await environment.click('.files button.ignore');
    await environment.page.waitForFunction(
      'document.querySelector(".name.btn.btn-default").innerText.trim() === ".gitignore"',
      { polling: 250 }
    );
    await environment.click('.files button.ignore');
    await environment.waitForElementHidden('.files .file .btn-default');
  });

  it('Test showing commit diff between two commits', async () => {
    await environment.clickOnNode('[data-ta-clickable="node-clickable-0"]');
    await environment.waitForElementVisible('.diff-wrapper');
    await environment.click('.commit-diff-filename');
    await environment.waitForElementVisible('.commit-line-diffs');
  });

  it('Test showing commit side by side diff between two commits', async () => {
    await environment.click('.commit-sideBySideDiff');
    await environment.waitForElementVisible('.commit-line-diffs');
  });

  it('Test wordwrap', async () => {
    await environment.click('.commit-wordwrap');
    await environment.waitForElementVisible('.word-wrap');
  });

  it('Test whitespace', async () => {
    await environment.click('.commit-whitespace');
    await environment.click('[data-ta-clickable="node-clickable-0"]');
  });

  it('Should be possible to discard a created file and ensure patching is not available for new file', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);
    await environment.waitForElementVisible('.files .file .btn-default');
    await environment.click('.files button');
    await environment.waitForElementHidden('[data-ta-container="patch-file"]');
    await environment.click('.files button.discard');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await environment.waitForElementHidden('.files .file .btn-default');
  });

  it('Should be possible to create a branch', async () => {
    await environment.createBranch('testbranch');
  });

  it('Should be possible to create and destroy a branch', async () => {
    await environment.createBranch('willbedeleted');
    await environment.clickOnNode('.branch[data-ta-name="willbedeleted"]');
    await environment.click('[data-ta-action="delete"]:not([style*="display: none"]) .dropmask');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await environment.ensureRedraw();
    await environment.waitForElementHidden('.branch[data-ta-name="willbedeleted"]');
  });

  it('Should be possible to create and destroy a tag', async () => {
    await environment.createTag('tagwillbedeleted');
    await environment.clickOnNode('.graph .ref.tag[data-ta-name="tagwillbedeleted"]');
    await environment.click('[data-ta-action="delete"]:not([style*="display: none"]) .dropmask');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await environment.ensureRedraw();
    await environment.waitForElementHidden('.graph .ref.tag[data-ta-name="tagwillbedeleted"]');
  });

  it('Commit changes to a file', async () => {
    await changeTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.waitForElementVisible('.files .file .btn-default');
    await environment.insert('.staging input.form-control', 'My commit message');
    await environment.click('.commit-btn');
    await environment.ensureRedraw();
    await environment.waitForElementHidden('.files .file .btn-default');
  });

  it('Show stats for changed file and discard it', async () => {
    await changeTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.waitForElementVisible('.files .file .additions');
    await environment.waitForElementVisible('.files .file .deletions');
    await environment.click('.files button.discard');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await environment.ensureRedraw();
    await environment.waitForElementHidden('.files .file .btn-default');
  });

  it.skip('Should be possible to patch a file', async () => {
    await changeTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    //   .patch('patch')
    environment.waitForElementVisible('.commit');
  });

  it('Checkout testbranch with action', async () => {
    await environment.clickOnNode('.branch[data-ta-name="testbranch"]');
    await environment.click('[data-ta-action="checkout"]:not([style*="display: none"]) .dropmask');
    await environment.ensureRedraw();
    await environment.waitForElementVisible('.ref.branch[data-ta-name="testbranch"].current');
  });

  it('Create another commit', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testy2.txt`, testRepoPaths[0]);
    await environment.commit('Branch commit');
    await environment.ensureRedraw();
  });

  it('Rebase', async () => {
    await environment.rebaseRefAction('testbranch', true);
  });

  it('Checkout master with double click', async () => {
    await environment.click('.branch[data-ta-name="master"]', 2);
    await environment.waitForElementVisible('.ref.branch[data-ta-name="master"].current');
  });

  it('Create yet another commit', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testy3.txt`, testRepoPaths[0]);
    await environment.commit('Branch commit');
    await environment.ensureRedraw();
  });

  it('Merge', async () => {
    await environment.mergeRefAction('testbranch', true);
  });

  it('Revert merge', async () => {
    await environment.clickOnNode('[data-ta-clickable="node-clickable-0"]');
    await environment.click('[data-ta-action="revert"]');
    await environment.ensureRedraw();
    await environment.waitForElementVisible(
      '[data-ta-node-title^="Revert \\"Merge branch \'testbranch\'"] .commit-container'
    );
  });

  it('Should be possible to move a branch', async () => {
    await environment.createBranch('movebranch');
    await environment.moveRef('movebranch', 'Init');
  });

  it('Should be possible to cancel creation of an empty commit', async () => {
    await environment.click('.empty-commit-link');
    await environment.click('.btn-stg-cancel');
    await environment.waitForElementVisible('.empty-commit-link');
  });

  it('Should be possible to create an empty commit', async () => {
    await environment.click('.empty-commit-link');
    await environment.click('.commit-btn');
    await environment.waitForElementVisible('.commit');
  });

  it('Should be possible to amend an empty commit', async () => {
    await environment.click('.empty-commit-link');
    await environment.click('.commit-btn');
    await environment.waitForElementVisible('.commit');
    await amendCommit();
    await environment.waitForElementVisible('.commit');
  });

  it('Should be possible to cancel amend of an empty commit', async () => {
    await environment.click('.amend-link');
    await environment.click('.btn-stg-cancel');
    await environment.waitForElementVisible('.empty-commit-link');
  });

  it('Should be possible to click refresh button', () => {
    return environment.click('button.refresh-button');
  });

  it('Go to home screen', async () => {
    await environment.click('.navbar .backlink');
    await environment.waitForElementVisible('.home');
  });
});


================================================
FILE: clicktests/spec.load-ahead.js
================================================
'use strict';
const environment = require('./environment')({
  serverStartupOptions: ['--numberOfNodesPerLoad=1'],
});
const testRepoPaths = [];

describe('[LOAD-AHEAD]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[0]);
  });

  it('Should be possible to create and commit 1', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.commit('commit-1');
    await environment.createBranch('branch-1');
  });

  it('Should be possible to create and commit 2', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile.txt`, testRepoPaths[0]);
    await environment.commit('commit-2');
  });

  it('Should be possible to create and commit 3', async () => {
    await environment.click('.branch .dropdown-toggle');
    await environment.click('[data-ta-clickable="checkoutrefs/heads/branch-1"]');
    await environment.waitForElementVisible('[data-ta-name="branch-1"].current');
  });

  it('Create a branch during collapsed mode', () => {
    return environment.createBranch('new-branch');
  });

  it('Load ahead', async () => {
    await environment.click('.load-ahead-button');
    await environment.waitForElementVisible('[data-ta-clickable="node-clickable-1"]');
    await environment.waitForElementHidden('.loadAhead');
  });
});


================================================
FILE: clicktests/spec.no-header.js
================================================
'use strict';
const environment = require('./environment')();
const testRepoPaths = [];

describe('[NO-HEADER]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', async () => {
    await environment.openUngit(testRepoPaths[0]);
    await environment.waitForElementVisible('.repository-view');
    await environment.waitForElementHidden('[data-ta-container="remote-error-popup"]');
  });

  it('Check for refresh button', async () => {
    await environment.click('.refresh-button');
    await environment.waitForElementHidden('[data-ta-container="remote-error-popup"]');
  });
});


================================================
FILE: clicktests/spec.remotes.js
================================================
'use strict';
const environment = require('./environment')();
const mkdirp = require('mkdirp').mkdirp;
const rimraf = require('rimraf').rimraf;
const { encodePath } = require('../source/address-parser');
const testRepoPaths = [];

describe('[REMOTES]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: true }, { bare: false, initCommits: 2 }]);

    testRepoPaths.push(`${testRepoPaths[1]}-cloned`); // A directory to test cloning
    await rimraf(testRepoPaths[2]); // clean clone test dir
    await mkdirp(testRepoPaths[2]); // create clone test dir
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[1]);
  });

  it('Should not be possible to push without remote', async () => {
    await environment.click('.branch[data-ta-name="master"][data-ta-local="true"]');
    await environment.ensureRedraw();
    await environment.waitForElementHidden('[data-ta-action="push"]:not([style*="display: none"])');
  });

  it('Should not be possible to commit & push without remote', async () => {
    await environment.click('.amend-link');
    await environment.click('.commit-grp .dropdown-toggle');
    await environment.ensureRedraw();
    await environment.waitForElementVisible('.commitnpush.disabled');
  });

  it('Adding a remote', async () => {
    await environment.click('.fetchButton .dropdown-toggle');
    await environment.click('.add-new-remote');

    await environment.insert('.modal #Name', 'myremote');
    await environment.insert('.modal #Url', testRepoPaths[0]);
    await environment.awaitAndClick('.modal .modal-footer .btn-primary');
    await environment.ensureRedraw();
    await environment.click('.fetchButton .dropdown-toggle');
    await environment.waitForElementVisible(
      '.fetchButton .dropdown-menu [data-ta-clickable="myremote"]'
    );
  });

  it('Fetch from newly added remote', async () => {
    const remoteGetResponseProm = environment.setApiListener('/remote/tags?', 'GET');
    await environment.click('.fetchButton .btn-main');
    await remoteGetResponseProm;
  });

  it('Remote delete check', async () => {
    await environment.click('.fetchButton .dropdown-toggle');
    await environment.click('[data-ta-clickable="myremote-remove"]');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await environment.ensureRedraw();
    await environment.click('.fetchButton .dropdown-toggle');
    await environment.waitForElementHidden('[data-ta-clickable="myremote"]');
  });

  // ----------- CLONING -------------
  it('navigate to empty folder path', async () => {
    await environment.goto(
      `${environment.getRootUrl()}/#/repository?path=${encodePath(testRepoPaths[2])}`
    );
    await environment.waitForElementVisible('.uninited');
  });

  it('Clone repository should bring you to repo page', async () => {
    await environment.insert('#cloneFromInput', testRepoPaths[1]);
    await environment.insert('#cloneToInput', testRepoPaths[2]);
    await environment.click('.uninited button[type="submit"]');
    await environment.waitForElementVisible('.repository-view');
    await environment.wait(1000); // ensure click bindings are initialized
  });

  it('Should be possible to fetch', async () => {
    const remoteGetResponseProm = environment.setApiListener('/remote/tags?', 'GET');
    await environment.click('.fetchButton .btn-main');
    await remoteGetResponseProm;
  });

  it('Should be possible to create and push a branch', async () => {
    await environment.createBranch('branchinclone');
    await environment.pushRefAction('branchinclone', true);
    await environment.waitForElementVisible('[data-ta-name="origin/branchinclone"]');
  });

  it('Should be possible to force push a branch', async () => {
    await environment.moveRef('branchinclone', 'Init Commit 0');
    await environment.pushRefAction('branchinclone', true);
    await environment.waitForElementHidden('[data-ta-action="push"]:not([style*="display: none"])');
  });

  it('Check for fetching remote branches for the branch list', async () => {
    await environment.click('.branch .dropdown-toggle');
    await environment.click('.options input');

    await environment.ensureRedraw();

    await environment.click('.options input');
    await environment.waitForElementVisible('li .octicon-globe');
  });

  it('checkout remote branches with matching local branch at wrong place', async () => {
    await environment.moveRef('branchinclone', 'Init Commit 1');
    await environment.click('.branch .dropdown-toggle');
    await environment.click('[data-ta-clickable="checkoutrefs/remotes/origin/branchinclone"]');
    await environment.ensureRedraw();
    await environment.waitForElementVisible('[data-ta-name="branchinclone"][data-ta-local="true"]');
  });

  it('Should be possible to commitnpush', async () => {
    await environment.createTestFile(`${testRepoPaths[2]}/commitnpush.txt`, testRepoPaths[2]);
    await environment.waitForElementVisible('.files .file .btn-default');
    await environment.insert('.staging input.form-control', 'Commit & Push');
    await environment.wait(250);
    await environment.click('.commit-grp .dropdown-toggle');
    await environment.click('.commitnpush');
    await environment.waitForElementVisible(
      '[data-ta-node-title="Commit & Push"] .commit-container'
    );
  });

  it('Should be possible to commitnpush with ff', async () => {
    await environment.click('.amend-link');
    await environment.insert('.staging input.form-control', 'Commit & Push with ff');
    await environment.click('.commit-grp .dropdown-toggle');
    await environment.click('.commitnpush');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await environment.waitForElementVisible(
      '[data-ta-node-title="Commit & Push with ff"] .commit-container'
    );
  });
});


================================================
FILE: clicktests/spec.screens.js
================================================
'use strict';
const environment = require('./environment')();
const mkdirp = require('mkdirp').mkdirp;
const rimraf = require('rimraf').rimraf;
const { encodePath } = require('../source/address-parser');
const testRepoPaths = [];

describe('[SCREENS]', () => {
  before('Environment init', () => environment.init());

  after('Environment stop', () => environment.shutdown());

  it('Open home screen', async () => {
    await environment.goto(environment.getRootUrl());
    await environment.waitForElementVisible('.home');
  });

  it('Open path screen', async () => {
    testRepoPaths.push(await environment.createTempFolder());

    await environment.goto(
      `${environment.getRootUrl()}/#/repository?path=${encodePath(testRepoPaths[0])}`
    );
    await environment.waitForElementVisible('.uninited');
  });

  it('Init repository should bring you to repo page', async () => {
    await environment.click('.uninited button.btn-primary');
    await environment.waitForElementVisible('.repository-view');
  });

  it('Clicking logo should bring you to home screen', async () => {
    await environment.click('.navbar .backlink');
    await environment.waitForElementVisible('.home');
    await environment.wait(1000);
  });

  it('Entering an invalid path and create directory in that location', async () => {
    await environment.insert(
      '.navbar .path-input-form input',
      `${testRepoPaths[0]}-test0/not/existing`
    );
    await environment.press('Enter');
    await environment.waitForElementVisible('.invalid-path');
    await environment.click('.invalid-path button');
    await environment.waitForElementVisible('.uninited button.btn-primary');
    await environment.wait(1000);
  });

  it('Entering an invalid path should bring you to an error screen', async () => {
    await environment.insert('.navbar .path-input-form input', '/a/path/that/doesnt/exist');
    await environment.press('Enter');
    await environment.waitForElementVisible('.invalid-path');
    await environment.wait(1000);
  });

  it('Entering a path to a repo should bring you to that repo', async () => {
    await environment.insert('.navbar .path-input-form input', testRepoPaths[0]);
    await environment.press('Enter');
    await environment.waitForElementVisible('.repository-view');
    await environment.wait(1000);
  });

  // getting odd cross-domain-error.
  it('Create test directory with ampersand and open it', async () => {
    const specialRepoPath = `${testRepoPaths[0]}-test1/test & repo`;

    await rimraf(specialRepoPath);
    await mkdirp(specialRepoPath);

    await environment.goto(
      `${environment.getRootUrl()}/#/repository?path=${encodePath(specialRepoPath)}`
    );

    await environment.waitForElementVisible('.uninited');
  });
});


================================================
FILE: clicktests/spec.stash.js
================================================
'use strict';
const environment = require('./environment')();
const testRepoPaths = [];

describe('[STASH]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [{ bare: false, initCommits: 1 }]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[0]);
  });

  it('Should be possible to stash a file', async () => {
    await environment.createTestFile(`${testRepoPaths[0]}/testfile2.txt`, testRepoPaths[0]);
    await environment.waitForElementVisible('.files .file .btn-default');
    await environment.click('.stash-all');
    await environment.click('.stash-toggle');
    await environment.waitForElementVisible('.stash .list-group-item');
  });

  it('Should be possible to open stash diff', async () => {
    await environment.click('.toggle-show-commit-diffs');
    await environment.waitForElementVisible('.stash .diff-wrapper');
  });

  it('Should be possible to pop a stash', async () => {
    await environment.click('.stash .stash-apply');
    await environment.waitForElementVisible('.files .file .btn-default');
  });
});


================================================
FILE: clicktests/spec.submodules.js
================================================
'use strict';
const environment = require('./environment')();
const testRepoPaths = [];

describe('[SUMBODULES]', () => {
  before('Environment init', async () => {
    await environment.init();
    await environment.createRepos(testRepoPaths, [
      { bare: false, initCommits: 1 },
      { bare: false },
    ]);
  });

  after('Environment stop', () => environment.shutdown());

  it('Open path screen', () => {
    return environment.openUngit(testRepoPaths[1]);
  });

  it('Submodule add', async () => {
    await environment.click('.submodule .dropdown-toggle');
    await environment.click('.fetchButton .add-submodule');

    await environment.insert('.modal #Path', 'subrepo');
    await environment.insert('.modal #Url', testRepoPaths[0]);
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await environment.ensureRedraw();
  });

  it('Submodule update', async () => {
    await environment.click('.submodule .dropdown-toggle');
    await environment.waitForElementVisible(
      '.fetchButton .dropdown-menu [data-ta-clickable="subrepo"]'
    );
    const submoduleResponseProm = environment.setApiListener('/submodules/update', 'POST');
    await environment.awaitAndClick('.fetchButton .update-submodule');
    await submoduleResponseProm;
  });

  it('Submodule delete check', async () => {
    const submoduleDeleteResponseProm = environment.setApiListener('/submodules?', 'DELETE');
    await environment.click('.submodule .dropdown-toggle');
    await environment.click('[data-ta-clickable="subrepo-remove"]');
    await environment.awaitAndClick('.modal-dialog .btn-primary');
    await submoduleDeleteResponseProm;
  });
});


================================================
FILE: components/ComponentRoot.ts
================================================
declare var ungit: any;

export class ComponentRoot {
  _apiCache: string;
  defaultDebounceOption = {
    maxWait: 1500,
    leading: false,
    trailing: true
  }

  constructor() { }

  isSamePayload(value: any) {
    const jsonString = JSON.stringify(value);

    if (this._apiCache === jsonString) {
      ungit.logger.debug(`ignoring redraw for same ${this.constructor.name} payload.`);
      return true;
    }
    ungit.logger.debug(`redrawing ${this.constructor.name} payload.  \n${jsonString}`);

    this._apiCache = jsonString
    return false;
  }

  clearApiCache() {
    this._apiCache = undefined
  }
}


================================================
FILE: components/app/app.html
================================================
<!-- ko component: header -->
<!-- /ko -->

<div class="app">
  <div class="container" data-bind="shown: shown">
    <div class="alert alert-danger" data-bind="visible: gitVersionErrorVisible">
      <span data-bind="text: gitVersionError"></span>
      <button type="button" class="close" data-bind="click: dismissGitVersionError">&times;</button>
    </div>

    <div class="alert alert-info" data-bind="visible: showNewVersionAvailable">
      A new version of ungit (<span data-bind="text: latestVersion"></span>) is
      <a href="https://github.com/FredrikNoren/ungit">available</a>! Run
      <code data-bind="text: newVersionInstallCommand"></code> to install. See what's new in the
      <a href="https://github.com/FredrikNoren/ungit/blob/master/CHANGELOG.md">changelog</a>.
      <button type="button" class="close" data-bind="click: dismissNewVersion">&times;</button>
    </div>

    <div class="alert alert-info clearfix" data-bind="visible: showBugtrackingNagscreen">
      <button type="button" class="close" data-bind="click: dismissBugtrackingNagscreen">
        &times;
      </button>
      <p><strong>Help make ungit better with the press of a button!</strong></p>

      <button class="btn btn-primary" data-bind="click: enableBugtracking">
        Enable automatic bug reports
      </button>
      <button class="btn btn-default" data-bind="click: dismissBugtrackingNagscreen">
        Naah, I&#39;ll skip that
      </button>
    </div>
  </div>

  <!-- ko if: content -->
  <div class="container-fluid" data-bind="component: content"></div>
  <!-- /ko -->
</div>

<!-- ko if: modal -->
<!-- ko template: { name: templateChooser, data: modal } -->
<!-- /ko -->
<!-- /ko -->

================================================
FILE: components/app/app.js
================================================
const ko = require('knockout');
const components = require('ungit-components');
const storage = require('ungit-storage');
const $ = require('jquery');

components.register('app', (args) => {
  return new AppViewModel(args.appContainer, args.server);
});

class AppViewModel {
  constructor(appContainer, server) {
    this.appContainer = appContainer;
    this.server = server;
    this.template = 'app';
    if (window.location.search.indexOf('noheader=true') < 0) {
      this.header = components.create('header', { app: this });
    }
    this.modal = ko.observable(null);
    this.repoList = ko.observableArray(this.getRepoList()); // visitedRepositories is legacy, remove in the next version
    this.repoList.subscribe((newValue) => {
      storage.setItem('repositories', JSON.stringify(newValue));
    });
    this.content = ko.observable(components.create('home', { app: this }));
    this.currentVersion = ko.observable();
    this.latestVersion = ko.observable();
    this.showNewVersionAvailable = ko.observable();
    this.newVersionInstallCommand =
      (ungit.platform == 'win32' ? '' : 'sudo -H ') + 'npm update -g ungit';
    this.bugtrackingEnabled = ko.observable(ungit.config.bugtracking);
    this.bugtrackingNagscreenDismissed = ko.observable(
      storage.getItem('bugtrackingNagscreenDismissed')
    );
    this.showBugtrackingNagscreen = ko.computed(() => {
      return !this.bugtrackingEnabled() && !this.bugtrackingNagscreenDismissed();
    });
    this.gitVersionErrorDismissed = ko.observable(storage.getItem('gitVersionErrorDismissed'));
    this.gitVersionError = ko.observable();
    this.gitVersionErrorVisible = ko.computed(() => {
      return (
        !ungit.config.gitVersionCheckOverride &&
        this.gitVersionError() &&
        !this.gitVersionErrorDismissed()
      );
    });
  }
  getRepoList() {
    const localStorageRepo = JSON.parse(
      storage.getItem('repositories') || storage.getItem('visitedRepositories') || '[]'
    );
    const newRepos = localStorageRepo
      .concat(ungit.config.defaultRepositories || [])
      .filter((v, i, a) => a.indexOf(v) === i)
      .sort();
    storage.setItem('repositories', JSON.stringify(newRepos));
    return newRepos;
  }
  updateNode(parentElement) {
    ko.renderTemplate('app', this, {}, parentElement);
  }
  shown() {
    // The ungit.config constiable collections configuration from all different paths and only updates when
    // ungit is restarted
    if (!ungit.config.bugtracking) {
      // Whereas the userconfig only reflects what's in the ~/.ungitrc and updates directly,
      // but is only used for changing around the configuration. We need to check this here
      // since ungit may have crashed without the server crashing since we enabled bugtracking,
      // and we don't want to show the nagscreen twice in that case.
      this.server
        .getPromise('/userconfig')
        .then((userConfig) => this.bugtrackingEnabled(userConfig.bugtracking))
        .catch((e) => this.server.unhandledRejection(e));
    }

    this.server
      .getPromise('/latestversion')
      .then((version) => {
        if (!version) return;
        this.currentVersion(version.currentVersion);
        this.latestVersion(version.latestVersion);
        this.showNewVersionAvailable(!ungit.config.ungitVersionCheckOverride && version.outdated);
      })
      .catch((e) => this.server.unhandledRejection(e));
    this.server
      .getPromise('/gitversion')
      .then((gitversion) => {
        if (gitversion && !gitversion.satisfied) {
          this.gitVersionError(gitversion.error);
        }
      })
      .catch((e) => this.server.unhandledRejection(e));
  }
  updateAnimationFrame(deltaT) {
    if (this.content() && this.content().updateAnimationFrame)
      this.content().updateAnimationFrame(deltaT);
  }
  onProgramEvent(event) {
    if (event.event === 'request-credentials') {
      this._handleCredentialsRequested(event);
    } else if (event.event === 'request-remember-repo') {
      this._handleRequestRememberRepo(event);
    } else if (event.event === 'modal-show-dialog') {
      this.showModal(event.modal);
    } else if (event.event === 'modal-close-dialog') {
      $('.modal.fade').modal('hide');
      this.modal(undefined);
    }

    if (this.content() && this.content().onProgramEvent) {
      this.content().onProgramEvent(event);
    }
    if (this.header && this.header.onProgramEvent) {
      this.header.onProgramEvent(event);
    }
  }
  _handleRequestRememberRepo(event) {
    const repoPath = event.repoPath;
    if (this.repoList.indexOf(repoPath) != -1) return;
    this.repoList.push(repoPath);
  }
  _handleCredentialsRequested(event) {
    // Only show one credentials dialog if we're asked to show another one while the first one is open
    // This happens for instance when we fetch nodes and remote tags at the same time
    if (!this._isShowingCredentialsDialog) {
      this._isShowingCredentialsDialog = true;
      components.showModal('credentialsmodal', { remote: event.remote });
    }
  }
  showModal(modal) {
    this.modal(modal);

    // when dom is ready, open the modal
    const checkExists = setInterval(() => {
      const modalDom = $('.modal.fade');
      if (modalDom.length) {
        clearInterval(checkExists);
        modalDom.modal();
        modalDom.on('hidden.bs.modal', function () {
          modal.close();
        });
      }
    }, 200);
  }
  gitSetUserConfig(bugTracking) {
    this.server.getPromise('/userconfig').then((userConfig) => {
      userConfig.bugtracking = bugTracking;
      return this.server.postPromise('/userconfig', userConfig).then(() => {
        this.bugtrackingEnabled(bugTracking);
      });
    });
  }
  enableBugtracking() {
    this.gitSetUserConfig(true);
  }
  dismissBugtrackingNagscreen() {
    storage.setItem('bugtrackingNagscreenDismissed', true);
    this.bugtrackingNagscreenDismissed(true);
  }
  dismissGitVersionError() {
    storage.setItem('gitVersionErrorDismissed', true);
    this.gitVersionErrorDismissed(true);
  }
  dismissNewVersion() {
    this.showNewVersionAvailable(false);
  }
  templateChooser(data) {
    if (!data) return '';
    return data.template;
  }
}


================================================
FILE: components/app/app.less
================================================
.app {
  margin-top: 15px;

  .container-fluid {
    padding-left: 40px;
    padding-right: 40px;
  }
}


================================================
FILE: components/app/ungit-plugin.json
================================================
{
  "exports": {
    "knockoutTemplates": {
      "app": "app.html"
    },
    "javascript": "app.bundle.js",
    "css": "app.css"
  }
}


================================================
FILE: components/branches/branches.html
================================================
<div class="btn-group branch">
  <button type="button" class="btn btn-default btn-main" data-bind="click: updateRefs">
    <span data-bind="html: branchIcon"></span>
    <span data-bind="text: refsLabel"></span>
  </button>
  <button
    type="button"
    class="btn btn-default dropdown-toggle"
    data-toggle="dropdown"
    aria-haspopup="true"
    aria-expanded="false"
  >
    <span class="caret"></span>
    <span class="sr-only">Toggle Branch List</span>
  </button>
  <ul class="dropdown-menu dropdown-menu-right" role="menu">
    <li class="dropdown-header options" onclick="event.stopPropagation()">
      <label>
        <input
          class="glyphicon"
          type="checkbox"
          data-bind="checked: isShowRemote, css: { 'glyphicon-check': isShowRemote, 'glyphicon-unchecked': !isShowRemote() }"
        />
        Remote
      </label>
      <label>
        <input
          class="glyphicon"
          type="checkbox"
          data-bind="checked: isShowBranch, css: { 'glyphicon-check': isShowBranch, 'glyphicon-unchecked': !isShowBranch() }"
        />
        Branch
      </label>
      <label>
        <input
          class="glyphicon"
          type="checkbox"
          data-bind="checked: isShowTag, css: { 'glyphicon-check': isShowTag, 'glyphicon-unchecked': !isShowTag() }"
        />
        Tag
      </label>
    </li>
    <li class="divider" role="separator"></li>
    <!-- ko foreach: branchesAndLocalTags -->
    <li>
      <a
        class="linked-remove"
        href="#"
        data-bind="html: displayHtml(), click: $parent.checkoutBranch.bind($parent), attr: { 'data-ta-clickable': 'checkout' + name }"
      ></a>
      <a
        class="list-link list-remove"
        href="#"
        data-bind="html: $parent.closeIcon, click: $parent.branchRemove.bind($parent), attr: { 'data-ta-clickable': name + '-remove' }"
      ></a>
    </li>
    <!-- /ko -->
  </ul>
</div>


================================================
FILE: components/branches/branches.js
================================================
const ko = require('knockout');
const _ = require('lodash');
const octicons = require('octicons');
const components = require('ungit-components');
const storage = require('ungit-storage');
const showRemote = 'showRemote';
const showBranch = 'showBranch';
const showTag = 'showTag';
const { ComponentRoot } = require('../ComponentRoot');

components.register('branches', (args) => {
  return new BranchesViewModel(args.server, args.graph, args.repoPath);
});

class BranchesViewModel extends ComponentRoot {
  constructor(server, graph, repoPath) {
    super();
    this.repoPath = repoPath;
    this.server = server;
    this.updateRefs = _.debounce(this._updateRefs, 250, this.defaultDebounceOption);
    this.branchesAndLocalTags = ko.observableArray();
    this.current = ko.observable();
    this.isShowRemote = ko.observable(storage.getItem(showRemote) != 'false');
    this.isShowBranch = ko.observable(storage.getItem(showBranch) != 'false');
    this.isShowTag = ko.observable(storage.getItem(showTag) != 'false');
    this.graph = graph;
    const setLocalStorageAndUpdate = (localStorageKey, value) => {
      storage.setItem(localStorageKey, value);
      this.updateRefs();
      return value;
    };
    this.shouldAutoFetch = ungit.config.autoFetch;
    this.isShowRemote.subscribe(() => {
      this.clearApiCache();
      setLocalStorageAndUpdate(showRemote);
    });
    this.isShowBranch.subscribe(() => {
      this.clearApiCache();
      setLocalStorageAndUpdate(showBranch);
    });
    this.isShowTag.subscribe(() => {
      this.clearApiCache();
      setLocalStorageAndUpdate(showTag);
    });
    this.refsLabel = ko.computed(() => this.current() || 'master (no commits yet)');
    this.branchIcon = octicons['git-branch'].toSVG({ height: 18 });
    this.closeIcon = octicons.x.toSVG({ height: 18 });
  }

  checkoutBranch(branch) {
    branch.checkout();
  }
  updateNode(parentElement) {
    ko.renderTemplate('branches', this, {}, parentElement);
  }
  clickFetch() {
    this.updateRefs(true);
  }
  onProgramEvent(event) {
    if (
      event.event === 'request-app-content-refresh' ||
      event.event === 'branch-updated' ||
      event.event === 'git-directory-changed' ||
      event.event === 'current-remote-changed'
    ) {
      this.updateRefs();
    }
  }
  async _updateRefs(forceRemoteFetch) {
    forceRemoteFetch = forceRemoteFetch || this.shouldAutoFetch || '';

    const branchesProm = this.server.getPromise('/branches', { path: this.repoPath() });
    const refsProm = this.server.getPromise('/refs', {
      path: this.repoPath(),
      remoteFetch: forceRemoteFetch,
    });

    try {
      // set current branch
      (await branchesProm).forEach((b) => {
        if (b.current) {
          this.current(b.name);
        }
      });
    } catch (e) {
      this.current('~error');
      ungit.logger.warn('error while setting current branch', e);
    }

    try {
      // update branches and tags references.
      const refs = await refsProm;
      if (this.isSamePayload(refs)) {
        return;
      }

      const version = Date.now();
      const sorted = refs
        .map((r) => {
          const ref = this.graph.getRef(r.name.replace('refs/tags', 'tag: refs/tags'));
          ref.node(this.graph.getNode(r.sha1));
          ref.version = version;
          return ref;
        })
        .sort((a, b) => {
          if (a.current() || b.current()) {
            return a.current() ? -1 : 1;
          } else if (a.isRemoteBranch === b.isRemoteBranch) {
            if (a.name < b.name) {
              return -1;
            }
            if (a.name > b.name) {
              return 1;
            }
            return 0;
          } else {
            return a.isRemoteBranch ? 1 : -1;
          }
        })
        .filter((ref) => {
          if (ref.localRefName == 'refs/stash') return false;
          if (ref.localRefName.endsWith('/HEAD')) return false;
          if (!this.isShowRemote() && ref.isRemote) return false;
          if (!this.isShowBranch() && ref.isBranch) return false;
          if (!this.isShowTag() && ref.isTag) return false;
          return true;
        });
      this.branchesAndLocalTags(sorted);
      this.graph.refs().forEach((ref) => {
        // ref was removed from another source
        if (!ref.isRemoteTag && ref.value !== 'HEAD' && (!ref.version || ref.version < version)) {
          ref.remove(true);
        }
      });
    } catch (e) {
      ungit.logger.error('error during branch update: ', e);
    }
  }

  branchRemove(branch) {
    let details = `"${branch.refName}"`;
    if (branch.isRemoteBranch) {
      details = `<code style='font-size: 100%'>REMOTE</code> ${details}`;
    }
    components.showModal('yesnomodal', {
      title: 'Are you sure?',
      details: 'Deleting ' + details + ' branch cannot be undone with ungit.',
      closeFunc: (isYes) => {
        if (!isYes) return;
        return branch.remove();
      },
    });
  }
}


================================================
FILE: components/branches/branches.less
================================================
@import 'public/less/variables.less';

.branch {
  .options {
    font-size: inherit;
    color: @gray-dark;

    label {
      cursor: pointer;
      font-weight: normal;
      margin: 0 15px 0 0;

      &:last-child {
        margin: 0;
      }
    }

    input {
      -moz-appearance: none;
      -webkit-appearance: none;
      appearance: none;
      cursor: pointer;
    }
  }

  .dropdown-menu.octicon {
    width: 18px;
  }
}


================================================
FILE: components/branches/ungit-plugin.json
================================================
{
  "exports": {
    "knockoutTemplates": {
      "branches": "branches.html"
    },
    "javascript": "branches.bundle.js",
    "css": "branches.css"
  }
}


================================================
FILE: components/commit/commit.html
================================================
<div
  class="commit"
  data-bind="css: { highlighted: highlighted, hover: nodeIsMousehover, selected: selected }"
>
  <div
    class="commit-box panel panel-default"
    data-bind="element: element, click: stopClickPropagation"
  >
    <div class="panel-body">
      <div class="arrow shadow"></div>
      <div class="arrow"></div>
      <div class="clearfix">
        <img
          class="pull-left img-circle gravatar"
          data-bind="attr: { src: `https://www.gravatar.com/avatar/${authorGravatar()}?default=404`, alt: `Profile Picture of ${authorName()}` }"
          onerror="this.style.display='none';"
        />
        <div>
          <div>
            <span
              class="title"
              data-bind="text: (title().length > 72 ? title().substring(0, 72) + '...' : title)"
            ></span>
            <span class="text-muted"
              >by <a data-bind="text: authorName, attr: { href: 'mailto:' + authorEmail() }"></a
            ></span>
            <!-- ko if: pgpVerifiedString() -->
            <span
              class="text-muted"
              data-bind="html: pgpIcon, attr: { title: pgpVerifiedString() }"
              data-toggle="tooltip"
            ></span>
            <!-- /ko -->
          </div>
          <div class="text-muted nodeSummaryContainer">
            <span
              data-bind="text: authorDateFromNow, attr: { title: authorDate }"
              data-toggle="tooltip"
              data-placement="bottom"
            ></span>
            | +<span data-bind="text: numberOfAddedLines"></span>, -<span
              data-bind="text: numberOfRemovedLines"
            ></span>
            |
            <span title="Commit" data-bind="text: sha1.substring(0, 8)"></span>
            <!-- ko if: navigator.clipboard -->
            <button class="btn btn-default btn-xs" type="button" data-bind="click: copyHash"><span class="glyphicon glyphicon-copy"></span></button>
            <!-- /ko -->
            <!-- ko foreach: parents -->
            | <a href="#" title="Parent Commit" data-bind="text: $data.substring(0, 8), click: $parent.gotoCommit.bind($parent, $data)"></a>
            <!-- /ko -->
          </div>
        </div>
      </div>
      <!-- ko if: selected() || nodeIsMousehover() -->
      <div class="details">
        <div
          class="body"
          data-bind="visible: title().length > 72, text: '...' + title().substring(72)"
        ></div>
        <div class="body" data-bind="text: body, visible: body"></div>
        <div
          class="diff-wrapper"
          data-bind="visible: showCommitDiff, style: diffStyle, click: stopClickPropagation"
        >
          <div class="diff-inner" data-bind="component: commitDiff"></div>
        </div>
      </div>
      <!-- /ko -->
    </div>
  </div>
</div>


================================================
FILE: components/commit/commit.js
================================================
const ko = require('knockout');
const md5 = require('blueimp-md5');
const moment = require('moment');
const octicons = require('octicons');
const components = require('ungit-components');

components.register('commit', (args) => new CommitViewModel(args));

class CommitViewModel {
  constructor(gitNode) {
    this.graph = gitNode.graph;
    this.repoPath = gitNode.graph.repoPath;
    this.sha1 = gitNode.sha1;
    this.server = gitNode.graph.server;
    this.highlighted = gitNode.highlighted;
    this.nodeIsMousehover = gitNode.nodeIsMousehover;
    this.selected = gitNode.selected;
    this.pgpVerifiedString = gitNode.pgpVerifiedString;
    this.pgpIcon = octicons.verified.toSVG({ height: 18 });
    this.element = ko.observable();
    this.message = ko.observable();
    this.title = ko.observable();
    this.body = ko.observable();
    this.authorDate = ko.observable();
    this.authorDateFromNow = ko.observable();
    this.authorName = ko.observable();
    this.authorEmail = ko.observable();
    this.fileLineDiffs = ko.observable();
    this.numberOfAddedLines = ko.observable();
    this.numberOfRemovedLines = ko.observable();
    this.parents = ko.observable();
    this.authorGravatar = ko.computed(() => md5((this.authorEmail() || '').trim().toLowerCase()));

    this.showCommitDiff = ko.computed(
      () => this.fileLineDiffs() && this.fileLineDiffs().length > 0
    );

    this.diffStyle = ko.computed(() => {
      const marginLeft = Math.min(gitNode.branchOrder() * 70, 450) * -1;
      if (this.selected() && this.element())
        return { 'margin-left': `${marginLeft}px`, width: `${window.innerWidth - 220}px` };
      else return {};
    });
  }

  updateNode(parentElement) {
    ko.renderTemplate('commit', this, {}, parentElement);
  }

  setData(args) {
    const message = args.message.split('\n');
    this.message(args.message);
    this.title(message[0]);
    this.body(message.slice(message[1] ? 1 : 2).join('\n'));
    this.authorDate(moment(new Date(args.authorDate)));
    this.authorDateFromNow(this.authorDate().fromNow());
    this.authorName(args.authorName);
    this.authorEmail(args.authorEmail);
    this.numberOfAddedLines(args.additions);
    this.numberOfRemovedLines(args.deletions);
    this.parents(args.parents || []);
    this.fileLineDiffs(args.fileLineDiffs);
    this.commitDiff = ko.observable(
      components.create('commitDiff', {
        fileLineDiffs: this.fileLineDiffs(),
        sha1: this.sha1,
        repoPath: this.repoPath,
        server: this.server,
        showDiffButtons: this.selected,
      })
    );
  }

  updateLastAuthorDateFromNow(deltaT) {
    this.lastUpdatedAuthorDateFromNow = this.lastUpdatedAuthorDateFromNow || 0;
    this.lastUpdatedAuthorDateFromNow += deltaT;
    if (this.lastUpdatedAuthorDateFromNow > 60 * 1000) {
      this.lastUpdatedAuthorDateFromNow = 0;
      this.authorDateFromNow(this.authorDate().fromNow());
    }
  }

  updateAnimationFrame(deltaT) {
    this.updateLastAuthorDateFromNow(deltaT);
  }

  stopClickPropagation(data, event) {
    event.stopImmediatePropagation();
  }

  copyHash() {
    navigator.clipboard.writeText(this.sha1);
  }

  gotoCommit(sha1) {
    const node = this.graph.nodesById[sha1];
    if (node) {
      node.toggleSelected();
    }
  }
}


================================================
FILE: components/commit/commit.less
================================================
@import 'public/less/variables.less';

.commit {
  position: relative;

  &.highlighted {
    z-index: 2;

    .commit-box {
      box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.2);
      background: #4b5766;
      left: -5px;

      .arrow {
        border-left-color: #4b5766;

        &.shadow {
          display: block;
        }
      }
    }
  }

  &.hover {
    z-index: 3;
  }

  &.selected {
    .details {
      .diff-wrapper {
        margin-bottom: 5px;
        transition: width 0.1s, left 0.05s;
        box-shadow: 0 0 15px rgba(0, 0, 0, 0.15);

        .diff-inner {
          box-shadow: 5px 5px 0 rgba(0, 0, 0, 0.2);
          padding: 10px;
          padding-top: 0;
        }

        .btn-group {
          margin-top: 10px;
        }
      }
    }
  }

  .commit-box {
    .arrow {
      right: -30px;
      top: 27px;
      border-left-color: @panel-bg;

      &.shadow {
        display: none;
        right: -34px;
        top: 32px;
        border-left-color: rgba(0, 0, 0, 0.2);
      }
    }
  }

  .commit-box > .panel-body {
    position: relative;
    padding: 10px;
    margin-bottom: 0;
    width: @log-width-small;
    min-height: 85px;

    .gravatar {
      display: none;
      margin-right: 10px;
    }

    .title {
      font-size: 1.3em;
      word-wrap: break-word;
      display: block;
    }

    .details {
      .body {
        font-family: 'Source Code Pro', monospace;
        white-space: pre-wrap;
        word-wrap: break-word;
        color: #8f9fa6;
      }

      .diff-wrapper {
        margin-top: 10px;
        background: #4b5766;
        border-radius: 3px;
      }
    }
  }
}

@media (min-width: @screen-md-min) {
  .commit {
    .commit-box > .panel-body {
      width: @log-width-large;

      .gravatar {
        display: block;
      }
    }
  }
}


================================================
FILE: components/commit/ungit-plugin.json
================================================
{
  "exports": {
    "knockoutTemplates": {
      "commit": "commit.html"
    },
    "javascript": "commit.bundle.js",
    "css": "commit.css"
  }
}


================================================
FILE: components/commitdiff/commitdiff.html
================================================
<div class="btn-toolbar" data-bind="visible: showDiffButtons">
  <div class="btn-group btn-group-xs">
    <button
      class="btn btn-default commit-whitespace"
      data-bind="click: whiteSpace.toggle, css: {active: whiteSpace.isActive}"
      data-toggle="tooltip"
      data-placement="bottom"
      data-container="body"
      title="Hide whitespace changes in diff"
    >
      <span data-bind="text: whiteSpace.text"></span>
    </button>
  </div>
  <div class="btn-group btn-group-xs">
    <button
      class="btn btn-default commit-sideBySideDiff"
      data-bind="click: textDiffType.toggle, css: {active: textDiffType.isActive}"
      data-toggle="tooltip"
      data-placement="bottom"
      data-container="body"
      title="Show side by side diff view"
    >
      <span data-bind="text: textDiffType.text"></span>
    </button>
  </div>
  <div class="btn-group btn-group-xs">
    <button
      class="btn btn-default commit-wordwrap"
      data-bind="click: wordWrap.toggle, css: {active: wordWrap.isActive}"
      data-toggle="tooltip"
      data-placement="bottom"
      data-container="body"
      title="Wrap words per line"
    >
      <span data-bind="text: wordWrap.text"></span>
    </button>
  </div>
</div>

<div data-bind="foreach: commitLineDiffs" class="commitdiff">
  <div class="file">
    <div class="head commit-diff-filename" data-bind="click: fileNameClick">
      <span data-bind="text: displayName"></span>
      <span class="file-stats" data-bind="visible: added() != '-'">
        (
        <span data-bind="text: added"></span>
        <span data-bind="text: removed"></span>
        )
      </span>
    </div>
    <div class="diffContainer commit-line-diffs" data-bind="component: specificDiff"></div>
  </div>
</div>


================================================
FILE: components/commitdiff/commitdiff.js
================================================
const ko = require('knockout');
const CommitLineDiff = require('./commitlinediff.js').CommitLineDiff;
const components = require('ungit-components');

components.register('commitDiff', (args) => new CommitDiff(args));

class CommitDiff {
  constructor(args) {
    this.sha1 = args.sha1;

    this.showDiffButtons = args.showDiffButtons;
    this.textDiffType = args.textDiffType = args.textDiffType || components.create('textdiff.type');
    this.wordWrap = args.wordWrap = args.wordWrap || components.create('textdiff.wordwrap');
    this.whiteSpace = args.whiteSpace = args.whiteSpace || components.create('textdiff.whitespace');

    this.commitLineDiffs = args.fileLineDiffs.map(
      (fileLineDiff) => new CommitLineDiff(args, fileLineDiff)
    );
  }

  updateNode(parentElement) {
    ko.renderTemplate('commitdiff', this, {}, parentElement);
  }
}


================================================
FILE: components/commitdiff/commitdiff.less
================================================
.commitdiff {
  width: 100%;

  .file {
    margin-top: 5px;
    background: rgba(255, 255, 255, 0.16);
    border-radius: 3px;

    .head {
      display: block;
      cursor: pointer;
      padding: 3px;
      padding-left: 6px;
      padding-right: 6px;
      color: rgba(255, 255, 255, 0.79);
      word-wrap: break-word;

      .file-stats {
        span:nth-of-type(1)::before {
          content: '+';
        }

        span:nth-of-type(1) {
          color: #9bf3a9;
        }

        span:nth-of-type(2)::before {
          content: '-';
        }

        span:nth-of-type(2) {
          color: #ec9d93;
        }
      }
    }

    .diff {
      background: rgba(0, 0, 0, 0.11);

      .textDiff {
        color: rgba(255, 255, 255, 0.3);
      }
    }
  }
}

.loadMore {
  .btn {
    display: block;
    margin: 20px 20px 10px 20px;
  }
}


================================================
FILE: components/commitdiff/commitlinediff.js
================================================
const ko = require('knockout');
const components = require('ungit-components');
const programEvents = require('ungit-program-events');

class CommitLineDiff {
  constructor(args, fileLineDiff) {
    this.added = ko.observable(fileLineDiff.additions);
    this.removed = ko.observable(fileLineDiff.deletions);
    this.fileName = ko.observable(fileLineDiff.fileName);
    this.oldFileName = ko.observable(fileLineDiff.oldFileName);
    this.displayName = ko.observable(fileLineDiff.displayName);
    this.fileType = fileLineDiff.type;
    this.isShowingDiffs = ko.observable(false);
    this.repoPath = args.repoPath;
    this.server = args.server;
    this.sha1 = args.sha1;
    this.textDiffType = args.textDiffType;
    this.wordWrap = args.wordWrap;
    this.whiteSpace = args.whiteSpace;
    this.specificDiff = ko.observable(this.getSpecificDiff());
  }

  getSpecificDiff() {
    return components.create(`${this.fileType}diff`, {
      filename: this.fileName(),
      oldFilename: this.oldFileName(),
      repoPath: this.repoPath,
      server: this.server,
      sha1: this.sha1,
      textDiffType: this.textDiffType,
      isShowingDiffs: this.isShowingDiffs,
      whiteSpace: this.whiteSpace,
      wordWrap: this.wordWrap,
    });
  }

  fileNameClick() {
    this.isShowingDiffs(!this.isShowingDiffs());
    programEvents.dispatch({ event: 'graph-render' });
  }
}

exports.CommitLineDiff = CommitLineDiff;


================================================
FILE: components/commitdiff/ungit-plugin.json
================================================
{
  "exports": {
    "knockoutTemplates": {
      "commitdiff": "commitdiff.html"
    },
    "javascript": "commitdiff.bundle.js",
    "css": "commitdiff.css"
  }
}


================================================
FILE: components/crash/crash.html
================================================
<div class="container">
  <div class="panel panel-default crash">
    <div class="panel-body">
      <h1>Whooops</h1>
      Unfortunately ungit interrupted its work because: <code data-bind="html: eventcause"></code
      ><br />
      The following tips will help solve this problem depending on the error:
      <br /><br />
      <ul>
        <li>
          General:
          <ul>
            <li>something went wrong, reload the page to start over</li>
            <li>check server out or logs for errors</li>
            <li>~/.ungitrc must contain valid JSON. Minimal valid json is "{}"</li>
          </ul>
        </li>

        <li>
          Connection Lost:
          <ul>
            <li>check status of server or network connection to the ungit server</li>
          </ul>
        </li>

        <li>
          Ad Blockers and Privacy Extensions:
          <ul>
            <li>
              add ungit server url to adblocker exception (with port definition) or disable for a
              while
            </li>
          </ul>
        </li>

        <li>
          Git does not have enough permissions:
          <ul>
            <li>check if you could write to .git directory (preferably on unix systems)</li>
          </ul>
        </li>

        <li>
          Other:
          <ul>
            <li>
              Find or report bug at
              <a href="https://github.com/FredrikNoren/ungit/issues">ungit github</a>.<br />
              Just common sense; do a quick search before posting, someone might already have
              created an issue (or resolved the problem!).<br />
              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, output from cli
              console and output from the browser console).
            </li>
          </ul>
        </li>
      </ul>
    </div>
  </div>
</div>


================================================
FILE: components/crash/crash.js
================================================
const ko = require('knockout');
const components = require('ungit-components');

components.register('crash', (err) => new CrashViewModel(err));

class CrashViewModel {
  constructor(err) {
    this.eventcause = err ? err : 'unknown error';
  }

  updateNode(parentElement) {
    ko.renderTemplate('crash', this, {}, parentElement);
  }
}


================================================
FILE: components/crash/crash.less
================================================
.crash {
  margin-top: 20px;
}


================================================
FILE: components/crash/ungit-plugin.json
================================================
{
  "exports": {
    "knockoutTemplates": {
      "crash": "crash.html"
    },
    "javascript": "crash.bundle.js",
    "css": "crash.css"
  }
}


================================================
FILE: components/gitErrors/gitErrors.html
================================================
<!-- ko foreach: gitErrors -->
<div class="alert" data-bind="css: { 'alert-danger': !isWarning, 'alert-warning': isWarning }">
  <button type="button" class="close" data-bind="html: $parent.closeIcon, click: dismiss"></button>
  <h3>
    <span data-bind="html: $parent.alertIcon"></span>
    Unhandled git error!
  </h3>
  <p>
    Ungit tried to run a git command that resulted in an unhandled error.
    <span data-bind="visible: bugReportWasSent">An automatic bug report was sent.</span>
  </p>
  <p data-bind="visible: tip, text: tip"></p>
  <h4>Command</h4>
  <pre data-bind="text: command"></pre>
  <h4>Error</h4>
  <pre data-bind="text: error"></pre>
  <h4>Stderr</h4>
  <pre data-bind="text: stderr"></pre>
  <h4>Stdout</h4>
  <pre data-bind="text: stdout"></pre>
</div>
<!-- /ko -->


================================================
FILE: components/gitErrors/gitErrors.js
================================================
const ko = require('knockout');
const octicons = require('octicons');
const components = require('ungit-components');

components.register('gitErrors', (args) => new GitErrorsViewModel(args.server, args.repoPath));

class GitErrorsViewModel {
  constructor(server, repoPath) {
    this.server = server;
    this.repoPath = repoPath;
    this.gitErrors = ko.observableArray();
    this.closeIcon = octicons.x.toSVG({ height: 18 });
    this.alertIcon = octicons.alert.toSVG({ height: 24 });
  }

  updateNode(parentElement) {
    ko.renderTemplate('gitErrors', this, {}, parentElement);
  }

  onProgramEvent(event) {
    if (event.event == 'git-error') this._handleGitError(event);
  }

  _handleGitError(event) {
    if (event.data.repoPath != this.repoPath()) return;
    this.gitErrors.push(new GitErrorViewModel(this, this.server, event.data));
  }
}

class GitErrorViewModel {
  constructor(gitErrors, server, data) {
    const self = this;
    this.gitErrors = gitErrors;
    this.server = server;
    this.tip = data.tip;
    this.isWarning = data.isWarning || false;
    this.command = data.command;
    this.error = data.error;
    this.stdout = data.stdout;
    this.stderr = data.stderr;
    this.showEnableBugtracking = ko.observable(false);
    this.bugReportWasSent = ungit.config.bugtracking;

    if (!data.shouldSkipReport && !ungit.config.bugtracking) {
      this.server.getPromise('/userconfig').then((userConfig) => {
        self.showEnableBugtracking(!userConfig.bugtracking);
      });
    }
  }

  dismiss() {
    this.gitErrors.gitErrors.remove(this);
  }
}


================================================
FILE: components/gitErrors/ungit-plugin.json
================================================
{
  "exports": {
    "knockoutTemplates": {
      "gitErrors": "gitErrors.html"
    },
    "javascript": "gitErrors.bundle.js"
  }
}


================================================
FILE: components/graph/animateable.js
================================================
const ko = require('knockout');
const Selectable = require('./selectable');

require('mina');

class Animateable extends Selectable {
  constructor(graph) {
    super(graph);
    this.element = ko.observable();
    this.previousGraph = undefined;
    this.element.subscribe((val) => {
      if (val) this.animate(true);
    });
    this.animate = (forceRefresh) => {
      const currentGraph = this.getGraphAttr();
      if (
        this.element() &&
        (forceRefresh || JSON.stringify(currentGraph) !== JSON.stringify(this.previousGraph))
      ) {
        // dom is valid and force refresh is requested or dom moved, redraw
        if (ungit.config.isAnimate) {
          const now = Date.now();
          window.mina(
            this.previousGraph || currentGraph,
            currentGraph,
            now,
            now + 750,
            window.mina.time,
            (val) => {
              this.setGraphAttr(val);
            },
            window.mina.elastic
          );
        } else {
          this.setGraphAttr(currentGraph);
        }
        this.previousGraph = currentGraph;
      }
    };
  }
}
module.exports = Animateable;


================================================
FILE: components/graph/edge.js
================================================
const ko = require('knockout');
const Animateable = require('./animateable');

class EdgeViewModel extends Animateable {
  constructor(graph, nodeAsha1, nodeBsha1) {
    super(graph);
    this.nodeA = graph.getNode(nodeAsha1);
    this.nodeB = graph.getNode(nodeBsha1);
    this.getGraphAttr = ko.computed(() => {
      if (this.nodeA.isViewable() && (!this.nodeB.isViewable() || !this.nodeB.isInited)) {
        return [
          this.nodeA.cx(),
          this.nodeA.cy(),
          this.nodeA.cx(),
          this.nodeA.cy(),
          this.nodeA.cx(),
          graph.graphHeight(),
          this.nodeA.cx(),
          graph.graphHeight(),
        ];
      } else if (this.nodeB.isInited && this.nodeB.cx() && this.nodeB.cy()) {
        return [
          this.nodeA.cx(),
          this.nodeA.cy(),
          this.nodeA.cx(),
          this.nodeA.cy(),
          this.nodeB.cx(),
          this.nodeB.cy(),
          this.nodeB.cx(),
          this.nodeB.cy(),
        ];
      } else {
        return [0, 0, 0, 0, 0, 0, 0, 0];
      }
    });
    this.getGraphAttr.subscribe(this.animate.bind(this));
  }

  setGraphAttr(val) {
    this.element().setAttribute('d', `M${val.slice(0, 4).join(',')}L${val.slice(4, 8).join(',')}`);
  }

  edgeMouseOver() {
    if (this.nodeA) {
      this.nodeA.isEdgeHighlighted(true);
    }
    if (this.nodeB) {
      this.nodeB.isEdgeHighlighted(true);
    }
  }

  edgeMouseOut() {
    if (this.nodeA) {
      this.nodeA.isEdgeHighlighted(false);
    }
    if (this.nodeB) {
      this.nodeB.isEdgeHighlighted(false);
    }
  }
}

module.exports = EdgeViewModel;


================================================
FILE: components/graph/git-graph-actions.js
================================================
const ko = require('knockout');
const octicons = require('octicons');
const components = require('ungit-components');
const programEvents = require('ungit-program-events');
const RefViewModel = require('./git-ref.js');
const HoverActions = require('./hover-actions');

const RebaseViewModel = HoverActions.RebaseViewModel;
const MergeViewModel = HoverActions.MergeViewModel;
const ResetViewModel = HoverActions.ResetViewModel;
const PushViewModel = HoverActions.PushViewModel;
const SquashViewModel = HoverActions.SquashViewModel;

class ActionBase {
  constructor(graph, text, style, icon) {
    this.graph = graph;
    this.server = graph.server;
    this.isRunning = ko.observable(false);
    this.isHighlighted = ko.computed(
      () => !graph.hoverGraphAction() || graph.hoverGraphAction() == this
    );
    this.text = text;
    this.style = style;
    this.icon = icon;
    this.cssClasses = ko.computed(() => {
      if (!this.isHighlighted() || this.isRunning()) {
        return `${this.style} dimmed`;
      } else {
        return this.style;
      }
    });
  }

  doPerform() {
    if (this.isRunning()) return;
    this.graph.hoverGraphAction(null);
    this.isRunning(true);
    return this.perform()
      .catch((e) => this.server.unhandledRejection(e))
      .finally(() => {
        this.isRunning(false);
      });
  }

  dragEnter() {
    if (!this.visible()) return;
    this.graph.hoverGraphAction(this);
  }

  dragLeave() {
    if (!this.visible()) return;
    this.graph.hoverGraphAction(null);
  }

  mouseover() {
    this.graph.hoverGraphAction(this);
  }

  mouseout() {
    this.graph.hoverGraphAction(null);
  }
}

class Move extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Move', 'move', octicons['arrow-left'].toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      return (
        this.graph.currentActionContext() instanceof RefViewModel &&
        this.graph.currentActionContext().node() != this.node
      );
    });
  }

  perform() {
    return this.graph.currentActionContext().moveTo(this.node.sha1);
  }
}

class Reset extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Reset', 'reset', octicons.trash.toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      if (!(this.graph.currentActionContext() instanceof RefViewModel)) return false;
      const context = this.graph.currentActionContext();
      if (context.node() != this.node) return false;
      const remoteRef = context.getRemoteRef(this.graph.currentRemote());
      return (
        remoteRef &&
        remoteRef.node() &&
        context &&
        context.node() &&
        remoteRef.node() != context.node() &&
        remoteRef.node().date < context.node().date
      );
    });
  }

  createHoverGraphic() {
    const context = this.graph.currentActionContext();
    if (!context) return null;
    const remoteRef = context.getRemoteRef(this.graph.currentRemote());
    const nodes = context.node().getPathToCommonAncestor(remoteRef.node()).slice(0, -1);
    return new ResetViewModel(nodes);
  }

  perform() {
    const context = this.graph.currentActionContext();
    const remoteRef = context.getRemoteRef(this.graph.currentRemote());
    return new Promise((resolve, reject) => {
      components.showModal('yesnomodal', {
        title: 'Are you sure?',
        details: 'Resetting to ref: ' + remoteRef.name + ' cannot be undone with ungit.',
        closeFunc: async (isYes) => {
          if (isYes) {
            await this.server
              .postPromise('/reset', {
                path: this.graph.repoPath(),
                to: remoteRef.name,
                mode: 'hard',
              })
              .then(resolve)
              .catch(reject);
            context.node(remoteRef.node());
          }
          this.isRunning(false);
        },
      });
    });
  }
}

class Rebase extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Rebase', 'rebase', octicons['repo-forked'].toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      return (
        this.graph.currentActionContext() instanceof RefViewModel &&
        (!ungit.config.showRebaseAndMergeOnlyOnRefs || this.node.refs().length > 0) &&
        this.graph.currentActionContext().current() &&
        this.graph.currentActionContext().node() != this.node
      );
    });
  }

  createHoverGraphic() {
    let onto = this.graph.currentActionContext();
    if (!onto) return;
    if (onto instanceof RefViewModel) onto = onto.node();
    const path = onto.getPathToCommonAncestor(this.node);
    return new RebaseViewModel(this.node, path);
  }

  perform() {
    return this.server
      .postPromise('/rebase', { path: this.graph.repoPath(), onto: this.node.sha1 })
      .catch((err) => {
        if (err.errorCode != 'merge-failed') {
          this.server.unhandledRejection(err);
        } else {
          ungit.logger.warn('rebase failed', err);
        }
      });
  }
}

class Merge extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Merge', 'merge', octicons['git-merge'].toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      if (!this.graph.checkedOutRef() || !this.graph.checkedOutRef().node()) return false;
      return (
        this.graph.currentActionContext() instanceof RefViewModel &&
        !this.graph.currentActionContext().current() &&
        this.graph.checkedOutRef().node() == this.node
      );
    });
  }

  createHoverGraphic() {
    let node = this.graph.currentActionContext();
    if (!node) return null;
    if (node instanceof RefViewModel) node = node.node();
    return new MergeViewModel(this.graph, this.node, node);
  }

  perform() {
    return this.server
      .postPromise('/merge', {
        path: this.graph.repoPath(),
        with: this.graph.currentActionContext().localRefName,
      })
      .catch((err) => {
        if (err.errorCode != 'merge-failed') {
          this.server.unhandledRejection(err);
        } else {
          ungit.logger.warn('merge failed', err);
        }
      });
  }
}

class Push extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Push', 'push', octicons['repo-push'].toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      return (
        this.graph.currentActionContext() instanceof RefViewModel &&
        this.graph.currentActionContext().node() == this.node &&
        this.graph.currentActionContext().canBePushed(this.graph.currentRemote())
      );
    });
  }

  createHoverGraphic() {
    const context = this.graph.currentActionContext();
    if (!context) return null;
    const remoteRef = context.getRemoteRef(this.graph.currentRemote());
    if (!remoteRef) return null;
    return new PushViewModel(remoteRef.node(), context.node());
  }

  perform() {
    const ref = this.graph.currentActionContext();
    const remoteRef = ref.getRemoteRef(this.graph.currentRemote());

    if (remoteRef) {
      return remoteRef.moveTo(ref.node().sha1);
    } else {
      return ref
        .createRemoteRef()
        .then(() => {
          if (this.graph.HEAD().name == ref.name) {
            this.graph.HEADref().node(ref.node());
          }
        })
        .finally(() => programEvents.dispatch({ event: 'request-fetch-tags' }));
    }
  }
}

class Checkout extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Checkout', 'checkout', octicons['desktop-download'].toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      if (this.graph.currentActionContext() instanceof RefViewModel)
        return (
          this.graph.currentActionContext().node() == this.node &&
          !this.graph.currentActionContext().current()
        );
      return ungit.config.allowCheckoutNodes && this.graph.currentActionContext() == this.node;
    });
  }

  perform() {
    return this.graph.currentActionContext().checkout();
  }
}

class Delete extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Delete', 'delete', octicons.x.toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      return (
        this.graph.currentActionContext() instanceof RefViewModel &&
        this.graph.currentActionContext().node() == this.node &&
        !this.graph.currentActionContext().current()
      );
    });
  }

  perform() {
    const context = this.graph.currentActionContext();
    let details = `"${context.refName}"`;
    if (context.isRemoteBranch) {
      details = `<code _style="font-size: 100%">REMOTE</code> ${details}`;
    }
    details = `Deleting ${details} branch or tag cannot be undone with ungit.`;

    return new Promise((resolve, reject) => {
      components.showModal('yesnomodal', {
        title: 'Are you sure?',
        details: details,
        closeFunc: async (isYes) => {
          if (isYes) {
            await context.remove().then(resolve).catch(reject);
          }
          this.isRunning(false);
        },
      });
    });
  }
}

class CherryPick extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Cherry pick', 'cherry-pick', octicons.cpu.toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      const context = this.graph.currentActionContext();
      return context === this.node && this.graph.HEAD() && context.sha1 !== this.graph.HEAD().sha1;
    });
  }

  perform() {
    return this.server
      .postPromise('/cherrypick', { path: this.graph.repoPath(), name: this.node.sha1 })
      .catch((err) => {
        if (err.errorCode != 'merge-failed') {
          this.server.unhandledRejection(err);
        } else {
          ungit.logger.warn('cherrypick failed', err);
        }
      });
  }
}

class Uncommit extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Uncommit', 'uncommit', octicons.zap.toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      return this.graph.currentActionContext() == this.node && this.graph.HEAD() == this.node;
    });
  }

  perform() {
    return this.server
      .postPromise('/reset', { path: this.graph.repoPath(), to: 'HEAD^', mode: 'mixed' })
      .then(() => {
        let targetNode = this.node.belowNode;
        while (targetNode && !targetNode.ancestorOfHEAD()) {
          targetNode = targetNode.belowNode;
        }
        this.graph.HEADref().node(targetNode ? targetNode : null);
        this.graph.checkedOutRef().node(targetNode ? targetNode : null);
      });
  }
}

class Revert extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Revert', 'revert', octicons.history.toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      return this.graph.currentActionContext() == this.node;
    });
  }

  perform() {
    return this.server.postPromise('/revert', {
      path: this.graph.repoPath(),
      commit: this.node.sha1,
    });
  }
}

class Squash extends ActionBase {
  constructor(graph, node) {
    super(graph, 'Squash', 'squash', octicons.fold.toSVG({ height: 18 }));
    this.node = node;
    this.visible = ko.computed(() => {
      if (this.isRunning()) return true;
      return (
        this.graph.currentActionContext() instanceof RefViewModel &&
        this.graph.currentActionContext().current() &&
        this.graph.currentActionContext().node() != this.node
      );
    });
  }

  createHoverGraphic() {
    let onto = this.graph.currentActionContext();
    if (!onto) return;
    if (onto instanceof RefViewModel) onto = onto.node();

    return new SquashViewModel(this.node, onto);
  }

  perform() {
    let onto = this.graph.currentActionContext();
    if (!onto) return;
    if (onto instanceof RefViewModel) onto = onto.node();
    // remove last element as it would be a common ancestor.
    const path = this.node.getPathToCommonAncestor(onto).slice(0, -1);

    if (path.length > 0) {
      // squashing branched out lineage
      // c is checkout with squash target of e, results in staging changes
      // from d and e on top of c
      //
      // a - b - (c)        a - b - (c) - [de]
      //  \           ->     \
      //   d  - <e>           d - <e>
      return this.server.postPromise('/squash', {
        path: this.graph.repoPath(),
        target: this.node.sha1,
      });
    } else {
      // squashing backward from same lineage
      // c is checkout with squash target of a, results in current ref moved
      // to a and staging changes within b and c on top of a
      //
      // <a> - b - (c)       (a) - b - c
      //                ->     \
      //                        [bc]
      return this.graph
        .currentActionContext()
        .moveTo(this.node.sha1, true)
        .then(() =>
          this.server.postPromise('/squash', { path: this.graph.repoPath(), target: onto.sha1 })
        );
    }
  }
}

const GraphActions = {
  Move: Move,
  Rebase: Rebase,
  Merge: Merge,
  Push: Push,
  Reset: Reset,
  Checkout: Checkout,
  Delete: Delete,
  CherryPick: CherryPick,
  Uncommit: Uncommit,
  Revert: Revert,
  Squash: Squash,
};
module.exports = GraphActions;


================================================
FILE: components/graph/git-node.js
================================================
const $ = require('jquery');
const ko = require('knockout');
const components = require('ungit-components');
const programEvents = require('ungit-program-events');
const Animateable = require('./animateable');
const GraphActions = require('./git-graph-actions');

const maxBranchesToDisplay = parseInt((ungit.config.numRefsToShow / 5) * 3); // 3/5 of refs to show to branches
const maxTagsToDisplay = ungit.config.numRefsToShow - maxBranchesToDisplay; // 2/5 of refs to show to tags

class GitNodeViewModel extends Animateable {
  constructor(graph, sha1) {
    super(graph);
    this.graph = graph;
    this.sha1 = sha1;
    this.isInited = false;
    this.title = ko.observable();
    this.parents = ko.observableArray();
    this.commitTime = undefined; // commit time in string
    this.date = undefined; // commit time in numeric format for sort
    this.color = ko.observable();
    this.ideologicalBranch = ko.observable();
    this.remoteTags = ko.observableArray();
    this.branchesAndLocalTags = ko.observableArray();
    this.signatureDate = ko.observable();
    this.signatureMade = ko.observable();
    this.pgpVerifiedString = ko.computed(() => {
      if (this.signatureMade()) {
        return `PGP by: ${this.signatureMade()} at ${this.signatureDate()}`;
      }
    });

    this.refs = ko.computed(() => {
      const rs = this.branchesAndLocalTags().concat(this.remoteTags());
      rs.sort((a, b) => {
        if (b.current()) return 1;
        if (a.current()) return -1;
        if (a.isLocal && !b.isLocal) return -1;
        if (!a.isLocal && b.isLocal) return 1;
        return a.refName < b.refName ? -1 : 1;
      });
      return rs;
    });
    // These are split up like this because branches and local tags can be found in the git log,
    // whereas remote tags needs to be fetched with another command (which is much slower)
    this.branches = ko.observableArray();
    this.branchesToDisplay = ko.observableArray();
    this.tags = ko.observableArray();
    this.tagsToDisplay = ko.observableArray();
    this.refs.subscribe((newValue) => {
      if (newValue) {
        this.branches(newValue.filter((r) => r.isBranch));
        this.tags(newValue.filter((r) => r.isTag));
        this.branchesToDisplay(
          this.branches.slice(
            0,
            ungit.config.numRefsToShow - Math.min(this.tags().length, maxTagsToDisplay)
          )
        );
        this.tagsToDisplay(
          this.tags.slice(0, ungit.config.numRefsToShow - this.branchesToDisplay().length)
        );
      } else {
        this.branches.removeAll();
        this.tags.removeAll();
        this.branchesToDisplay.removeAll();
        this.tagsToDisplay.removeAll();
      }
    });
    this.ancestorOfHEAD = ko.observable(false);
    this.nodeIsMousehover = ko.observable(false);
    this.commitContainerVisible = ko.computed(
      () => this.ancestorOfHEAD() || this.nodeIsMousehover() || this.selected()
    );
    this.isEdgeHighlighted = ko.observable(false);
    // for small empty black circle to highlight a node
    this.isNodeAccented = ko.computed(() => this.selected() || this.isEdgeHighlighted());
    // to show changed files and diff boxes on the left of node
    this.highlighted = ko.computed(() => this.nodeIsMousehover() || this.selected());
    this.selected.subscribe(() => {
      programEvents.dispatch({ event: 'graph-render' });
    });
    this.showNewRefAction = ko.computed(() => !graph.currentActionContext());
    this.showRefSearch = ko.computed(
      () => this.branches().length + this.tags().length > ungit.config.numRefsToShow
    );
    this.newBranchName = ko.observable();
    this.newBranchNameHasFocus = ko.observable(true);
    this.branchingFormVisible = ko.observable(false);
    this.canCreateRef = ko.computed(
      () =>
        this.newBranchName() && this.newBranchName().trim() && !this.newBranchName().includes(' ')
    );
    this.branchOrder = ko.observable();
    this.aboveNode = undefined;
    this.belowNode = undefined;
    this.refSearchFormVisible = ko.observable(false);
    this.commitComponent 
Download .txt
gitextract_do455as5/

├── .gitattributes
├── .github/
│   └── workflows/
│       ├── bump.yml
│       └── ci.yml
├── .gitignore
├── .mochaclicktest.json
├── .mochatest.json
├── .npmignore
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE.md
├── MERGETOOL.md
├── PLUGINS.md
├── README.md
├── appveyor.yml
├── bin/
│   ├── credentials-helper
│   └── ungit
├── clicktests/
│   ├── environment.js
│   ├── spec.authentication.js
│   ├── spec.bare.js
│   ├── spec.branches.js
│   ├── spec.commands.js
│   ├── spec.discard.js
│   ├── spec.generic.js
│   ├── spec.load-ahead.js
│   ├── spec.no-header.js
│   ├── spec.remotes.js
│   ├── spec.screens.js
│   ├── spec.stash.js
│   └── spec.submodules.js
├── components/
│   ├── ComponentRoot.ts
│   ├── app/
│   │   ├── app.html
│   │   ├── app.js
│   │   ├── app.less
│   │   └── ungit-plugin.json
│   ├── branches/
│   │   ├── branches.html
│   │   ├── branches.js
│   │   ├── branches.less
│   │   └── ungit-plugin.json
│   ├── commit/
│   │   ├── commit.html
│   │   ├── commit.js
│   │   ├── commit.less
│   │   └── ungit-plugin.json
│   ├── commitdiff/
│   │   ├── commitdiff.html
│   │   ├── commitdiff.js
│   │   ├── commitdiff.less
│   │   ├── commitlinediff.js
│   │   └── ungit-plugin.json
│   ├── crash/
│   │   ├── crash.html
│   │   ├── crash.js
│   │   ├── crash.less
│   │   └── ungit-plugin.json
│   ├── gitErrors/
│   │   ├── gitErrors.html
│   │   ├── gitErrors.js
│   │   └── ungit-plugin.json
│   ├── graph/
│   │   ├── animateable.js
│   │   ├── edge.js
│   │   ├── git-graph-actions.js
│   │   ├── git-node.js
│   │   ├── git-ref.js
│   │   ├── graph-graphics.html
│   │   ├── graph.html
│   │   ├── graph.js
│   │   ├── graph.less
│   │   ├── hover-actions.js
│   │   ├── selectable.js
│   │   └── ungit-plugin.json
│   ├── header/
│   │   ├── header.html
│   │   ├── header.js
│   │   ├── header.less
│   │   └── ungit-plugin.json
│   ├── home/
│   │   ├── home.html
│   │   ├── home.js
│   │   ├── home.less
│   │   └── ungit-plugin.json
│   ├── imagediff/
│   │   ├── imagediff.html
│   │   ├── imagediff.js
│   │   ├── imagediff.less
│   │   └── ungit-plugin.json
│   ├── login/
│   │   ├── login.html
│   │   ├── login.js
│   │   └── ungit-plugin.json
│   ├── modals/
│   │   ├── formModal.html
│   │   ├── forms.ts
│   │   ├── modalBase.ts
│   │   ├── modals.ts
│   │   ├── promptModal.html
│   │   ├── prompts.ts
│   │   └── ungit-plugin.json
│   ├── path/
│   │   ├── path.html
│   │   ├── path.js
│   │   ├── path.less
│   │   └── ungit-plugin.json
│   ├── refreshbutton/
│   │   ├── refreshbutton.html
│   │   ├── refreshbutton.js
│   │   ├── refreshbutton.less
│   │   └── ungit-plugin.json
│   ├── remotes/
│   │   ├── remotes.html
│   │   ├── remotes.js
│   │   └── ungit-plugin.json
│   ├── repository/
│   │   ├── repository.html
│   │   ├── repository.js
│   │   ├── repository.less
│   │   └── ungit-plugin.json
│   ├── staging/
│   │   ├── staging.html
│   │   ├── staging.js
│   │   ├── staging.less
│   │   └── ungit-plugin.json
│   ├── stash/
│   │   ├── stash.html
│   │   ├── stash.js
│   │   ├── stash.less
│   │   └── ungit-plugin.json
│   ├── submodules/
│   │   ├── submodules.html
│   │   ├── submodules.js
│   │   └── ungit-plugin.json
│   └── textdiff/
│       ├── textdiff.html
│       ├── textdiff.js
│       ├── textdiff.less
│       └── ungit-plugin.json
├── eslint.config.mjs
├── package.json
├── public/
│   ├── images/
│   │   └── icon.icns
│   ├── index.html
│   ├── less/
│   │   ├── bootstrap.less
│   │   ├── d2h.less
│   │   ├── styles.less
│   │   └── variables.less
│   ├── main.js
│   ├── source/
│   │   ├── bootstrap.js
│   │   ├── components.js
│   │   ├── jquery-ui.js
│   │   ├── knockout-bindings.js
│   │   ├── main.js
│   │   ├── navigation.js
│   │   ├── program-events.js
│   │   ├── server.js
│   │   └── storage.js
│   └── vendor/
│       └── css/
│           └── animate.css
├── scripts/
│   ├── build.js
│   ├── electronpackage.js
│   ├── electronzip.js
│   ├── npmpublish.js
│   └── teststabilitytester.js
├── source/
│   ├── address-parser.js
│   ├── bugtracker.js
│   ├── config.js
│   ├── git-api.js
│   ├── git-parser.js
│   ├── git-promise.js
│   ├── server.js
│   ├── sysinfo.js
│   ├── ungit-plugin.js
│   └── utils/
│       ├── cache.js
│       ├── file-type.js
│       └── logger.js
├── test/
│   ├── common-es6.js
│   ├── spec.address-parser.js
│   ├── spec.cache.js
│   ├── spec.credentials-helper.js
│   ├── spec.file-type.js
│   ├── spec.git-api.branching.js
│   ├── spec.git-api.conflict-no-auto-stash.js
│   ├── spec.git-api.conflict.js
│   ├── spec.git-api.diff.js
│   ├── spec.git-api.discardchanges.js
│   ├── spec.git-api.ignorefile.js
│   ├── spec.git-api.js
│   ├── spec.git-api.patch.js
│   ├── spec.git-api.remote.js
│   ├── spec.git-api.squash.js
│   ├── spec.git-api.stash.js
│   ├── spec.git-api.submodule.js
│   └── spec.git-parser.js
└── tsconfig.json
Download .txt
SYMBOL INDEX (404 symbols across 43 files)

FILE: clicktests/environment.js
  class Environment (line 23) | class Environment {
    method constructor (line 24) | constructor(config) {
    method getRootUrl (line 35) | getRootUrl() {
    method init (line 39) | async init() {
    method startServer (line 56) | async startServer() {
    method shutdown (line 112) | async shutdown() {
    method backgroundAction (line 131) | async backgroundAction(method, url, body) {
    method createRepos (line 149) | async createRepos(testRepoPaths, config) {
    method initRepo (line 159) | async initRepo(options) {
    method createTempFolder (line 170) | async createTempFolder() {
    method createCommits (line 175) | async createCommits(config, limit, x) {
    method createTestFile (line 189) | async createTestFile(filename, repoPath) {
    method goto (line 198) | async goto(url) {
    method openUngit (line 223) | async openUngit(tempDirPath) {
    method waitForElementVisible (line 229) | waitForElementVisible(selector, timeout) {
    method waitForElementHidden (line 233) | waitForElementHidden(selector, timeout) {
    method wait (line 237) | wait(duration) {
    method type (line 241) | type(text) {
    method insert (line 244) | async insert(selector, text) {
    method press (line 250) | press(key) {
    method click (line 254) | async click(selector, clickCount) {
    method waitForBranch (line 270) | waitForBranch(branchName) {
    method commit (line 278) | async commit(commitMessage) {
    method _createRef (line 287) | async _createRef(type, name) {
    method createTag (line 300) | createTag(name) {
    method createBranch (line 303) | createBranch(name) {
    method _verifyRefAction (line 307) | async _verifyRefAction(action) {
    method _refAction (line 321) | async _refAction(ref, local, action, validateFunc) {
    method pushRefAction (line 340) | async pushRefAction(ref, local) {
    method rebaseRefAction (line 356) | async rebaseRefAction(ref, local) {
    method mergeRefAction (line 362) | async mergeRefAction(ref, local) {
    method moveRef (line 368) | async moveRef(ref, targetNodeCommitTitle) {
    method triggerProgramEvents (line 398) | triggerProgramEvents() {
    method ensureRedraw (line 411) | async ensureRedraw() {
    method awaitAndClick (line 431) | async awaitAndClick(selector, time = 1000) {
    method clickOnNode (line 437) | async clickOnNode(nodeSelector) {
    method setApiListener (line 466) | setApiListener(apiPart, method, bodyMatcher = () => true) {

FILE: components/ComponentRoot.ts
  class ComponentRoot (line 3) | class ComponentRoot {
    method constructor (line 11) | constructor() { }
    method isSamePayload (line 13) | isSamePayload(value: any) {
    method clearApiCache (line 26) | clearApiCache() {

FILE: components/app/app.js
  class AppViewModel (line 10) | class AppViewModel {
    method constructor (line 11) | constructor(appContainer, server) {
    method getRepoList (line 46) | getRepoList() {
    method updateNode (line 57) | updateNode(parentElement) {
    method shown (line 60) | shown() {
    method updateAnimationFrame (line 92) | updateAnimationFrame(deltaT) {
    method onProgramEvent (line 96) | onProgramEvent(event) {
    method _handleRequestRememberRepo (line 115) | _handleRequestRememberRepo(event) {
    method _handleCredentialsRequested (line 120) | _handleCredentialsRequested(event) {
    method showModal (line 128) | showModal(modal) {
    method gitSetUserConfig (line 143) | gitSetUserConfig(bugTracking) {
    method enableBugtracking (line 151) | enableBugtracking() {
    method dismissBugtrackingNagscreen (line 154) | dismissBugtrackingNagscreen() {
    method dismissGitVersionError (line 158) | dismissGitVersionError() {
    method dismissNewVersion (line 162) | dismissNewVersion() {
    method templateChooser (line 165) | templateChooser(data) {

FILE: components/branches/branches.js
  class BranchesViewModel (line 15) | class BranchesViewModel extends ComponentRoot {
    method constructor (line 16) | constructor(server, graph, repoPath) {
    method checkoutBranch (line 50) | checkoutBranch(branch) {
    method updateNode (line 53) | updateNode(parentElement) {
    method clickFetch (line 56) | clickFetch() {
    method onProgramEvent (line 59) | onProgramEvent(event) {
    method _updateRefs (line 69) | async _updateRefs(forceRemoteFetch) {
    method branchRemove (line 140) | branchRemove(branch) {

FILE: components/commit/commit.js
  class CommitViewModel (line 9) | class CommitViewModel {
    method constructor (line 10) | constructor(gitNode) {
    method updateNode (line 46) | updateNode(parentElement) {
    method setData (line 50) | setData(args) {
    method updateLastAuthorDateFromNow (line 74) | updateLastAuthorDateFromNow(deltaT) {
    method updateAnimationFrame (line 83) | updateAnimationFrame(deltaT) {
    method stopClickPropagation (line 87) | stopClickPropagation(data, event) {
    method copyHash (line 91) | copyHash() {
    method gotoCommit (line 95) | gotoCommit(sha1) {

FILE: components/commitdiff/commitdiff.js
  class CommitDiff (line 7) | class CommitDiff {
    method constructor (line 8) | constructor(args) {
    method updateNode (line 21) | updateNode(parentElement) {

FILE: components/commitdiff/commitlinediff.js
  class CommitLineDiff (line 5) | class CommitLineDiff {
    method constructor (line 6) | constructor(args, fileLineDiff) {
    method getSpecificDiff (line 23) | getSpecificDiff() {
    method fileNameClick (line 37) | fileNameClick() {

FILE: components/crash/crash.js
  class CrashViewModel (line 6) | class CrashViewModel {
    method constructor (line 7) | constructor(err) {
    method updateNode (line 11) | updateNode(parentElement) {

FILE: components/gitErrors/gitErrors.js
  class GitErrorsViewModel (line 7) | class GitErrorsViewModel {
    method constructor (line 8) | constructor(server, repoPath) {
    method updateNode (line 16) | updateNode(parentElement) {
    method onProgramEvent (line 20) | onProgramEvent(event) {
    method _handleGitError (line 24) | _handleGitError(event) {
  class GitErrorViewModel (line 30) | class GitErrorViewModel {
    method constructor (line 31) | constructor(gitErrors, server, data) {
    method dismiss (line 51) | dismiss() {

FILE: components/graph/animateable.js
  class Animateable (line 6) | class Animateable extends Selectable {
    method constructor (line 7) | constructor(graph) {

FILE: components/graph/edge.js
  class EdgeViewModel (line 4) | class EdgeViewModel extends Animateable {
    method constructor (line 5) | constructor(graph, nodeAsha1, nodeBsha1) {
    method setGraphAttr (line 39) | setGraphAttr(val) {
    method edgeMouseOver (line 43) | edgeMouseOver() {
    method edgeMouseOut (line 52) | edgeMouseOut() {

FILE: components/graph/git-graph-actions.js
  class ActionBase (line 14) | class ActionBase {
    method constructor (line 15) | constructor(graph, text, style, icon) {
    method doPerform (line 34) | doPerform() {
    method dragEnter (line 45) | dragEnter() {
    method dragLeave (line 50) | dragLeave() {
    method mouseover (line 55) | mouseover() {
    method mouseout (line 59) | mouseout() {
  class Move (line 64) | class Move extends ActionBase {
    method constructor (line 65) | constructor(graph, node) {
    method perform (line 77) | perform() {
  class Reset (line 82) | class Reset extends ActionBase {
    method constructor (line 83) | constructor(graph, node) {
    method createHoverGraphic (line 103) | createHoverGraphic() {
    method perform (line 111) | perform() {
  class Rebase (line 137) | class Rebase extends ActionBase {
    method constructor (line 138) | constructor(graph, node) {
    method createHoverGraphic (line 152) | createHoverGraphic() {
    method perform (line 160) | perform() {
  class Merge (line 173) | class Merge extends ActionBase {
    method constructor (line 174) | constructor(graph, node) {
    method createHoverGraphic (line 188) | createHoverGraphic() {
    method perform (line 195) | perform() {
  class Push (line 211) | class Push extends ActionBase {
    method constructor (line 212) | constructor(graph, node) {
    method createHoverGraphic (line 225) | createHoverGraphic() {
    method perform (line 233) | perform() {
  class Checkout (line 252) | class Checkout extends ActionBase {
    method constructor (line 253) | constructor(graph, node) {
    method perform (line 267) | perform() {
  class Delete (line 272) | class Delete extends ActionBase {
    method constructor (line 273) | constructor(graph, node) {
    method perform (line 286) | perform() {
  class CherryPick (line 309) | class CherryPick extends ActionBase {
    method constructor (line 310) | constructor(graph, node) {
    method perform (line 320) | perform() {
  class Uncommit (line 333) | class Uncommit extends ActionBase {
    method constructor (line 334) | constructor(graph, node) {
    method perform (line 343) | perform() {
  class Revert (line 357) | class Revert extends ActionBase {
    method constructor (line 358) | constructor(graph, node) {
    method perform (line 367) | perform() {
  class Squash (line 375) | class Squash extends ActionBase {
    method constructor (line 376) | constructor(graph, node) {
    method createHoverGraphic (line 389) | createHoverGraphic() {
    method perform (line 397) | perform() {

FILE: components/graph/git-node.js
  class GitNodeViewModel (line 11) | class GitNodeViewModel extends Animateable {
    method constructor (line 12) | constructor(graph, sha1) {
    method getGraphAttr (line 118) | getGraphAttr() {
    method setGraphAttr (line 122) | setGraphAttr(val) {
    method render (line 127) | render() {
    method setData (line 155) | setData(logEntry) {
    method showBranchingForm (line 170) | showBranchingForm() {
    method showRefSearchForm (line 175) | showRefSearchForm(obj, event) {
    method createBranch (line 212) | createBranch() {
    method createTag (line 237) | createTag() {
    method toggleSelected (line 253) | toggleSelected() {
    method removeRef (line 269) | removeRef(ref) {
    method pushRef (line 277) | pushRef(ref) {
    method updateAnimationFrame (line 285) | updateAnimationFrame(deltaT) {
    method getPathToCommonAncestor (line 289) | getPathToCommonAncestor(node) {
    method isAncestor (line 300) | isAncestor(node) {
    method getRightToLeftStrike (line 309) | getRightToLeftStrike() {
    method getLeftToRightStrike (line 313) | getLeftToRightStrike() {
    method nodeMouseover (line 317) | nodeMouseover() {
    method nodeMouseout (line 321) | nodeMouseout() {
    method isViewable (line 325) | isViewable() {

FILE: components/graph/git-ref.js
  class RefViewModel (line 8) | class RefViewModel extends Selectable {
    method constructor (line 9) | constructor(fullRefName, graph) {
    method _colorFromHashOfString (line 87) | _colorFromHashOfString(string) {
    method dragStart (line 91) | dragStart() {
    method dragEnd (line 97) | dragEnd() {
    method moveTo (line 102) | moveTo(target, rewindWarnOverride) {
    method remove (line 176) | remove(isClientOnly) {
    method getLocalRef (line 206) | getLocalRef() {
    method getLocalRefFullName (line 210) | getLocalRefFullName() {
    method getRemoteRef (line 216) | getRemoteRef(remote) {
    method getRemoteRefFullName (line 220) | getRemoteRefFullName(remote) {
    method canBePushed (line 226) | canBePushed(remote) {
    method createRemoteRef (line 234) | createRemoteRef() {
    method checkout (line 245) | checkout() {

FILE: components/graph/graph.js
  class GraphViewModel (line 14) | class GraphViewModel extends ComponentRoot {
    method constructor (line 15) | constructor(server, repoPath) {
    method updateNode (line 89) | updateNode(parentElement) {
    method getNode (line 93) | getNode(sha1, logEntry) {
    method getRef (line 100) | getRef(ref, constructIfUnavailable) {
    method _loadNodesFromApi (line 113) | async _loadNodesFromApi() {
    method traverseNodeLeftParents (line 159) | traverseNodeLeftParents(node, callback) {
    method computeNode (line 167) | computeNode(nodes) {
    method getEdge (line 214) | getEdge(nodeAsha1, nodeBsha1) {
    method markNodesIdeologicalBranches (line 223) | markNodesIdeologicalBranches(refs) {
    method traverseNodeParents (line 249) | traverseNodeParents(node, callback) {
    method handleBubbledClick (line 260) | handleBubbledClick(elem, event) {
    method onProgramEvent (line 276) | onProgramEvent(event) {
    method updateAnimationFrame (line 293) | updateAnimationFrame(deltaT) {
    method _updateBranches (line 299) | async _updateBranches() {
    method setRemoteTags (line 314) | setRemoteTags(remoteTags) {
    method checkHeadMove (line 344) | checkHeadMove(toNode) {

FILE: components/graph/hover-actions.js
  class HoverViewModel (line 27) | class HoverViewModel {
    method constructor (line 28) | constructor() {
  class MergeViewModel (line 35) | class MergeViewModel extends HoverViewModel {
    method constructor (line 36) | constructor(graph, headNode, node) {
    method destroy (line 58) | destroy() {
  class RebaseViewModel (line 65) | class RebaseViewModel extends HoverViewModel {
    method constructor (line 66) | constructor(onto, nodesThatWillMove) {
  class ResetViewModel (line 84) | class ResetViewModel extends HoverViewModel {
    method constructor (line 85) | constructor(nodes) {
  class PushViewModel (line 99) | class PushViewModel extends HoverViewModel {
    method constructor (line 100) | constructor(fromNode, toNode) {
  class SquashViewModel (line 118) | class SquashViewModel extends HoverViewModel {
    method constructor (line 119) | constructor(from, onto) {

FILE: components/graph/selectable.js
  class Selectable (line 3) | class Selectable {
    method constructor (line 4) | constructor(graph) {

FILE: components/header/header.js
  class HeaderViewModel (line 10) | class HeaderViewModel {
    method constructor (line 11) | constructor(app) {
    method updateNode (line 24) | updateNode(parentElement) {
    method submitPath (line 28) | submitPath() {
    method onProgramEvent (line 32) | onProgramEvent(event) {
    method addCurrentPathToRepoList (line 41) | addCurrentPathToRepoList() {

FILE: components/home/home.js
  class HomeRepositoryViewModel (line 8) | class HomeRepositoryViewModel {
    method constructor (line 9) | constructor(home, path) {
    method updateState (line 23) | updateState() {
    method remove (line 40) | remove() {
  class HomeViewModel (line 46) | class HomeViewModel {
    method constructor (line 47) | constructor(app) {
    method updateNode (line 54) | updateNode(parentElement) {
    method shown (line 58) | shown() {
    method update (line 62) | update() {
    method template (line 77) | get template() {

FILE: components/imagediff/imagediff.js
  class ImageDiffViewModel (line 8) | class ImageDiffViewModel {
    method constructor (line 9) | constructor(args) {
    method updateNode (line 33) | updateNode(parentElement) {
    method invalidateDiff (line 37) | invalidateDiff() {}
    method newImageError (line 39) | newImageError() {
    method oldImageError (line 43) | oldImageError() {

FILE: components/login/login.js
  class LoginViewModel (line 7) | class LoginViewModel {
    method constructor (line 8) | constructor(server) {
    method updateNode (line 28) | updateNode(parentElement) {
    method login (line 32) | login() {

FILE: components/modals/forms.ts
  class FormModalViewModel (line 15) | class FormModalViewModel extends ModalViewModel {
    method constructor (line 19) | constructor(title: string, taModalName: string, showCancel: boolean) {
    method submit (line 26) | submit() {
  class CredentialsModalViewModel (line 31) | class CredentialsModalViewModel extends FormModalViewModel {
    method constructor (line 32) | constructor(remote: string) {
    method submit (line 38) | submit() {
  class AddRemoteModalViewModel (line 48) | class AddRemoteModalViewModel extends FormModalViewModel {
    method constructor (line 50) | constructor(path: string) {
    method submit (line 57) | async submit() {
  class AddSubmoduleModalViewModel (line 71) | class AddSubmoduleModalViewModel extends FormModalViewModel {
    method constructor (line 73) | constructor(path: string) {
    method submit (line 80) | async submit() {

FILE: components/modals/modalBase.ts
  class ModalViewModel (line 3) | class ModalViewModel {
    method constructor (line 7) | constructor(title: string, taModalName: string) {
    method close (line 12) | close() {
  class FormItems (line 17) | class FormItems {
    method constructor (line 22) | constructor(name: string, value: ko.Observable, type: string, autoFocu...
  class PromptOptions (line 30) | class PromptOptions {
    method constructor (line 36) | constructor(label: string, primary: boolean, taId: string, close: Func...

FILE: components/modals/prompts.ts
  class PromptModalViewModel (line 21) | class PromptModalViewModel extends ModalViewModel {
    method constructor (line 26) | constructor(title: string, taModalName: string, details: string, close...
    method close (line 34) | close(isYes: boolean = false, isMute: boolean = false) {
    method closeYes (line 39) | closeYes() {
    method closeYesMute (line 43) | closeYesMute() {
    method closeNo (line 47) | closeNo() {
  class YesNoModalViewModel (line 52) | class YesNoModalViewModel extends PromptModalViewModel {
    method constructor (line 53) | constructor(title: string, details: string, closeFunc: Function) {
  class YesNoMuteModalViewModel (line 60) | class YesNoMuteModalViewModel extends PromptModalViewModel {
    method constructor (line 61) | constructor(title: string, details: string, closeFunc: Function) {
  class TooManyFilesModalViewModel (line 69) | class TooManyFilesModalViewModel extends PromptModalViewModel {
    method constructor (line 70) | constructor(title: string, details: string, closeFunc: Function) {
  class TextEditModal (line 77) | class TextEditModal extends PromptModalViewModel {
    method constructor (line 78) | constructor(title: string, details: string, closeFunc: Function) {

FILE: components/path/path.js
  class SubRepositoryViewModel (line 16) | class SubRepositoryViewModel {
    method constructor (line 17) | constructor(server, path) {
  class PathViewModel (line 35) | class PathViewModel extends ComponentRoot {
    method constructor (line 36) | constructor(server, path) {
    method toggleShowCreateRepo (line 67) | toggleShowCreateRepo() {
    method updateShowCreateRepoMetadata (line 73) | updateShowCreateRepoMetadata() {
    method updateNode (line 81) | updateNode(parentElement) {
    method shown (line 84) | shown() {
    method updateAnimationFrame (line 87) | updateAnimationFrame(deltaT) {
    method updateStatus (line 90) | async updateStatus() {
    method initRepository (line 123) | initRepository() {
    method onProgramEvent (line 129) | async onProgramEvent(event) {
    method cloneRepository (line 141) | cloneRepository() {
    method createDir (line 158) | createDir() {

FILE: components/refreshbutton/refreshbutton.js
  class RefreshButton (line 8) | class RefreshButton {
    method constructor (line 9) | constructor(isLarge) {
    method refresh (line 14) | refresh() {
    method updateNode (line 19) | updateNode(parentElement) {

FILE: components/remotes/remotes.js
  class RemotesViewModel (line 9) | class RemotesViewModel {
    method constructor (line 10) | constructor(server, repoPath) {
    method updateNode (line 31) | updateNode(parentElement) {
    method clickFetch (line 35) | clickFetch() {
    method onProgramEvent (line 39) | async onProgramEvent(event) {
    method fetch (line 49) | async fetch(options) {
    method updateRemotes (line 112) | updateRemotes() {
    method showAddRemoteDialog (line 151) | showAddRemoteDialog() {
    method remoteRemove (line 155) | remoteRemove(remote) {

FILE: components/repository/repository.js
  class RepositoryViewModel (line 9) | class RepositoryViewModel {
    method constructor (line 10) | constructor(server, path) {
    method updateNode (line 46) | updateNode(parentElement) {
    method onProgramEvent (line 50) | onProgramEvent(event) {
    method updateAnimationFrame (line 64) | updateAnimationFrame(deltaT) {
    method refreshSubmoduleStatus (line 68) | refreshSubmoduleStatus() {
    method editGitignore (line 93) | editGitignore() {

FILE: components/staging/staging.js
  class StagingViewModel (line 16) | class StagingViewModel extends ComponentRoot {
    method constructor (line 17) | constructor(server, repoPath, graph) {
    method updateNode (line 123) | updateNode(parentElement) {
    method onProgramEvent (line 127) | onProgramEvent(event) {
    method _refreshContent (line 138) | async _refreshContent() {
    method loadStatus (line 192) | loadStatus(status) {
    method setFiles (line 212) | setFiles(files) {
    method toggleAmend (line 234) | toggleAmend() {
    method toggleEmptyCommit (line 250) | toggleEmptyCommit() {
    method resetMessages (line 256) | resetMessages() {
    method commit (line 270) | commit() {
    method commitnpush (line 295) | commitnpush() {
    method conflictResolution (line 340) | conflictResolution(apiPath) {
    method invalidateFilesDiffs (line 351) | invalidateFilesDiffs() {
    method cancelAmendEmpty (line 357) | cancelAmendEmpty() {
    method discardAllChanges (line 361) | discardAllChanges() {
    method stashAll (line 374) | stashAll() {
    method toggleAllStages (line 380) | toggleAllStages() {
    method onEnter (line 387) | onEnter(d, e) {
    method onAltEnter (line 394) | onAltEnter(d, e) {
  class FileViewModel (line 402) | class FileViewModel {
    method constructor (line 403) | constructor(staging, name, oldName, displayName) {
    method getSpecificDiff (line 455) | getSpecificDiff() {
    method setState (line 471) | setState(state) {
    method toggleStaged (line 489) | toggleStaged() {
    method discardChanges (line 498) | discardChanges() {
    method ignoreFile (line 526) | ignoreFile() {
    method resolveConflict (line 539) | resolveConflict() {
    method launchMergeTool (line 545) | launchMergeTool() {
    method toggleDiffs (line 555) | toggleDiffs() {
    method patchClick (line 559) | patchClick() {

FILE: components/stash/stash.js
  class StashItemViewModel (line 11) | class StashItemViewModel {
    method constructor (line 12) | constructor(stash, data) {
    method apply (line 34) | apply() {
    method drop (line 40) | drop() {
    method toggleShowCommitDiffs (line 53) | toggleShowCommitDiffs() {
  class StashViewModel (line 58) | class StashViewModel extends ComponentRoot {
    method constructor (line 59) | constructor(server, repoPath) {
    method updateNode (line 72) | updateNode(parentElement) {
    method onProgramEvent (line 76) | onProgramEvent(event) {
    method _refresh (line 82) | async _refresh() {
    method toggleShowStash (line 112) | toggleShowStash() {

FILE: components/submodules/submodules.js
  class SubmodulesViewModel (line 10) | class SubmodulesViewModel extends ComponentRoot {
    method constructor (line 11) | constructor(server, repoPath) {
    method onProgramEvent (line 22) | onProgramEvent(event) {
    method updateNode (line 28) | updateNode(parentElement) {
    method _fetchSubmodules (line 35) | async _fetchSubmodules() {
    method updateSubmodules (line 45) | updateSubmodules() {
    method showAddSubmoduleDialog (line 51) | showAddSubmoduleDialog() {
    method submoduleLinkClick (line 55) | submoduleLinkClick(submodule) {
    method submodulePathClick (line 59) | submodulePathClick(submodule) {
    method submoduleRemove (line 63) | submoduleRemove(submodule) {

FILE: components/textdiff/textdiff.js
  class WordWrap (line 14) | class WordWrap {
    method constructor (line 15) | constructor() {
  class Type (line 26) | class Type {
    method constructor (line 27) | constructor() {
  class WhiteSpace (line 47) | class WhiteSpace {
    method constructor (line 48) | constructor() {
  class TextDiffViewModel (line 59) | class TextDiffViewModel {
    method constructor (line 60) | constructor(args) {
    method updateNode (line 94) | updateNode(parentElement) {
    method getDiffArguments (line 98) | getDiffArguments() {
    method invalidateDiff (line 108) | invalidateDiff() {
    method getDiffJson (line 113) | getDiffJson() {
    method render (line 138) | render() {
    method loadMore (line 194) | loadMore() {
    method getPatchCheckBox (line 199) | getPatchCheckBox(symbol, index, isActive) {
    method togglePatchLine (line 208) | togglePatchLine(index) {

FILE: public/main.js
  function openUngitBrowser (line 18) | function openUngitBrowser(pathToNavigateTo) {
  function launch (line 23) | function launch(callback) {
  function checkIfUngitIsRunning (line 48) | function checkIfUngitIsRunning(callback) {

FILE: public/source/knockout-bindings.js
  function scrollToEndCheck (line 152) | function scrollToEndCheck() {
  function handleElementFocusIn (line 170) | function handleElementFocusIn() {
  function handleElementFocusOut (line 174) | function handleElementFocusOut() {

FILE: public/source/main.js
  function WindowTitle (line 63) | function WindowTitle() {

FILE: public/source/navigation.js
  function parseHash (line 15) | function parseHash(newHash, oldHash) {

FILE: public/source/server.js
  function Server (line 19) | function Server() {

FILE: scripts/build.js
  function lessFile (line 123) | async function lessFile(source, destination) {
  function browserifyFile (line 137) | async function browserifyFile(source, destination) {
  function copyToFolder (line 151) | async function copyToFolder(source, destination) {

FILE: scripts/electronzip.js
  function zipDirectory (line 37) | async function zipDirectory(source, destination) {

FILE: source/bugtracker.js
  class BugTracker (line 11) | class BugTracker {
    method constructor (line 12) | constructor(subsystem) {
    method notify (line 21) | notify(exception) {

FILE: source/git-api.js
  class RepoWatcher (line 56) | class RepoWatcher extends EventEmitter {
    method constructor (line 57) | constructor() {
    method watchItem (line 62) | async watchItem(name, item, filter) {
    method addWorkdir (line 80) | addWorkdir(item, filter) {
    method addGit (line 83) | addGit(item, filter) {
    method close (line 86) | close() {

FILE: source/ungit-plugin.js
  class UngitPlugin (line 12) | class UngitPlugin {
    method constructor (line 13) | constructor(args) {
    method init (line 24) | init(env) {
    method compile (line 44) | compile() {

FILE: source/utils/cache.js
  class OurCache (line 5) | class OurCache extends NodeCache {
    method constructor (line 6) | constructor() {
    method resolveFunc (line 17) | resolveFunc(key) {
    method registerFunc (line 49) | registerFunc(func, key, ttl) {
    method invalidateFunc (line 78) | invalidateFunc(key) {
    method deregisterFunc (line 87) | deregisterFunc(key) {
Condensed preview — 175 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (719K chars).
[
  {
    "path": ".gitattributes",
    "chars": 68,
    "preview": "* text=auto eol=lf\n/bin/credentials-helper eol=lf\n/bin/ungit eol=lf\n"
  },
  {
    "path": ".github/workflows/bump.yml",
    "chars": 1303,
    "preview": "name: Bump Dependencies\n\non:\n  push:\n    branches:\n      - master\n  schedule:\n    - cron: '0 0 * * *'\n  workflow_dispatc"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 6364,
    "preview": "name: CI\n\non: [push, pull_request]\n\npermissions:\n  id-token: write   # Required for npm OIDC provenance publishing\n  con"
  },
  {
    "path": ".gitignore",
    "chars": 307,
    "preview": "# Ignore generated folders\n.nyc_output/\nbuild/\ncoverage/\ndist/\nnode_modules/\npublic/css/\npublic/fonts/glyphicons-*\npubli"
  },
  {
    "path": ".mochaclicktest.json",
    "chars": 126,
    "preview": "{\n  \"spec\": \"clicktests/spec.*.js\",\n  \"file\": \"./source/utils/logger.js\",\n  \"timeout\": 20000,\n  \"bail\": true,\n  \"exit\": "
  },
  {
    "path": ".mochatest.json",
    "chars": 104,
    "preview": "{\n  \"spec\": \"test/spec.*.js\",\n  \"file\": \"./source/utils/logger.js\",\n  \"timeout\": 12000,\n  \"exit\": true\n}"
  },
  {
    "path": ".npmignore",
    "chars": 622,
    "preview": "# Ignore whole folders\n.github/\n.nyc_output/\nassets/\nbuild/\nclicktests/\ncoverage/\ndist/\nscripts/\ntest/\n\n# Ignore non-com"
  },
  {
    "path": ".prettierignore",
    "chars": 304,
    "preview": "# Generated folders\n.nyc_output/\nbuild/\ncoverage/\n\n# Third-party files\npublic/css/\npublic/js\npublic/vendor/\n\n# Browserif"
  },
  {
    "path": ".prettierrc",
    "chars": 193,
    "preview": "{\n  \"endOfLine\": \"lf\",\n  \"trailingComma\": \"es5\",\n  \"jsdocUseInlineCommentForASingleTagBlock\": true,\n  \"plugins\": [\"@home"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 44471,
    "preview": "# Change Log\nAll notable changes to this project will be documented in this file.\nThis project adheres to [Semantic Vers"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 5521,
    "preview": "# Contributing Guidelines\n\nThese are the contributing guidelines as well as some documentation on how the code is struct"
  },
  {
    "path": "LICENSE.md",
    "chars": 1075,
    "preview": "MIT License\n\nCopyright (c) 2013-2026 Fredrik Norén\n\nPermission is hereby granted, free of charge, to any person obtainin"
  },
  {
    "path": "MERGETOOL.md",
    "chars": 3968,
    "preview": "If you have your own merge tool that you would like to use, such as Kaleidoscope or p4merge, you can configure ungit to "
  },
  {
    "path": "PLUGINS.md",
    "chars": 3109,
    "preview": "Writing Ungit plugins\n=====================\n\nIt's super easy to write an Ungit plugin. Here's how to write a completely "
  },
  {
    "path": "README.md",
    "chars": 8057,
    "preview": "ungit\n======\n[![Release](https://img.shields.io/github/v/release/FredrikNoren/ungit)](https://github.com/FredrikNoren/un"
  },
  {
    "path": "appveyor.yml",
    "chars": 744,
    "preview": "skip_branch_with_pr: true\nimage: Visual Studio 2022\n\nenvironment:\n  matrix:\n    - nodejs_version: '' # latest\n    - node"
  },
  {
    "path": "bin/credentials-helper",
    "chars": 881,
    "preview": "#!/usr/bin/env node\nconst http = require('http');\nconst socketId = process.argv[2];\nconst portAndRootPath = process.argv"
  },
  {
    "path": "bin/ungit",
    "chars": 3117,
    "preview": "#!/usr/bin/env node\n\nconst startLaunchTime = Date.now();\n\nconst config = require('../source/config');\nconst openPromise "
  },
  {
    "path": "clicktests/environment.js",
    "chars": 15569,
    "preview": "'use strict';\nconst logger = require('../source/utils/logger');\nconst child_process = require('child_process');\nconst pu"
  },
  {
    "path": "clicktests/spec.authentication.js",
    "chars": 1426,
    "preview": "'use strict';\nconst testuser = { username: 'testuser', password: 'testpassword' };\nconst environment = require('./enviro"
  },
  {
    "path": "clicktests/spec.bare.js",
    "chars": 752,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[BARE]', () => {\n  be"
  },
  {
    "path": "clicktests/spec.branches.js",
    "chars": 8337,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\nconst _ = require('lodash');\n\nde"
  },
  {
    "path": "clicktests/spec.commands.js",
    "chars": 3827,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\nconst _ = require('lodash');\n\nco"
  },
  {
    "path": "clicktests/spec.discard.js",
    "chars": 3071,
    "preview": "'use strict';\n\nconst muteGraceTimeDuration = 5000;\nconst createAndDiscard = async (env, testRepoPath, dialogButtonToClic"
  },
  {
    "path": "clicktests/spec.generic.js",
    "chars": 9960,
    "preview": "'use strict';\nconst environment = require('./environment')({\n  serverStartupOptions: ['--no-disableDiscardWarning'],\n  r"
  },
  {
    "path": "clicktests/spec.load-ahead.js",
    "chars": 1581,
    "preview": "'use strict';\nconst environment = require('./environment')({\n  serverStartupOptions: ['--numberOfNodesPerLoad=1'],\n});\nc"
  },
  {
    "path": "clicktests/spec.no-header.js",
    "chars": 773,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[NO-HEADER]', () => {"
  },
  {
    "path": "clicktests/spec.remotes.js",
    "chars": 5976,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst mkdirp = require('mkdirp').mkdirp;\nconst rimraf = re"
  },
  {
    "path": "clicktests/spec.screens.js",
    "chars": 2773,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst mkdirp = require('mkdirp').mkdirp;\nconst rimraf = re"
  },
  {
    "path": "clicktests/spec.stash.js",
    "chars": 1212,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[STASH]', () => {\n  b"
  },
  {
    "path": "clicktests/spec.submodules.js",
    "chars": 1668,
    "preview": "'use strict';\nconst environment = require('./environment')();\nconst testRepoPaths = [];\n\ndescribe('[SUMBODULES]', () => "
  },
  {
    "path": "components/ComponentRoot.ts",
    "chars": 619,
    "preview": "declare var ungit: any;\n\nexport class ComponentRoot {\n  _apiCache: string;\n  defaultDebounceOption = {\n    maxWait: 1500"
  },
  {
    "path": "components/app/app.html",
    "chars": 1698,
    "preview": "<!-- ko component: header -->\n<!-- /ko -->\n\n<div class=\"app\">\n  <div class=\"container\" data-bind=\"shown: shown\">\n    <di"
  },
  {
    "path": "components/app/app.js",
    "chars": 6212,
    "preview": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst storage = require('ungit-storage')"
  },
  {
    "path": "components/app/app.less",
    "chars": 104,
    "preview": ".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",
    "chars": 137,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"app\": \"app.html\"\n    },\n    \"javascript\": \"app.bundle.js\",\n    \"css\":"
  },
  {
    "path": "components/branches/branches.html",
    "chars": 1917,
    "preview": "<div class=\"btn-group branch\">\n  <button type=\"button\" class=\"btn btn-default btn-main\" data-bind=\"click: updateRefs\">\n "
  },
  {
    "path": "components/branches/branches.js",
    "chars": 4967,
    "preview": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = re"
  },
  {
    "path": "components/branches/branches.less",
    "chars": 435,
    "preview": "@import 'public/less/variables.less';\n\n.branch {\n  .options {\n    font-size: inherit;\n    color: @gray-dark;\n\n    label "
  },
  {
    "path": "components/branches/ungit-plugin.json",
    "chars": 157,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"branches\": \"branches.html\"\n    },\n    \"javascript\": \"branches.bundle."
  },
  {
    "path": "components/commit/commit.html",
    "chars": 2806,
    "preview": "<div\n  class=\"commit\"\n  data-bind=\"css: { highlighted: highlighted, hover: nodeIsMousehover, selected: selected }\"\n>\n  <"
  },
  {
    "path": "components/commit/commit.js",
    "chars": 3291,
    "preview": "const ko = require('knockout');\nconst md5 = require('blueimp-md5');\nconst moment = require('moment');\nconst octicons = r"
  },
  {
    "path": "components/commit/commit.less",
    "chars": 1805,
    "preview": "@import 'public/less/variables.less';\n\n.commit {\n  position: relative;\n\n  &.highlighted {\n    z-index: 2;\n\n    .commit-b"
  },
  {
    "path": "components/commit/ungit-plugin.json",
    "chars": 149,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"commit\": \"commit.html\"\n    },\n    \"javascript\": \"commit.bundle.js\",\n "
  },
  {
    "path": "components/commitdiff/commitdiff.html",
    "chars": 1761,
    "preview": "<div class=\"btn-toolbar\" data-bind=\"visible: showDiffButtons\">\n  <div class=\"btn-group btn-group-xs\">\n    <button\n      "
  },
  {
    "path": "components/commitdiff/commitdiff.js",
    "chars": 857,
    "preview": "const ko = require('knockout');\nconst CommitLineDiff = require('./commitlinediff.js').CommitLineDiff;\nconst components ="
  },
  {
    "path": "components/commitdiff/commitdiff.less",
    "chars": 853,
    "preview": ".commitdiff {\n  width: 100%;\n\n  .file {\n    margin-top: 5px;\n    background: rgba(255, 255, 255, 0.16);\n    border-radiu"
  },
  {
    "path": "components/commitdiff/commitlinediff.js",
    "chars": 1423,
    "preview": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst programEvents = require('ungit-pro"
  },
  {
    "path": "components/commitdiff/ungit-plugin.json",
    "chars": 165,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"commitdiff\": \"commitdiff.html\"\n    },\n    \"javascript\": \"commitdiff.b"
  },
  {
    "path": "components/crash/crash.html",
    "chars": 1950,
    "preview": "<div class=\"container\">\n  <div class=\"panel panel-default crash\">\n    <div class=\"panel-body\">\n      <h1>Whooops</h1>\n  "
  },
  {
    "path": "components/crash/crash.js",
    "chars": 339,
    "preview": "const ko = require('knockout');\nconst components = require('ungit-components');\n\ncomponents.register('crash', (err) => n"
  },
  {
    "path": "components/crash/crash.less",
    "chars": 31,
    "preview": ".crash {\n  margin-top: 20px;\n}\n"
  },
  {
    "path": "components/crash/ungit-plugin.json",
    "chars": 145,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"crash\": \"crash.html\"\n    },\n    \"javascript\": \"crash.bundle.js\",\n    "
  },
  {
    "path": "components/gitErrors/gitErrors.html",
    "chars": 791,
    "preview": "<!-- ko foreach: gitErrors -->\n<div class=\"alert\" data-bind=\"css: { 'alert-danger': !isWarning, 'alert-warning': isWarni"
  },
  {
    "path": "components/gitErrors/gitErrors.js",
    "chars": 1584,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\n\nc"
  },
  {
    "path": "components/gitErrors/ungit-plugin.json",
    "chars": 133,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"gitErrors\": \"gitErrors.html\"\n    },\n    \"javascript\": \"gitErrors.bund"
  },
  {
    "path": "components/graph/animateable.js",
    "chars": 1156,
    "preview": "const ko = require('knockout');\nconst Selectable = require('./selectable');\n\nrequire('mina');\n\nclass Animateable extends"
  },
  {
    "path": "components/graph/edge.js",
    "chars": 1606,
    "preview": "const ko = require('knockout');\nconst Animateable = require('./animateable');\n\nclass EdgeViewModel extends Animateable {"
  },
  {
    "path": "components/graph/git-graph-actions.js",
    "chars": 13634,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nco"
  },
  {
    "path": "components/graph/git-node.js",
    "chars": 10788,
    "preview": "const $ = require('jquery');\nconst ko = require('knockout');\nconst components = require('ungit-components');\nconst progr"
  },
  {
    "path": "components/graph/git-ref.js",
    "chars": 8915,
    "preview": "const ko = require('knockout');\nconst md5 = require('blueimp-md5');\nconst octicons = require('octicons');\nconst programE"
  },
  {
    "path": "components/graph/graph-graphics.html",
    "chars": 3036,
    "preview": "<svg\n  class=\"graphLog\"\n  xmlns=\"http://www.w3.org/2000/svg\"\n  version=\"1.1\"\n  data-bind=\"attr: { width: graphWidth, hei"
  },
  {
    "path": "components/graph/graph.html",
    "chars": 4588,
    "preview": "<div class=\"graph\" data-bind=\"scrolledToEnd: scrolledToEnd, click: handleBubbledClick\">\n  <!-- ko template: { name: 'gra"
  },
  {
    "path": "components/graph/graph.js",
    "chars": 11690,
    "preview": "const ko = require('knockout');\nconst _ = require('lodash');\nconst moment = require('moment');\nconst octicons = require("
  },
  {
    "path": "components/graph/graph.less",
    "chars": 3672,
    "preview": "@import 'public/less/variables.less';\n\n.graph {\n  position: relative;\n  display: inline-block;\n  width: 100%;\n\n  .graphL"
  },
  {
    "path": "components/graph/hover-actions.js",
    "chars": 3569,
    "preview": "const getEdgeModelWithD = (d, stroke, strokeWidth, strokeDasharray, markerEnd) => ({\n  d,\n  stroke: stroke ? stroke : '#"
  },
  {
    "path": "components/graph/selectable.js",
    "chars": 538,
    "preview": "var ko = require('knockout');\n\nclass Selectable {\n  constructor(graph) {\n    this.selected = ko.computed({\n      read() "
  },
  {
    "path": "components/graph/ungit-plugin.json",
    "chars": 191,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"graph\": \"graph.html\",\n      \"graphGraphics\": \"graph-graphics.html\"\n  "
  },
  {
    "path": "components/header/header.html",
    "chars": 1298,
    "preview": "<div class=\"navbar navbar-default navbar-fixed-top\">\n  <a\n    class=\"backlink\"\n    href=\"#/\"\n    data-toggle=\"tooltip\"\n "
  },
  {
    "path": "components/header/header.js",
    "chars": 1451,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nco"
  },
  {
    "path": "components/header/header.less",
    "chars": 1454,
    "preview": "@import 'public/less/variables.less';\n\nhtml {\n  scroll-behavior: smooth;\n  scroll-padding-top: 100px;\n}\n\n.navbarPadder {"
  },
  {
    "path": "components/header/ungit-plugin.json",
    "chars": 149,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"header\": \"header.html\"\n    },\n    \"javascript\": \"header.bundle.js\",\n "
  },
  {
    "path": "components/home/home.html",
    "chars": 981,
    "preview": "<div class=\"container home animated fadeInLeft\" data-bind=\"shown: shown\">\n  <div class=\"nux\" data-bind=\"visible: showNux"
  },
  {
    "path": "components/home/home.js",
    "chars": 2070,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nco"
  },
  {
    "path": "components/home/home.less",
    "chars": 385,
    "preview": ".home {\n  .nux {\n    text-align: center;\n\n    .logo-large {\n      margin-top: 20px;\n      margin-bottom: 50px;\n    }\n  }"
  },
  {
    "path": "components/home/ungit-plugin.json",
    "chars": 141,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"home\": \"home.html\"\n    },\n    \"javascript\": \"home.bundle.js\",\n    \"cs"
  },
  {
    "path": "components/imagediff/imagediff.html",
    "chars": 994,
    "preview": "<!-- ko if: isShowingDiffs -->\n<!-- ko if: state() == 'new' -->\n<div class=\"imageDiff img-added\">\n  <img data-bind=\"attr"
  },
  {
    "path": "components/imagediff/imagediff.js",
    "chars": 1442,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nco"
  },
  {
    "path": "components/imagediff/imagediff.less",
    "chars": 178,
    "preview": ".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.i"
  },
  {
    "path": "components/imagediff/ungit-plugin.json",
    "chars": 161,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"imagediff\": \"imagediff.html\"\n    },\n    \"javascript\": \"imagediff.bund"
  },
  {
    "path": "components/login/login.html",
    "chars": 1015,
    "preview": "<div class=\"container\">\n  <div data-bind=\"visible: status() == 'loading'\"></div>\n\n  <div class=\"login col-lg-6\" data-bin"
  },
  {
    "path": "components/login/login.js",
    "chars": 1233,
    "preview": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst signals = require('signals');\n\ncom"
  },
  {
    "path": "components/login/ungit-plugin.json",
    "chars": 121,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"login\": \"login.html\"\n    },\n    \"javascript\": \"login.bundle.js\"\n  }\n}"
  },
  {
    "path": "components/modals/formModal.html",
    "chars": 1164,
    "preview": "<div class=\"modal fade\" tabindex=\"-1\" role=\"dialog\">\n  <div class=\"modal-dialog\">\n    <div class=\"modal-content\">\n      "
  },
  {
    "path": "components/modals/forms.ts",
    "chars": 2830,
    "preview": "import * as ko from 'knockout';\nimport { ModalViewModel, FormItems } from './modalBase';\ndeclare const ungit: any;\n\nungi"
  },
  {
    "path": "components/modals/modalBase.ts",
    "chars": 920,
    "preview": "declare const ungit: any;\n\nexport class ModalViewModel {\n  title: string\n  taModalName: string\n  timestamp = new Date()."
  },
  {
    "path": "components/modals/modals.ts",
    "chars": 37,
    "preview": "import './forms';\nimport './prompts';"
  },
  {
    "path": "components/modals/promptModal.html",
    "chars": 680,
    "preview": "<div class=\"modal fade\" tabindex=\"-1\" role=\"dialog\" data-bind=\"attr: { 'data-ta-container': taModalName }\">\n  <div class"
  },
  {
    "path": "components/modals/prompts.ts",
    "chars": 3272,
    "preview": "import * as ko from 'knockout';\nimport { ModalViewModel, PromptOptions } from './modalBase';\ndeclare const ungit: any;\n\n"
  },
  {
    "path": "components/modals/ungit-plugin.json",
    "chars": 170,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"formModal\": \"formModal.html\",\n      \"promptModal\": \"promptModal.html\""
  },
  {
    "path": "components/path/path.html",
    "chars": 3412,
    "preview": "<div class=\"path\" data-bind=\"shown: shown\">\n  <!-- ko if: status() == 'uninited' -->\n  <div class=\"uninited container\">\n"
  },
  {
    "path": "components/path/path.js",
    "chars": 5694,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nco"
  },
  {
    "path": "components/path/path.less",
    "chars": 242,
    "preview": ".create-dir {\n  margin-top: 50px;\n}\n\n.create-repo-container {\n  background-color: #643a44;\n  box-shadow: 0 -1px 15px #25"
  },
  {
    "path": "components/path/ungit-plugin.json",
    "chars": 141,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"path\": \"path.html\"\n    },\n    \"javascript\": \"path.bundle.js\",\n    \"cs"
  },
  {
    "path": "components/refreshbutton/refreshbutton.html",
    "chars": 212,
    "preview": "<button\n  class=\"btn btn-default refresh-button\"\n  data-bind=\"html: refreshIcon, css: { 'btn-lg': isLarge }, click: refr"
  },
  {
    "path": "components/refreshbutton/refreshbutton.js",
    "chars": 618,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nco"
  },
  {
    "path": "components/refreshbutton/refreshbutton.less",
    "chars": 88,
    "preview": ".toolbar {\n  .refresh-button {\n    background: rgba(0, 0, 0, 0.1);\n    border: 0;\n  }\n}\n"
  },
  {
    "path": "components/refreshbutton/ungit-plugin.json",
    "chars": 177,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"refreshbutton\": \"refreshbutton.html\"\n    },\n    \"javascript\": \"refres"
  },
  {
    "path": "components/remotes/remotes.html",
    "chars": 1267,
    "preview": "<div class=\"btn-group fetchButton\">\n  <button\n    type=\"button\"\n    class=\"btn btn-default btn-main\"\n    data-bind=\"clic"
  },
  {
    "path": "components/remotes/remotes.js",
    "chars": 5263,
    "preview": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = re"
  },
  {
    "path": "components/remotes/ungit-plugin.json",
    "chars": 127,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"remotes\": \"remotes.html\"\n    },\n    \"javascript\": \"remotes.bundle.js\""
  },
  {
    "path": "components/repository/repository.html",
    "chars": 1314,
    "preview": "<div class=\"repository-view animated fadeInLeft\" data-bind=\"attr: { style: 'tab-size: ' + ungit.config.tabSize }\">\n  <!-"
  },
  {
    "path": "components/repository/repository.js",
    "chars": 4875,
    "preview": "const ko = require('knockout');\nconst octicons = require('octicons');\nconst components = require('ungit-components');\nco"
  },
  {
    "path": "components/repository/repository.less",
    "chars": 210,
    "preview": ".repository-view {\n  position: relative;\n  height: auto;\n  margin-bottom: 1px;\n  padding-bottom: 1px;\n\n  .repository-act"
  },
  {
    "path": "components/repository/ungit-plugin.json",
    "chars": 165,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"repository\": \"repository.html\"\n    },\n    \"javascript\": \"repository.b"
  },
  {
    "path": "components/staging/staging.html",
    "chars": 9084,
    "preview": "<div\n  class=\"staging panel panel-default\"\n  data-bind=\"css: { commitValidationError: commitValidationError }\"\n>\n  <div "
  },
  {
    "path": "components/staging/staging.js",
    "chars": 18862,
    "preview": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = re"
  },
  {
    "path": "components/staging/staging.less",
    "chars": 4117,
    "preview": "@import 'public/less/variables.less';\n\n.staging {\n  background: #643a44;\n  box-shadow: 0 -1px 15px #252833;\n  color: #b8"
  },
  {
    "path": "components/staging/ungit-plugin.json",
    "chars": 153,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"staging\": \"staging.html\"\n    },\n    \"javascript\": \"staging.bundle.js\""
  },
  {
    "path": "components/stash/stash.html",
    "chars": 1690,
    "preview": "<div class=\"stash-toggle stash-toggle-text\" data-bind=\"click: toggleShowStash, visible: !isShow()\">\n  <span class=\"expan"
  },
  {
    "path": "components/stash/stash.js",
    "chars": 3575,
    "preview": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst moment = requir"
  },
  {
    "path": "components/stash/stash.less",
    "chars": 564,
    "preview": ".stash {\n  z-index: 4;\n  margin-left: 20px;\n  margin-right: 20px;\n  margin-bottom: -15px;\n  background: #55323c;\n\n  h4 {"
  },
  {
    "path": "components/stash/ungit-plugin.json",
    "chars": 145,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"stash\": \"stash.html\"\n    },\n    \"javascript\": \"stash.bundle.js\",\n    "
  },
  {
    "path": "components/submodules/submodules.html",
    "chars": 1624,
    "preview": "<div class=\"btn-group fetchButton submodule\">\n  <button type=\"button\" class=\"btn btn-default btn-main\" data-bind=\"click:"
  },
  {
    "path": "components/submodules/submodules.js",
    "chars": 2571,
    "preview": "const ko = require('knockout');\nconst _ = require('lodash');\nconst octicons = require('octicons');\nconst components = re"
  },
  {
    "path": "components/submodules/ungit-plugin.json",
    "chars": 136,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"submodules\": \"submodules.html\"\n    },\n    \"javascript\": \"submodules.b"
  },
  {
    "path": "components/textdiff/textdiff.html",
    "chars": 387,
    "preview": "<!-- ko if: isShowingDiffs -->\n<div class=\"textdiff\">\n  <!-- ko if: isParsed -->\n  <div\n    data-bind=\"template: {nodes:"
  },
  {
    "path": "components/textdiff/textdiff.js",
    "chars": 6973,
    "preview": "const ko = require('knockout');\nconst components = require('ungit-components');\nconst diff2html = require('diff2html');\n"
  },
  {
    "path": "components/textdiff/textdiff.less",
    "chars": 78,
    "preview": ".textdiff {\n  .load-more {\n    padding: 10px 0;\n    text-align: center;\n  }\n}\n"
  },
  {
    "path": "components/textdiff/ungit-plugin.json",
    "chars": 157,
    "preview": "{\n  \"exports\": {\n    \"knockoutTemplates\": {\n      \"textdiff\": \"textdiff.html\"\n    },\n    \"javascript\": \"textdiff.bundle."
  },
  {
    "path": "eslint.config.mjs",
    "chars": 1984,
    "preview": "import js from '@eslint/js';\nimport globals from 'globals';\nimport mochaPlugin from 'eslint-plugin-mocha';\nimport nodePl"
  },
  {
    "path": "package.json",
    "chars": 3188,
    "preview": "{\n  \"name\": \"ungit\",\n  \"productName\": \"ungit\",\n  \"author\": \"Fredrik Norén <fredrik.jw.noren@gmail.com>\",\n  \"description\""
  },
  {
    "path": "public/index.html",
    "chars": 1768,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <meta name=\"viewport\" content=\"width=device-w"
  },
  {
    "path": "public/less/bootstrap.less",
    "chars": 1626,
    "preview": "// Core variables and mixins\n@import '../../node_modules/bootstrap/less/mixins.less';\n\n// Reset\n@import '../../node_modu"
  },
  {
    "path": "public/less/d2h.less",
    "chars": 1213,
    "preview": "// diff2html style overrides\n\n.d2h-file-wrapper {\n  border: none;\n  margin-bottom: 0;\n}\n\n.d2h-file-header {\n  display: n"
  },
  {
    "path": "public/less/styles.less",
    "chars": 2347,
    "preview": "@import 'variables.less';\n@import 'bootstrap.less';\n@import (less) '../../node_modules/nprogress/nprogress.css';\n@import"
  },
  {
    "path": "public/less/variables.less",
    "chars": 1366,
    "preview": "@import '../../node_modules/bootstrap/less/variables.less';\n\n// Application variables\n// -------------------------------"
  },
  {
    "path": "public/main.js",
    "chars": 3864,
    "preview": "var startLaunchTime = Date.now();\n\nvar child_process = require('child_process');\nvar path = require('path');\nconst { enc"
  },
  {
    "path": "public/source/bootstrap.js",
    "chars": 155,
    "preview": "/*\n * Import the Bootstrap components individually.\n */\n\nrequire('bootstrap/js/dropdown');\nrequire('bootstrap/js/modal')"
  },
  {
    "path": "public/source/components.js",
    "chars": 612,
    "preview": "const components = {};\nmodule.exports = components;\nungit.components = components;\n\ncomponents.registered = {};\n\ncompone"
  },
  {
    "path": "public/source/jquery-ui.js",
    "chars": 566,
    "preview": "/*\n * Import the autocomplete widget and its dependencies.\n * The current order of the imports is required.\n */\n\n// All "
  },
  {
    "path": "public/source/knockout-bindings.js",
    "chars": 8638,
    "preview": "/* eslint no-unused-vars: \"off\" */\n\nvar _ = require('lodash');\nvar ko = require('knockout');\nvar $ = require('jquery');\n"
  },
  {
    "path": "public/source/main.js",
    "chars": 5264,
    "preview": "var $ = require('jquery');\njQuery = $; // this is for old backward compatability of bootrap modules\nvar ko = require('kn"
  },
  {
    "path": "public/source/navigation.js",
    "chars": 667,
    "preview": "var programEvents = require('ungit-program-events');\n\nvar navigation = {};\nmodule.exports = navigation;\n\nvar hasher = (n"
  },
  {
    "path": "public/source/program-events.js",
    "chars": 230,
    "preview": "const signals = require('signals');\n\nconst programEvents = new signals.Signal();\nmodule.exports = programEvents;\nungit.p"
  },
  {
    "path": "public/source/server.js",
    "chars": 6923,
    "preview": "var programEvents = require('ungit-program-events');\n\nvar rootPath = (ungit.config && ungit.config.rootPath) || '';\nvar "
  },
  {
    "path": "public/source/storage.js",
    "chars": 659,
    "preview": "/**\n * A wrapper around LocalStorage to support environments where LocalStorage is not available.\n * Stores and retrieve"
  },
  {
    "path": "public/vendor/css/animate.css",
    "chars": 61119,
    "preview": "@charset \"UTF-8\";\n/*\nAnimate.css - http://daneden.me/animate\nLicensed under the MIT license\n\nCopyright (c) 2013 Daniel E"
  },
  {
    "path": "scripts/build.js",
    "chars": 5752,
    "preview": "const fsSync = require('fs');\nconst fs = fsSync.promises;\nconst path = require('path');\n\nconst browserify = require('bro"
  },
  {
    "path": "scripts/electronpackage.js",
    "chars": 1434,
    "preview": "const process = require('process');\nconst path = require('path');\nconst fs = require('fs').promises;\nconst electronPacka"
  },
  {
    "path": "scripts/electronzip.js",
    "chars": 1422,
    "preview": "const fsSync = require('fs');\nconst fs = fsSync.promises;\nconst path = require('path');\nconst archiver = require('archiv"
  },
  {
    "path": "scripts/npmpublish.js",
    "chars": 1546,
    "preview": "const fs = require('fs').promises;\nconst path = require('path');\n\nmodule.exports = async ({ github, context, core, exec "
  },
  {
    "path": "scripts/teststabilitytester.js",
    "chars": 1362,
    "preview": "// This repeatedly runs the click and unit tests to verify their stability\n\nvar childProcess = require('child_process');"
  },
  {
    "path": "source/address-parser.js",
    "chars": 2259,
    "preview": "'use strict';\n\n// USED BY FRONT END\n// DO NOT GO ES6\nconst addressWindowsLocalRegexp = /[a-zA-Z]:\\\\([^\\\\]+\\\\?)*/;\nconst "
  },
  {
    "path": "source/bugtracker.js",
    "chars": 971,
    "preview": "'use strict';\n\nconst logger = require('./utils/logger');\nconst sysinfo = require('./sysinfo');\nconst config = require('."
  },
  {
    "path": "source/config.js",
    "chars": 14750,
    "preview": "'use strict';\n\nconst rc = require('rc');\nconst path = require('path');\nconst fs = require('fs');\nconst process = require"
  },
  {
    "path": "source/git-api.js",
    "chars": 37135,
    "preview": "const path = require('path');\nconst temp = require('temp');\nconst gitParser = require('./git-parser');\nconst logger = re"
  },
  {
    "path": "source/git-parser.js",
    "chars": 13053,
    "preview": "const path = require('path');\nconst fileType = require('./utils/file-type.js');\n\nexports.parseGitStatus = (text) => {\n  "
  },
  {
    "path": "source/git-promise.js",
    "chars": 21772,
    "preview": "const child_process = require('child_process');\nconst gitParser = require('./git-parser');\nconst path = require('path');"
  },
  {
    "path": "source/server.js",
    "chars": 11853,
    "preview": "const logger = require('./utils/logger');\nconst config = require('./config');\nconst BugTracker = require('./bugtracker')"
  },
  {
    "path": "source/sysinfo.js",
    "chars": 1156,
    "preview": "const getMac = require('getmac').default;\nconst md5 = require('blueimp-md5');\nconst semver = require('semver');\nconst lo"
  },
  {
    "path": "source/ungit-plugin.js",
    "chars": 3632,
    "preview": "const fsSync = require('fs');\nconst fs = fsSync.promises;\nconst path = require('path');\nconst express = require('express"
  },
  {
    "path": "source/utils/cache.js",
    "chars": 2680,
    "preview": "const NodeCache = require('node-cache');\nconst md5 = require('blueimp-md5');\nconst funcMap = {}; // Will there ever be a"
  },
  {
    "path": "source/utils/file-type.js",
    "chars": 241,
    "preview": "'use strict';\n\nconst path = require('path');\nconst imageFileExtensions = ['.PNG', '.JPG', '.BMP', '.GIF', '.JPEG'];\n\nmod"
  },
  {
    "path": "source/utils/logger.js",
    "chars": 1139,
    "preview": "const winston = require('winston');\nconst path = require('path');\nconst config = require('../config');\n\nconst transports"
  },
  {
    "path": "test/common-es6.js",
    "chars": 2164,
    "preview": "const expect = require('expect.js');\nconst path = require('path');\nconst restGit = require('../source/git-api');\n\nexport"
  },
  {
    "path": "test/spec.address-parser.js",
    "chars": 4612,
    "preview": "const expect = require('expect.js');\nconst addressParser = require('../source/address-parser');\n\ndescribe('git-parser ad"
  },
  {
    "path": "test/spec.cache.js",
    "chars": 3326,
    "preview": "const expect = require('expect.js');\nconst cache = require('../source/utils/cache');\n\ndescribe('cache', () => {\n  it('sh"
  },
  {
    "path": "test/spec.credentials-helper.js",
    "chars": 1441,
    "preview": "const expect = require('expect.js');\nconst child_process = require('child_process');\nconst http = require('http');\nconst"
  },
  {
    "path": "test/spec.file-type.js",
    "chars": 673,
    "preview": "const fileType = require('../source/utils/file-type.js');\nconst expect = require('expect.js');\n\ndescribe('file type', ()"
  },
  {
    "path": "test/spec.git-api.branching.js",
    "chars": 6480,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.conflict-no-auto-stash.js",
    "chars": 2077,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.conflict.js",
    "chars": 11466,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.diff.js",
    "chars": 7818,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.discardchanges.js",
    "chars": 5826,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.ignorefile.js",
    "chars": 3365,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst fs "
  },
  {
    "path": "test/spec.git-api.js",
    "chars": 14514,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst fs "
  },
  {
    "path": "test/spec.git-api.patch.js",
    "chars": 11622,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.remote.js",
    "chars": 8562,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.squash.js",
    "chars": 2789,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.stash.js",
    "chars": 1250,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-api.submodule.js",
    "chars": 3508,
    "preview": "const expect = require('expect.js');\nconst request = require('supertest');\nconst express = require('express');\nconst pat"
  },
  {
    "path": "test/spec.git-parser.js",
    "chars": 33082,
    "preview": "const expect = require('expect.js');\nconst path = require('path');\nconst gitParser = require('../source/git-parser');\nco"
  },
  {
    "path": "tsconfig.json",
    "chars": 940,
    "preview": "{\n  \"compilerOptions\": {\n    \"forceConsistentCasingInFileNames\": false,\n    \"allowSyntheticDefaultImports\": true,\n    \"r"
  }
]

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

About this extraction

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

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

Copied to clipboard!