diff --git a/.eslintignore b/.eslintignore
index 93de4b10dfee9ebf31933d514ae3cb66e034ad32..b4bfa5a1f7ac085334e906974b5826169c815ee1 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1,5 +1,6 @@
 /coverage/
 /coverage-javascript/
+/node_modules/
 /public/
 /tmp/
 /vendor/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e522d47d19d00e5a8cae88f47ce5629c2495d233..b256e8a2a5f12efb2c3e1394b515bfc67b6a3651 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,6 +15,7 @@ variables:
   USE_BUNDLE_INSTALL: "true"
   GIT_DEPTH: "20"
   PHANTOMJS_VERSION: "2.1.1"
+  GET_SOURCES_ATTEMPTS: "3"
 
 before_script:
   - source ./scripts/prepare_build.sh
diff --git a/.rubocop.yml b/.rubocop.yml
index 13df3f996139ac85da048dd76d0736a04494154f..80eb4a5c19ea9a7983daea50ca4c0efa22b3f375 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -292,7 +292,8 @@ Style/MultilineMethodDefinitionBraceLayout:
 
 # Checks indentation of binary operations that span more than one line.
 Style/MultilineOperationIndentation:
-  Enabled: false
+  Enabled: true
+  EnforcedStyle: indented
 
 # Avoid multi-line `? :` (the ternary operator), use if/unless instead.
 Style/MultilineTernaryOperator:
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index c4e41f94594c72f3175d1c465d1f360697ba1386..ee74734aa2258df77aa09402d55798a1e2e55212 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-4.0.3
+4.1.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 524cb55242b53f6a64cc646ea05db6acc7696d2d..26aaba0e86632e4d537006e45b0ec918d780b3b4 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-1.1.1
+1.2.0
diff --git a/Gemfile b/Gemfile
index 2cc7764e6b8f048d3338790e3611e3bb14994ae6..bea31b53b1c721eb80dc7f45f6dc34b058f4a6c3 100644
--- a/Gemfile
+++ b/Gemfile
@@ -22,7 +22,6 @@ gem 'doorkeeper',             '~> 4.2.0'
 gem 'omniauth',               '~> 1.3.1'
 gem 'omniauth-auth0',         '~> 1.4.1'
 gem 'omniauth-azure-oauth2',  '~> 0.0.6'
-gem 'omniauth-bitbucket',     '~> 0.0.2'
 gem 'omniauth-cas3',          '~> 1.1.2'
 gem 'omniauth-facebook',      '~> 4.0.0'
 gem 'omniauth-github',        '~> 1.1.1'
@@ -67,7 +66,7 @@ gem 'gollum-rugged_adapter', '~> 0.4.2', require: false
 gem 'github-linguist', '~> 4.7.0', require: 'linguist'
 
 # API
-gem 'grape',        '~> 0.15.0'
+gem 'grape',        '~> 0.18.0'
 gem 'grape-entity', '~> 0.6.0'
 gem 'rack-cors',    '~> 0.4.0', require: 'rack/cors'
 
@@ -178,6 +177,9 @@ gem 'asana', '~> 0.4.0'
 # FogBugz integration
 gem 'ruby-fogbugz', '~> 0.2.1'
 
+# Kubernetes integration
+gem 'kubeclient', '~> 2.2.0'
+
 # d3
 gem 'd3_rails', '~> 3.5.0'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 3de1a7cbf262178febf56fcfbfaee170cd7cd9a3..811adfc5c1d48a2e1756ed15337cfbdae27b3491 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -161,6 +161,8 @@ GEM
     diff-lcs (1.2.5)
     diffy (3.1.0)
     docile (1.1.5)
+    domain_name (0.5.20161021)
+      unf (>= 0.0.5, < 1.0.0)
     doorkeeper (4.2.0)
       railties (>= 4.2)
     dropzonejs-rails (0.7.2)
@@ -282,15 +284,15 @@ GEM
       json
       multi_json
       request_store (>= 1.0)
-    grape (0.15.0)
+    grape (0.18.0)
       activesupport
       builder
       hashie (>= 2.1.0)
       multi_json (>= 1.3.2)
       multi_xml (>= 0.5.2)
+      mustermann-grape (~> 0.4.0)
       rack (>= 1.3.0)
       rack-accept
-      rack-mount
       virtus (>= 1.0.0)
     grape-entity (0.6.0)
       activesupport
@@ -318,6 +320,15 @@ GEM
     html2text (0.2.0)
       nokogiri (~> 1.6)
     htmlentities (4.3.4)
+    http (0.9.8)
+      addressable (~> 2.3)
+      http-cookie (~> 1.0)
+      http-form_data (~> 1.0.1)
+      http_parser.rb (~> 0.6.0)
+    http-cookie (1.0.3)
+      domain_name (~> 0.5)
+    http-form_data (1.0.1)
+    http_parser.rb (0.6.0)
     httparty (0.13.7)
       json (~> 1.8)
       multi_xml (>= 0.5.2)
@@ -352,6 +363,10 @@ GEM
     knapsack (1.11.0)
       rake
       timecop (>= 0.1.0)
+    kubeclient (2.2.0)
+      http (= 0.9.8)
+      recursive-open-struct (= 1.0.0)
+      rest-client
     launchy (2.4.3)
       addressable (~> 2.3)
     letter_opener (1.4.1)
@@ -385,9 +400,14 @@ GEM
     multi_json (1.12.1)
     multi_xml (0.5.5)
     multipart-post (2.0.0)
+    mustermann (0.4.0)
+      tool (~> 0.2)
+    mustermann-grape (0.4.0)
+      mustermann (= 0.4.0)
     mysql2 (0.3.20)
     net-ldap (0.12.1)
     net-ssh (3.0.1)
+    netrc (0.11.0)
     newrelic_rpm (3.16.0.318)
     nokogiri (1.6.8)
       mini_portile2 (~> 2.1.0)
@@ -412,10 +432,6 @@ GEM
       jwt (~> 1.0)
       omniauth (~> 1.0)
       omniauth-oauth2 (~> 1.1)
-    omniauth-bitbucket (0.0.2)
-      multi_json (~> 1.7)
-      omniauth (~> 1.1)
-      omniauth-oauth (~> 1.0)
     omniauth-cas3 (1.1.3)
       addressable (~> 2.3)
       nokogiri (~> 1.6.6)
@@ -489,14 +505,12 @@ GEM
     pry-rails (0.3.4)
       pry (>= 0.9.10)
     pyu-ruby-sasl (0.0.3.3)
-    rack (1.6.4)
+    rack (1.6.5)
     rack-accept (0.4.5)
       rack (>= 0.4)
     rack-attack (4.4.1)
       rack
     rack-cors (0.4.0)
-    rack-mount (0.8.3)
-      rack (>= 1.0.0)
     rack-oauth2 (1.2.3)
       activesupport (>= 2.3)
       attr_required (>= 0.0.5)
@@ -543,6 +557,7 @@ GEM
       json (~> 1.4)
     recaptcha (3.0.0)
       json
+    recursive-open-struct (1.0.0)
     redcarpet (3.3.3)
     redis (3.2.2)
     redis-actionpack (5.0.1)
@@ -568,6 +583,10 @@ GEM
       listen (~> 3.0)
     responders (2.3.0)
       railties (>= 4.2.0, < 5.1)
+    rest-client (2.0.0)
+      http-cookie (>= 1.0.2, < 2.0)
+      mime-types (>= 1.16, < 4.0)
+      netrc (~> 0.8)
     rinku (2.0.0)
     rotp (2.1.2)
     rouge (2.0.7)
@@ -722,6 +741,7 @@ GEM
     tilt (2.0.5)
     timecop (0.8.1)
     timfel-krb5-auth (0.8.3)
+    tool (0.2.3)
     truncato (0.7.8)
       htmlentities (~> 4.3.1)
       nokogiri (~> 1.6.1)
@@ -840,7 +860,7 @@ DEPENDENCIES
   gollum-lib (~> 4.2)
   gollum-rugged_adapter (~> 0.4.2)
   gon (~> 6.1.0)
-  grape (~> 0.15.0)
+  grape (~> 0.18.0)
   grape-entity (~> 0.6.0)
   haml_lint (~> 0.18.2)
   hamlit (~> 2.6.1)
@@ -859,6 +879,7 @@ DEPENDENCIES
   jwt
   kaminari (~> 0.17.0)
   knapsack (~> 1.11.0)
+  kubeclient (~> 2.2.0)
   letter_opener_web (~> 1.3.0)
   license_finder (~> 2.1.0)
   licensee (~> 8.0.0)
@@ -877,7 +898,6 @@ DEPENDENCIES
   omniauth (~> 1.3.1)
   omniauth-auth0 (~> 1.4.1)
   omniauth-azure-oauth2 (~> 0.0.6)
-  omniauth-bitbucket (~> 0.0.2)
   omniauth-cas3 (~> 1.1.2)
   omniauth-facebook (~> 4.0.0)
   omniauth-github (~> 1.1.1)
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
index 82e526ae0ef44af8b276eddc133c7aa350e4cff7..8a260aae1b15de8ae6c4ae993a41a8025562f824 100644
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign */
+
 ((global) => {
   const MAX_MESSAGE_LENGTH = 500;
   const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index 31852e4750c1475d3e574cb66dba818f52f31b37..5a7d823e84c8a1d766634d55f999389dd4fb460b 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-arrow-callback, camelcase, quotes, comma-dangle, padded-blocks, max-len */
+/* global Turbolinks */
+
 (function() {
   this.Admin = (function() {
     function Admin() {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1c625e2f2b10aa50aecb5eb51ca8fa7a25baadde..f60f27d1210937aa5a3c405b839b80dfe9dfc2ef 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, no-undef, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, quotes, object-shorthand, camelcase, no-var, comma-dangle, prefer-arrow-callback, indent, object-curly-spacing, quote-props, no-param-reassign, padded-blocks, max-len */
+
 (function() {
-  this.Api = {
+  var Api = {
     groupsPath: "/api/:version/groups.json",
     groupPath: "/api/:version/groups/:id.json",
     namespacesPath: "/api/:version/namespaces.json",
@@ -10,6 +11,7 @@
     licensePath: "/api/:version/templates/licenses/:key",
     gitignorePath: "/api/:version/templates/gitignores/:key",
     gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
+    dockerfilePath: "/api/:version/dockerfiles/:key",
     issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
     group: function(group_id, callback) {
       var url = Api.buildUrl(Api.groupPath)
@@ -119,6 +121,10 @@
         return callback(file);
       });
     },
+    dockerfileYml: function(key, callback) {
+      var url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
+      $.get(url, callback);
+    },
     issueTemplate: function(namespacePath, projectPath, key, type, callback) {
       var url = Api.buildUrl(Api.issuableTemplatePath)
         .replace(':key', key)
@@ -140,4 +146,5 @@
     }
   };
 
+  window.Api = Api;
 }).call(this);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index b7c4673c8e39f8a8dacc2a6e88ab15e0c71e9634..043c6a11c4fcfc652c525c63dbb66e8ab4e15e57 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,4 +1,11 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, no-undef, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, quotes, consistent-return, prefer-arrow-callback, comma-dangle, object-shorthand, no-new, max-len */
+/* global bp */
+/* global Cookies */
+/* global Flash */
+/* global ConfirmDangerModal */
+/* global AwardsHandler */
+/* global Aside */
+
 // This is a manifest file that'll be compiled into including all the files listed below.
 // Add new JavaScript code in separate files in this directory and they'll automatically
 // be included in the compiled file accessible from http://example.com/assets/application.js
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index f4302e2e9f64cd64c3602917b64b0a8938c881d6..107a7978a87d462ec7e59befd7254a0ef0410b86 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-undef, no-plusplus, no-return-assign, camelcase, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, no-var, spaced-comment, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, no-unused-vars, no-else-return, prefer-template, quotes, comma-dangle, no-param-reassign, no-void, radix, keyword-spacing, space-before-blocks, brace-style, no-underscore-dangle, no-plusplus, no-return-assign, camelcase, padded-blocks */
+/* global Cookies */
+
 (function() {
   this.AwardsHandler = (function() {
     var FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index a5d62f881fea0c6a683359eb937fd20e96bfa355..c62a4c5a45658c17dab95ef88124de98635f8674 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, consistent-return, padded-blocks, max-len */
+/* global autosize */
 
 /*= require jquery.ba-resize */
 /*= require autosize */
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 4edcaa15fe5289a554bd3d8f18668f938e59fddf..586f941a6e35f6cf2734f5025462db5a8dfaabfd 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, camelcase, max-len, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, camelcase, consistent-return, quotes, object-shorthand, comma-dangle, padded-blocks, max-len */
+
 // Quick Submit behavior
 //
 // When a child field of a form with a `js-quick-submit` class receives a
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index 37531aaec9b49c89e0b9586e1330660567a829ea..57bd13eecf8aeaa07ad17e8f358790d9c5f895f1 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Api */
+
 /*= require blob/template_selector */
 ((global) => {
 
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..bdf9501761366aaf3c297a2b5f68059ca4abcf16
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_dockerfile_selector.js.es6
@@ -0,0 +1,18 @@
+/* global Api */
+/*= require blob/template_selector */
+
+(() => {
+  const global = window.gl || (window.gl = {});
+
+  class BlobDockerfileSelector extends gl.TemplateSelector {
+    requestFile(query) {
+      return Api.dockerfileYml(query.name, this.requestFileSuccess.bind(this));
+    }
+
+    requestFileSuccess(file) {
+      return super.requestFileSuccess(file);
+    }
+  }
+
+  global.BlobDockerfileSelector = BlobDockerfileSelector;
+})();
diff --git a/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6 b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..9cee79fa5d56756846ab5c01cbe46f33d1e887f7
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_dockerfile_selectors.js.es6
@@ -0,0 +1,27 @@
+(() => {
+  const global = window.gl || (window.gl = {});
+
+  class BlobDockerfileSelectors {
+    constructor({ editor, $dropdowns } = {}) {
+      this.editor = editor;
+      this.$dropdowns = $dropdowns || $('.js-dockerfile-selector');
+      this.initSelectors();
+    }
+
+    initSelectors() {
+      const editor = this.editor;
+      this.$dropdowns.each((i, dropdown) => {
+        const $dropdown = $(dropdown);
+        return new gl.BlobDockerfileSelector({
+          editor,
+          pattern: /(Dockerfile)/,
+          data: $dropdown.data('data'),
+          wrapper: $dropdown.closest('.js-dockerfile-selector-wrap'),
+          dropdown: $dropdown,
+        });
+      });
+    }
+  }
+
+  global.BlobDockerfileSelectors = BlobDockerfileSelectors;
+})();
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index e0a2e8ac12e91227b396afd5be9e93cfcd9fb235..eab686c45c3e99ead7b063106403d46ad3cd0a89 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, no-undef, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, camelcase, object-shorthand, quotes, comma-dangle, prefer-arrow-callback, no-unused-vars, prefer-template, no-useless-escape, no-alert, padded-blocks, max-len */
+/* global Dropzone */
+
 (function() {
   this.BlobFileDropzone = (function() {
     function BlobFileDropzone(form, method) {
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 7e8f1062ab3ad11e9d73b5947c3419838911620f..15563e429a0f65cbe713e6e2ac4e270f8549dabd 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, padded-blocks */
+/* global Api */
 
 /*= require blob/template_selector */
 
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 9a694daa010d90509cd989efcbf55b2100e1ad33..d7f950936881d4e973ea621b80042501a28db30e 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, no-undef, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-unused-expressions, no-cond-assign, no-sequences, comma-dangle, padded-blocks, max-len */
+/* global BlobGitignoreSelector */
+
 (function() {
   this.BlobGitignoreSelectors = (function() {
     function BlobGitignoreSelectors(opts) {
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 9a77fe35d55292c71622eb5b5ff272c605b02742..d9c6f65a0830394fe137d3d611fe01cb64704f9b 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-rest-params, comma-dangle, padded-blocks */
+/* global Api */
 
 /*= require blob/template_selector */
 
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
index adeb8ba1318500e3ced0e4f05d620fb7a8f2858b..268640681d42dc3bb0797875a524299906b622e9 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-param-reassign, padded-blocks */
+/* global BlobLicenseSelector */
+
 ((global) => {
   class BlobLicenseSelectors {
     constructor({ $dropdowns, editor }) {
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 0ff5c0fab057ab95438e20d4a753ece248edad19..7a1ee9998c8542fc9fd85759b4a6376f16f15ae3 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable indent, comma-dangle, object-shorthand, func-names, space-before-function-paren, arrow-parens, no-unused-vars, class-methods-use-this, no-var, consistent-return, prefer-const, no-param-reassign, space-in-parens, max-len */
+
 ((global) => {
     class TemplateSelector {
       constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index b8eb0f60a8eb1a50548f38455a5e12b9e55e4ff4..8c40e36a80a1a8750b30261483396564c026a8cf 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-undef, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, vars-on-top, no-unused-vars, no-new, padded-blocks, max-len */
+/* global EditBlob */
+/* global NewCommitForm */
+
 /*= require_tree . */
 
 (function() {
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 0c74aaaa8525da67113fd84dd387f07db12fe515..fa43ff611ccbb566abf20d26b16015a7d5502ebf 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, no-undef, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, no-param-reassign, quotes, prefer-template, no-new, comma-dangle, one-var, one-var-declaration-per-line, prefer-arrow-callback, no-else-return, no-unused-vars, padded-blocks, max-len */
+/* global ace */
+/* global BlobGitignoreSelectors */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -33,6 +36,9 @@
       new gl.BlobCiYamlSelectors({
         editor: this.editor
       });
+      new gl.BlobDockerfileSelectors({
+        editor: this.editor
+      });
     }
 
     EditBlob.prototype.initModePanesAndLinks = function() {
@@ -57,7 +63,7 @@
           content: this.editor.getValue()
         }, function(response) {
           currentPane.empty().append(response);
-          return currentPane.syntaxHighlight();
+          return currentPane.renderGFM();
         });
       } else {
         this.$toggleButton.show();
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 7ba918a05f89ef021f5021b01761e7acb34f7c56..ab2343c72fc3b058b3e7bb312b8422364d0b05c8 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable one-var, indent, quote-props, comma-dangle, space-before-function-paren */
+/* global Vue */
+/* global BoardService */
+
 //= require vue
 //= require vue-resource
 //= require Sortable
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 31de3b2528478bbcf7d50f05e725cff5b20256e7..d1fb0ec48e0fa516bf945d71aa8f3d6b65ec91e0 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, radix */
+/* global Vue */
+/* global Sortable */
+
 //= require ./board_blank_state
 //= require ./board_delete
 //= require ./board_list
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 691487b272ac8ed00df06b13ae3f6f86a5d55521..0a47a22fad27ff317d7b68f65637b90c381b0e29 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle, semi */
+/* global Vue */
+/* global ListLabel */
+
 (() => {
   const Store = gl.issueBoards.BoardsStore;
 
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 2299dafd21767f3daf081255810feecf255526ba..5fc5028081177b09657466ba57d341f3eb71114c 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, dot-notation */
+/* global Vue */
+
 (() => {
   const Store = gl.issueBoards.BoardsStore;
 
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
index c45e1926c5ca6f87bc7afff6175b6323824365ce..861600424a59aa7dddf4a73977cd84e46c12a740 100644
--- a/app/assets/javascripts/boards/components/board_delete.js.es6
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-alert */
+/* global Vue */
+
 (() => {
   window.gl = window.gl || {};
   window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 43ebeef39c4898abc57788be94920cd965d00a28..6711930622b18e6e109ae78a81f9736fd8163be0 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, max-len, no-plusplus */
+/* global Vue */
+/* global Sortable */
+
 //= require ./board_card
 //= require ./board_new_issue
 
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index a7989a2ff4c59cf1b8e3d48006b91970a46123d4..2386d3a613cee1e61008e37f170a63f569b9246d 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+/* global Vue */
+/* global ListIssue */
+
 (() => {
   const Store = gl.issueBoards.BoardsStore;
 
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
index 1644a7727378f369b57963d0233ec3c90e37c010..02459722bbfbc4cc669e465af4f1b31836216ef9 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js.es6
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, no-new */
+/* global Vue */
+/* global IssuableContext */
+/* global MilestoneSelect */
+/* global LabelsSelect */
+/* global Sidebar */
+
 (() => {
   const Store = gl.issueBoards.BoardsStore;
 
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 10ce746deb59b8b6d7116a48d42cae1e0ef5aca6..3f5cf8420a8593e88e637d08f2f5fc5986ea0509 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, indent */
+
 (() => {
   window.gl = window.gl || {};
   window.gl.issueBoards = window.gl.issueBoards || {};
@@ -45,10 +46,10 @@
 
           return $li.append($a.prepend($labelColor));
         },
-  			search: {
-  				fields: ['title']
-  			},
-  			filterable: true,
+        search: {
+          fields: ['title']
+        },
+        filterable: true,
         selectable: true,
         multiSelect: true,
         clicked (label, $el, e) {
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
index 9eceac4edddebefa9b8c30e02f57e74e9c8c0cd9..7e192e90fe64ab430662cf6b3915060a0067f96c 100644
--- a/app/assets/javascripts/boards/filters/due_date_filters.js.es6
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* global Vue */
+
 Vue.filter('due-date', (value) => {
   const date = new Date(value);
   return $.datepicker.formatDate('M d, yy', date);
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index 5f99de39122cdebe97d99740d079a88dd6906f2d..a5e62ed775dd3c8dc2c631630957093874678920 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, no-mixed-operators, prefer-const, comma-dangle, semi */
+/* global DocumentTouch */
+
 ((w) => {
   window.gl = window.gl || {};
   window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index 21d735e8231e6e5ec0c81b9d250f1b5bc68f13b4..1199e022ff180d26256be8f54650d21196e48046 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, space-before-function-paren, arrow-body-style, space-in-parens, arrow-parens, comma-dangle, max-len */
+/* global Vue */
+/* global ListLabel */
+/* global ListMilestone */
+/* global ListUser */
+
 class ListIssue {
   constructor (obj) {
     this.id = obj.iid;
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index 0910fe9a8540800646b377c372bc5dc5cb5e16aa..8f20a1bbec750b16888743023a410df917843487 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars, space-before-function-paren */
+
 class ListLabel {
   constructor (obj) {
     this.id = obj.id;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 429bd27c3fbd3ae19561e087bdea6fe75b09c167..a8d38c164856b6e3d4b5a10d8fecbfdc0e4dffc6 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, no-underscore-dangle, class-methods-use-this, consistent-return, no-plusplus, prefer-const, space-in-parens, no-shadow, no-param-reassign, max-len, no-unused-vars */
+/* global ListIssue */
+/* global ListLabel */
+
 class List {
   constructor (obj) {
     this.id = obj.id;
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
index a48969e19c93c5140c28ef90d4e119cfb17b90a8..9c173c1b70b630573e2037d96d50bcf8c44ecea1 100644
--- a/app/assets/javascripts/boards/models/milestone.js.es6
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
 class ListMilestone {
-  constructor (obj) {
+  constructor(obj) {
     this.id = obj.id;
     this.title = obj.title;
   }
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
index 583a973fc4676fe4dc523eed9aaff0cac0ecf175..a8a3892e2ad4b16dc831f1095c0556676e750af5 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-unused-vars */
+
 class ListUser {
-  constructor (user) {
+  constructor(user) {
     this.id = user.id;
     this.name = user.name;
     this.username = user.username;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index f59a2ed7937a2eda7f378fbfd3689e965c1e98d1..189a870319818465138ea109f9e3d37584424ded 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, comma-dangle, no-param-reassign, camelcase, prefer-const, no-extra-semi, max-len, no-unused-vars */
+/* global Vue */
+
 class BoardService {
   constructor (root, boardId) {
     this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index bb2a4de8b18b05306504b7172ce8d0ad1e21e73a..e7a14ea5bcaf3bf3cb9e723849cfd515fd6b40cf 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, space-before-function-paren, one-var, indent, space-in-parens, no-shadow, radix, dot-notation, semi, max-len */
+/* global Cookies */
+/* global List */
+
 (() => {
   window.gl = window.gl || {};
   window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index 80f137ca12ea87a7f9704e67db61f113faf14534..3723a2039f90c7379d6c113213ee82983d4b0e11 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, no-plusplus */
+/* global Vue */
+
 Vue.http.interceptors.push((request, next) => {
   Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
 
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index e7ceb602601e022f0b65706a023ed2a4c6dcfb22..a7e72430141920da250a0cc00d33c61abc2067dd 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, quotes, no-shadow, prefer-arrow-callback, prefer-template, consistent-return, padded-blocks, no-return-assign, new-parens, no-param-reassign, max-len */
+
 (function() {
-  this.Breakpoints = (function() {
+  var Breakpoints = (function() {
     var BreakpointInstance, instance;
 
     function Breakpoints() {}
@@ -68,4 +69,5 @@
     };
   })(this));
 
+  window.Breakpoints = Breakpoints;
 }).call(this);
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 116a47b090722e7f42acf63e3ec32445ef84990a..824febe3fd3d89eeb19529c3ab53b9c989ff968d 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, no-undef, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-param-reassign, quotes, yoda, no-else-return, consistent-return, comma-dangle, semi, object-shorthand, prefer-template, one-var, one-var-declaration-per-line, no-unused-vars, max-len, vars-on-top, padded-blocks */
+/* global Breakpoints */
+/* global Turbolinks */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -193,6 +196,7 @@
     };
 
     Build.prototype.stepTrace = function(e) {
+      var $currentTarget;
       e.preventDefault();
       $currentTarget = $(e.currentTarget);
       $.scrollTo($currentTarget.attr('href'), {
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
index 0ecd20bc11e10331a2755aae3e972a09f4c455de..993424d422f9a47fdefb5f4b93509ab33d6f7f16 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, prefer-arrow-callback, space-before-blocks, space-before-function-paren, comma-spacing, max-len */
+
 $(function(){
   $('.reveal-variables').off('click').on('click',function(){
     $('.js-build').toggle().niceScroll();
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 67509ea7d91c56cff1f4e445902ce60a6c452476..67b33a4d7ee01ede2fcc3f13c587d72450692d35 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, padded-blocks */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, padded-blocks */
+/* global CommitFile */
+
 (function() {
   this.Commit = (function() {
     function Commit() {
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index 600bac8834a0a71ded3a5a155421b6e57ed3436e..27512312c7cb79dc89751955ffc3b1a81239e245 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, padded-blocks */
+/* global ImageFile */
+
 (function() {
   this.CommitFile = (function() {
     function CommitFile(file) {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 3627aaf5080e624650ec3157b346f4210f4c0f9a..24a6e4ff0e977855a8df9ee4daadb45ddd7404fd 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-undef, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, padded-blocks, max-len, prefer-arrow-callback */
+/* global Pager */
+
 (function() {
   this.CommitsList = (function() {
     function CommitsList() {}
@@ -6,8 +8,8 @@
     CommitsList.timer = null;
 
     CommitsList.init = function(limit) {
-      $("body").on("click", ".day-commits-table li.commit", function(event) {
-        if (event.target.nodeName !== "A") {
+      $("body").on("click", ".day-commits-table li.commit", function(e) {
+        if (e.target.nodeName !== "A") {
           location.href = $(this).attr("url");
           e.stopPropagation();
           return false;
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index bd980f87e7222e78f9e5e761ed250c56a7b3dd5d..45c974b2b68e7b4471653247bc4cc552d4b4475d 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, object-shorthand, comma-dangle, prefer-arrow-callback, no-else-return, newline-per-chained-call, no-dupe-keys, wrap-iife, padded-blocks, max-len */
+
 (function() {
   this.CompareAutocomplete = (function() {
     function CompareAutocomplete() {
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index efa228a75d90dd4824052ddd9e0a98b575583ff2..6a13f38588d1a95bbd1308aafd9085014e3a1b97 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-undef, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, prefer-arrow-callback, padded-blocks, max-len */
+/* global Clipboard */
 
 /*= require clipboard */
 
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 744aa0afa03e84aa9e0383c795c8c2e9410768dd..947c129d5b5be87b43369d5de4834162f6df32e6 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */
+/* global Api */
+
 (function (w) {
   class CreateLabelDropdown {
     constructor ($el, namespacePath, projectPath) {
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index 52e2846d2790a4a252246c8b4b5440ebe44a7dcf..c59d3996fab2dfcb5724a0452cd9d391ae70a7b0 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, quotes, no-lonely-if, semi, max-len */
+/* global Vue */
+/* global CommentsStore */
+
 (() => {
   const CommentAndResolveBtn = Vue.extend({
     props: {
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index 983e554b9c1b69853fcfe3ec6f67405dddb03c35..f47867fc3b077a142f12935be20d4b1ac01af812 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,6 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-else-return, guard-for-in, no-restricted-syntax, one-var, indent, space-before-function-paren, no-plusplus, no-lonely-if, no-continue, brace-style, max-len, quotes, semi */
+/* global Vue */
+/* global DiscussionMixins */
+/* global CommentsStore */
+
 (() => {
-  JumpToDiscussion = Vue.extend({
+  const JumpToDiscussion = Vue.extend({
     mixins: [DiscussionMixins],
     props: {
       discussionId: String
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index 27af9fc96ad2817b490f1568a2d95e534b013949..88a19fc6e1df6307bb478f6f0a574dd1640c10a8 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,4 +1,9 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, quote-props, no-else-return, camelcase, no-new, max-len */
+/* global Vue */
+/* global CommentsStore */
+/* global ResolveService */
+/* global Flash */
+
 (() => {
   const ResolveBtn = Vue.extend({
     props: {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
index 9522ccb49da735a9f0eeeec20e28e826c7810b4a..72cdae812bc47763629894500b61ba19b736f957 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, func-names, no-param-reassign */
+/* global Vue */
+/* global DiscussionMixins */
+/* global CommentsStore */
+
 ((w) => {
   w.ResolveCount = Vue.extend({
     mixins: [DiscussionMixins],
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index b945a09fcbe2e94a87cc4c7706e341c24a3648f6..ee5f62b2d9e2545d9a34eea2fc39582b9eefa52d 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, space-before-function-paren, comma-dangle, no-else-return, quotes, max-len */
+/* global Vue */
+/* global CommentsStore */
+/* global ResolveService */
+
 (() => {
   const ResolveDiscussionBtn = Vue.extend({
     props: {
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index bd4c20aed8badb741ace94011c604bcd46e827c3..840b5aa922ec54fe1d520a78ef84a47babf1fab3 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, comma-dangle, new-cap, no-new */
+/* global Vue */
+/* global ResolveCount */
+
 //= require vue
 //= require vue-resource
 //= require_directory ./models
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
index 7a929017f36a00e706c98262a52273d1b95a3f5c..a9ea18bf82b870031604513a42ba6053edaa7292 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, guard-for-in, no-restricted-syntax, comma-dangle, no-plusplus, no-param-reassign, max-len */
+
 ((w) => {
   w.DiscussionMixins = {
     computed: {
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
index badcdccc840138200076c8e561e6620616c534a4..efcd46680a7027e15e0ba6ccb80573b9242cfe83 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, camelcase, guard-for-in, no-restricted-syntax, no-unused-vars, max-len */
+/* global Vue */
+/* global NoteModel */
+
 class DiscussionModel {
   constructor (discussionId) {
     this.id = discussionId;
@@ -69,7 +72,7 @@ class DiscussionModel {
 
       gl.utils.localTimeAgo($('.js-timeago', `${discussionSelector}`));
     } else {
-       $discussionHeadline.remove();
+      $discussionHeadline.remove();
     }
   }
 
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
index d0541b0263278efe32e105b30d6312b26148f770..e3bce1d2038e8895e374a3ceb4085967ac002b8e 100644
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -1,6 +1,7 @@
-/* eslint-disable */
+/* eslint-disable camelcase, no-unused-vars */
+
 class NoteModel {
-  constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
+  constructor(discussionId, noteId, canResolve, resolved, resolved_by) {
     this.discussionId = discussionId;
     this.id = noteId;
     this.canResolve = canResolve;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index 86953ce7ffb2df158bfc262d4c3f83a284b16861..78c74985f78589dfd95e1bc66099e65005b0e7f2 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this, one-var, indent, camelcase, no-new, comma-dangle, semi, no-param-reassign, max-len */
+/* global Vue */
+/* global Flash */
+/* global CommentsStore */
+
 ((w) => {
   class ResolveServiceClass {
     constructor() {
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
index f42ca406bb1df2779b5a761043dcedd7e9495cbc..1a7abbc6f75ceb8ae1bbbcdd74a69a91676315ab 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable object-shorthand, func-names, camelcase, prefer-const, no-restricted-syntax, guard-for-in, comma-dangle, max-len, no-param-reassign */
+/* global Vue */
+/* global DiscussionModel */
+
 ((w) => {
   w.CommentsStore = {
     state: {},
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index 413117c22268317a4b4ba9fdff2832b55a061545..1e259a16f068701d8ef5116bec5584715fcc15cc 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,4 +1,42 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len, padded-blocks */
+/* global UsernameValidator */
+/* global ActiveTabMemoizer */
+/* global ShortcutsNavigation */
+/* global Build */
+/* global Issuable */
+/* global Issue */
+/* global ShortcutsIssuable */
+/* global ZenMode */
+/* global Milestone */
+/* global GLForm */
+/* global IssuableForm */
+/* global LabelsSelect */
+/* global MilestoneSelect */
+/* global MergedButtons */
+/* global Commit */
+/* global NotificationsForm */
+/* global TreeView */
+/* global NotificationsDropdown */
+/* global UsersSelect */
+/* global GroupAvatar */
+/* global LineHighlighter */
+/* global ShortcutsBlob */
+/* global ProjectFork */
+/* global BuildArtifacts */
+/* global GroupsSelect */
+/* global Search */
+/* global Admin */
+/* global NamespaceSelects */
+/* global ShortcutsDashboardNavigation */
+/* global Project */
+/* global ProjectAvatar */
+/* global CompareAutocomplete */
+/* global ProjectNew */
+/* global Star */
+/* global ProjectShow */
+/* global Labels */
+/* global Shortcuts */
+
 (function() {
   var Dispatcher;
 
@@ -36,7 +74,9 @@
         case 'projects:merge_requests:index':
         case 'projects:issues:index':
           Issuable.init();
-          new gl.IssuableBulkActions();
+          new gl.IssuableBulkActions({
+            prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
+          });
           shortcut_handler = new ShortcutsNavigation();
           break;
         case 'projects:issues:show':
@@ -106,10 +146,6 @@
           new ZenMode();
           new MergedButtons();
           break;
-        case 'projects:merge_requests:index':
-          shortcut_handler = new ShortcutsNavigation();
-          Issuable.init();
-          break;
         case 'dashboard:activity':
           new gl.Activities();
           break;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index e1e76bca6ad374f7403f798a9a0bb4905e06182c..56cb39be642d6f116f4b4bab9d5fe63231c1064f 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, no-undef, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, one-var, no-var, one-var-declaration-per-line, no-unused-vars, camelcase, quotes, no-useless-concat, prefer-template, quote-props, comma-dangle, object-shorthand, consistent-return, no-plusplus, prefer-arrow-callback, padded-blocks */
+/* global Dropzone */
 
 /*= require preview_markdown */
 
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index e84f5ac9183f8db1807730b7e330deb01e30c964..2b7d57d86c6b534374860804845f4621f4719278 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, comma-dangle, prefer-template, consistent-return, class-methods-use-this, arrow-body-style, prefer-const, padded-blocks, no-unused-vars, no-underscore-dangle, no-new, max-len, semi, no-sequences, no-unused-expressions, no-param-reassign */
+
 (function(global) {
   class DueDateSelect {
     constructor({ $dropdown, $loading } = {}) {
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 1db29dd47fb7bde5d496c8c9c058daeb522d5d59..88c3d257ceaae682f07c7a3e4028744fdc085b1d 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -164,8 +164,7 @@
                   {{state.availableCounter}}
                 </span>
               </a>
-            </li>
-            <li v-bind:class="{ 'active' : scope === 'stopped' }">
+            </li><li v-bind:class="{ 'active' : scope === 'stopped' }">
               <a :href="projectStoppedEnvironmentsPath">
                 Stopped
                 <span class="badge js-stopped-environments-count">
diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6
index 2e046a601462cf19e6e7a447beeb81a3b5d9bb85..4674d5202e6833ad06a442546df6a5c1d325ccfa 100644
--- a/app/assets/javascripts/environments/components/environment_item.js.es6
+++ b/app/assets/javascripts/environments/components/environment_item.js.es6
@@ -449,7 +449,7 @@
           </span>
         </td>
 
-        <td>
+        <td class="environments-build-cell">
           <a v-if="shouldRenderBuildName"
             class="build-link"
             :href="model.last_deployment.deployable.build_path">
diff --git a/app/assets/javascripts/extensions/object.js.es6 b/app/assets/javascripts/extensions/object.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..70a2d765abd04cfdb9e140a1db86dda483c24e4d
--- /dev/null
+++ b/app/assets/javascripts/extensions/object.js.es6
@@ -0,0 +1,26 @@
+/* eslint-disable no-restricted-syntax */
+
+// Adapted from https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#Polyfill
+if (typeof Object.assign !== 'function') {
+  Object.assign = function assign(target, ...args) {
+    if (target == null) { // TypeError if undefined or null
+      throw new TypeError('Cannot convert undefined or null to object');
+    }
+
+    const to = Object(target);
+
+    for (let index = 0; index < args.length; index += 1) {
+      const nextSource = args[index];
+
+      if (nextSource != null) { // Skip over if undefined or null
+        for (const nextKey in nextSource) {
+          // Avoid bugs when hasOwnProperty is shadowed
+          if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
+            to[nextKey] = nextSource[nextKey];
+          }
+        }
+      }
+    }
+    return to;
+  };
+}
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 0122e84716138066f1e70ea6ecce0c1eb092ca2d..785f286997019c307fc5653dc67fdaa2c527f58f 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, max-len, one-var, one-var-declaration-per-line, quotes, prefer-template, newline-per-chained-call, comma-dangle, new-cap, no-else-return, padded-blocks, consistent-return */
+/* global FilesCommentButton */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 2f3da7451194cf472a89632fd3c34fa24271e6f8..17d03c87bf592b1fb9e1fbeafa0a5d282c01efab 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,19 +1,29 @@
-/* eslint-disable */
+/* eslint-disable func-names, space-before-function-paren, no-template-curly-in-string, comma-dangle, object-shorthand, quotes, dot-notation, no-else-return, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-param-reassign, no-useless-escape, prefer-template, consistent-return, wrap-iife, prefer-arrow-callback, camelcase, no-unused-vars, no-useless-return, padded-blocks, vars-on-top, indent, no-extra-semi, no-multi-spaces, semi, max-len */
+
 // Creates the variables for setting up GFM auto-completion
 (function() {
-  if (window.GitLab == null) {
-    window.GitLab = {};
+  if (window.gl == null) {
+    window.gl = {};
   }
 
   function sanitize(str) {
     return str.replace(/<(?:.|\n)*?>/gm, '');
   }
 
-  GitLab.GfmAutoComplete = {
-    dataLoading: false,
-    dataLoaded: false,
+  window.gl.GfmAutoComplete = {
+    dataSources: {},
+    defaultLoadingData: ['loading'],
     cachedData: {},
-    dataSource: '',
+    isLoadingData: {},
+    atTypeMap: {
+      ':': 'emojis',
+      '@': 'members',
+      '#': 'issues',
+      '!': 'mergeRequests',
+      '~': 'labels',
+      '%': 'milestones',
+      '/': 'commands'
+    },
     // Emoji
     Emoji: {
       template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
@@ -34,46 +44,45 @@
       template: '<li>${title}</li>'
     },
     Loading: {
-      template: '<li><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+      template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
     },
     DefaultOptions: {
       sorter: function(query, items, searchKey) {
-        // Highlight first item only if at least one char was typed
-        this.setting.highlightFirst = this.setting.alwaysHighlightFirst || query.length > 0;
-        if ((items[0].name != null) && items[0].name === 'loading') {
+        if (gl.GfmAutoComplete.isLoading(items)) {
           return items;
         }
         return $.fn.atwho["default"].callbacks.sorter(query, items, searchKey);
       },
       filter: function(query, data, searchKey) {
-        if (data[0] === 'loading') {
+        if (gl.GfmAutoComplete.isLoading(data)) {
+          gl.GfmAutoComplete.togglePreventSelection.call(this, true);
+          gl.GfmAutoComplete.fetchData(this.$inputor, this.at);
           return data;
+        } else {
+          gl.GfmAutoComplete.togglePreventSelection.call(this, false);
+          return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
         }
-        return $.fn.atwho["default"].callbacks.filter(query, data, searchKey);
       },
       beforeInsert: function(value) {
         if (value && !this.setting.skipSpecialCharacterTest) {
           var withoutAt = value.substring(1);
           if (withoutAt && /[^\w\d]/.test(withoutAt)) value = value.charAt() + '"' + withoutAt + '"';
         }
-        if (!GitLab.GfmAutoComplete.dataLoaded) {
-          return this.at;
-        } else {
-          return value;
-        }
+        return value;
       },
       matcher: function (flag, subtext) {
         // The below is taken from At.js source
         // Tweaked to commands to start without a space only if char before is a non-word character
         // https://github.com/ichord/At.js
-        var _a, _y, regexp, match;
+        var _a, _y, regexp, match, atSymbols;
+        atSymbols = Object.keys(this.app.controllers).join('|');
         subtext = subtext.split(' ').pop();
         flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
 
         _a = decodeURI("%C3%80");
         _y = decodeURI("%C3%BF");
 
-        regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?!\\W)([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)|([^\\x00-\\xff]*)$", 'gi');
+        regexp = new RegExp("(?:\\B|\\W|\\s)" + flag + "(?![" + atSymbols + "])([A-Za-z" + _a + "-" + _y + "0-9_\'\.\+\-]*)$", 'gi');
 
         match = regexp.exec(subtext);
 
@@ -84,69 +93,46 @@
         }
       }
     },
-    setup: _.debounce(function(input) {
+    setup: function(input) {
       // Add GFM auto-completion to all input fields, that accept GFM input.
       this.input = input || $('.js-gfm-input');
-      // destroy previous instances
-      this.destroyAtWho();
-      // set up instances
-      this.setupAtWho();
-
-      if (this.dataSource && !this.dataLoading && !this.cachedData) {
-        this.dataLoading = true;
-        return this.fetchData(this.dataSource)
-          .done((data) => {
-            this.dataLoading = false;
-            this.loadData(data);
-          });
-        };
-
-      if (this.cachedData != null) {
-        return this.loadData(this.cachedData);
-      }
-    }, 1000),
-    setupAtWho: function() {
+      this.setupLifecycle();
+    },
+    setupLifecycle() {
+      this.input.each((i, input) => {
+        const $input = $(input);
+        $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
+      });
+    },
+    setupAtWho: function($input) {
       // Emoji
-      this.input.atwho({
+      $input.atwho({
         at: ':',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.path != null) {
-              return _this.Emoji.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
+        displayTpl: function(value) {
+          return value.path != null ? this.Emoji.template : this.Loading.template;
+        }.bind(this),
         insertTpl: ':${name}:',
-        data: ['loading'],
         startWithSpace: false,
         skipSpecialCharacterTest: true,
+        data: this.defaultLoadingData,
         callbacks: {
           sorter: this.DefaultOptions.sorter,
-          filter: this.DefaultOptions.filter,
           beforeInsert: this.DefaultOptions.beforeInsert,
-          matcher: this.DefaultOptions.matcher
+          filter: this.DefaultOptions.filter
         }
       });
       // Team Members
-      this.input.atwho({
+      $input.atwho({
         at: '@',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.username != null) {
-              return _this.Members.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
+        displayTpl: function(value) {
+          return value.username != null ? this.Members.template : this.Loading.template;
+        }.bind(this),
         insertTpl: '${atwho-at}${username}',
         searchKey: 'search',
-        data: ['loading'],
         startWithSpace: false,
         alwaysHighlightFirst: true,
         skipSpecialCharacterTest: true,
+        data: this.defaultLoadingData,
         callbacks: {
           sorter: this.DefaultOptions.sorter,
           filter: this.DefaultOptions.filter,
@@ -177,20 +163,14 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '#',
         alias: 'issues',
         searchKey: 'search',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.title != null) {
-              return _this.Issues.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
-        data: ['loading'],
+        displayTpl: function(value) {
+          return value.title != null ? this.Issues.template : this.Loading.template;
+        }.bind(this),
+        data: this.defaultLoadingData,
         insertTpl: '${atwho-at}${id}',
         startWithSpace: false,
         callbacks: {
@@ -212,26 +192,21 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '%',
         alias: 'milestones',
         searchKey: 'search',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.title != null) {
-              return _this.Milestones.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
         insertTpl: '${atwho-at}${title}',
-        data: ['loading'],
+        displayTpl: function(value) {
+          return value.title != null ? this.Milestones.template : this.Loading.template;
+        }.bind(this),
         startWithSpace: false,
+        data: this.defaultLoadingData,
         callbacks: {
           matcher: this.DefaultOptions.matcher,
           sorter: this.DefaultOptions.sorter,
           beforeInsert: this.DefaultOptions.beforeInsert,
+          filter: this.DefaultOptions.filter,
           beforeSave: function(milestones) {
             return $.map(milestones, function(m) {
               if (m.title == null) {
@@ -246,21 +221,15 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '!',
         alias: 'mergerequests',
         searchKey: 'search',
-        displayTpl: (function(_this) {
-          return function(value) {
-            if (value.title != null) {
-              return _this.Issues.template;
-            } else {
-              return _this.Loading.template;
-            }
-          };
-        })(this),
-        data: ['loading'],
         startWithSpace: false,
+        displayTpl: function(value) {
+          return value.title != null ? this.Issues.template : this.Loading.template;
+        }.bind(this),
+        data: this.defaultLoadingData,
         insertTpl: '${atwho-at}${id}',
         callbacks: {
           sorter: this.DefaultOptions.sorter,
@@ -281,18 +250,31 @@
           }
         }
       });
-      this.input.atwho({
+      $input.atwho({
         at: '~',
         alias: 'labels',
         searchKey: 'search',
-        displayTpl: this.Labels.template,
+        data: this.defaultLoadingData,
+        displayTpl: function(value) {
+          return this.isLoading(value) ? this.Loading.template : this.Labels.template;
+        }.bind(this),
         insertTpl: '${atwho-at}${title}',
         startWithSpace: false,
         callbacks: {
           matcher: this.DefaultOptions.matcher,
           sorter: this.DefaultOptions.sorter,
           beforeInsert: this.DefaultOptions.beforeInsert,
+          filter: this.DefaultOptions.filter,
           beforeSave: function(merges) {
+            if (gl.GfmAutoComplete.isLoading(merges)) return merges;
+            var sanitizeLabelTitle;
+            sanitizeLabelTitle = function(title) {
+              if (/[\w\?&]+\s+[\w\?&]+/g.test(title)) {
+                return "\"" + (sanitize(title)) + "\"";
+              } else {
+                return sanitize(title);
+              }
+            };
             return $.map(merges, function(m) {
               return {
                 title: sanitize(m.title),
@@ -304,12 +286,14 @@
         }
       });
       // We don't instantiate the slash commands autocomplete for note and issue/MR edit forms
-      this.input.filter('[data-supports-slash-commands="true"]').atwho({
+      $input.filter('[data-supports-slash-commands="true"]').atwho({
         at: '/',
         alias: 'commands',
         searchKey: 'search',
         skipSpecialCharacterTest: true,
+        data: this.defaultLoadingData,
         displayTpl: function(value) {
+          if (this.isLoading(value)) return this.Loading.template;
           var tpl = '<li>/${name}';
           if (value.aliases.length > 0) {
             tpl += ' <small>(or /<%- aliases.join(", /") %>)</small>';
@@ -322,7 +306,7 @@
           }
           tpl += '</li>';
           return _.template(tpl)(value);
-        },
+        }.bind(this),
         insertTpl: function(value) {
           var tpl = "/${name} ";
           var reference_prefix = null;
@@ -340,6 +324,7 @@
           filter: this.DefaultOptions.filter,
           beforeInsert: this.DefaultOptions.beforeInsert,
           beforeSave: function(commands) {
+            if (gl.GfmAutoComplete.isLoading(commands)) return commands;
             return $.map(commands, function(c) {
               var search = c.name;
               if (c.aliases.length > 0) {
@@ -367,32 +352,40 @@
       });
       return;
     },
-    destroyAtWho: function() {
-      return this.input.atwho('destroy');
-    },
-    fetchData: function(dataSource) {
-      return $.getJSON(dataSource);
+    fetchData: function($input, at) {
+      if (this.isLoadingData[at]) return;
+      this.isLoadingData[at] = true;
+      if (this.cachedData[at]) {
+        this.loadData($input, at, this.cachedData[at]);
+      } else {
+        $.getJSON(this.dataSources[this.atTypeMap[at]], (data) => {
+          this.loadData($input, at, data);
+        }).fail(() => { this.isLoadingData[at] = false; });
+      }
     },
-    loadData: function(data) {
-      this.cachedData = data;
-      this.dataLoaded = true;
-      // load members
-      this.input.atwho('load', '@', data.members);
-      // load issues
-      this.input.atwho('load', 'issues', data.issues);
-      // load milestones
-      this.input.atwho('load', 'milestones', data.milestones);
-      // load merge requests
-      this.input.atwho('load', 'mergerequests', data.mergerequests);
-      // load emojis
-      this.input.atwho('load', ':', data.emojis);
-      // load labels
-      this.input.atwho('load', '~', data.labels);
-      // load commands
-      this.input.atwho('load', '/', data.commands);
+    loadData: function($input, at, data) {
+      this.isLoadingData[at] = false;
+      this.cachedData[at] = data;
+      $input.atwho('load', at, data);
       // This trigger at.js again
       // otherwise we would be stuck with loading until the user types
-      return $(':focus').trigger('keyup');
+      return $input.trigger('keyup');
+    },
+    isLoading(data) {
+      if (!data) return false;
+      if (Array.isArray(data)) data = data[0];
+      return data === this.defaultLoadingData[0] || data.name === this.defaultLoadingData[0];
+    },
+    togglePreventSelection(isPrevented = !!this.setting.tabSelectsMatch) {
+      this.setting.tabSelectsMatch = !isPrevented;
+      this.setting.spaceSelectsMatch = !isPrevented;
+      const eventListenerAction = `${isPrevented ? 'add' : 'remove'}EventListener`;
+      this.$inputor[0][eventListenerAction]('keydown', gl.GfmAutoComplete.preventSpaceTabEnter);
+    },
+    preventSpaceTabEnter(e) {
+      const key = e.which || e.keyCode;
+      const preventables = [9, 13, 32];
+      if (preventables.indexOf(key) > -1) e.preventDefault();
     }
   };
 
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 9a91018a8e4647bc9ad2fa1c04c3112ff1b67bb1..57dabfe05e4f0f8c5a8a9b8a338399b79a4cf4a3 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, no-undef, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, space-before-blocks, prefer-rest-params, max-len, vars-on-top, no-plusplus, wrap-iife, no-unused-vars, quotes, no-shadow, no-cond-assign, prefer-arrow-callback, semi, no-return-assign, no-else-return, camelcase, comma-dangle, no-lonely-if, guard-for-in, no-restricted-syntax, consistent-return, padded-blocks, prefer-template, no-param-reassign, no-loop-func, no-extra-semi, keyword-spacing, no-mixed-operators */
+/* global fuzzaldrinPlus */
+/* global Turbolinks */
+
 (function() {
   var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
     bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
@@ -20,7 +23,6 @@
       this.filterInputBlur = (ref = this.options.filterInputBlur) != null ? ref : true;
       $inputContainer = this.input.parent();
       $clearButton = $inputContainer.find('.js-dropdown-input-clear');
-      this.indeterminateIds = [];
       $clearButton.on('click', (function(_this) {
         // Clear click
         return function(e) {
@@ -188,7 +190,7 @@
   })();
 
   GitLabDropdown = (function() {
-    var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
+    var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, CURSOR_SELECT_SCROLL_PADDING, currentIndex;
 
     LOADING_CLASS = "is-loading";
 
@@ -345,12 +347,12 @@
           $el = $(this);
           selected = self.rowClicked($el);
           if (self.options.clicked) {
-            self.options.clicked(selected, $el, e);
+            self.options.clicked(selected[0], $el, e, selected[1]);
           }
 
           // Update label right after all modifications in dropdown has been done
           if (self.options.toggleLabel) {
-            self.updateLabel(selected, $el, self);
+            self.updateLabel(selected[0], $el, self);
           }
 
           $el.trigger('blur');
@@ -441,12 +443,6 @@
       this.resetRows();
       this.addArrowKeyEvent();
 
-      if (this.options.setIndeterminateIds) {
-        this.options.setIndeterminateIds.call(this);
-      }
-      if (this.options.setActiveIds) {
-        this.options.setActiveIds.call(this);
-      }
       // Makes indeterminate items effective
       if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
         this.parseData(this.fullData);
@@ -480,11 +476,6 @@
       if (this.options.filterable) {
         $input.blur().val("");
       }
-      // Triggering 'keyup' will re-render the dropdown which is not always required
-      // specially if we want to keep the state of the dropdown needed for bulk-assignment
-      if (!this.options.persistWhenHide) {
-        $input.trigger("input");
-      }
       if (this.dropdown.find(".dropdown-toggle-page").length) {
         $('.dropdown-menu', this.dropdown).removeClass(PAGE_TWO_CLASS);
       }
@@ -617,7 +608,8 @@
     };
 
     GitLabDropdown.prototype.rowClicked = function(el) {
-      var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
+      var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value, isMarking;
+
       fieldName = this.options.fieldName;
       isInput = $(this.el).is('input');
       if (this.renderedData) {
@@ -638,7 +630,7 @@
           el.addClass(ACTIVE_CLASS);
         }
 
-        return selectedObject;
+        return [selectedObject];
       }
 
       field = [];
@@ -656,6 +648,7 @@
       }
 
       if (el.hasClass(ACTIVE_CLASS)) {
+        isMarking = false;
         el.removeClass(ACTIVE_CLASS);
         if (field && field.length) {
           if (isInput) {
@@ -665,6 +658,7 @@
           }
         }
       } else if (el.hasClass(INDETERMINATE_CLASS)) {
+        isMarking = true;
         el.addClass(ACTIVE_CLASS);
         el.removeClass(INDETERMINATE_CLASS);
         if (field && field.length && value == null) {
@@ -674,6 +668,7 @@
           this.addInput(fieldName, value, selectedObject);
         }
       } else {
+        isMarking = true;
         if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
           this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
           if (!isInput) {
@@ -694,7 +689,7 @@
         }
       }
 
-      return selectedObject;
+      return [selectedObject, isMarking];
     };
 
     GitLabDropdown.prototype.focusTextInput = function() {
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 6ce392d2a5b54e117f87980da9025971d60ea46e..63f9cafa8d05cee108b59cdc1d87d360393b6768 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,4 +1,4 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, class-methods-use-this, max-len, space-before-function-paren, arrow-parens, no-param-reassign, padded-blocks */
 
 //= require gl_field_error
 
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index db5d9e75b3ab69f766b904502ed8f43e68ffc6c1..7dc2d13e5d8e8e83d0d6943c2d66c45ad03f9fba 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-undef, no-new, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-new, padded-blocks, max-len */
+/* global GitLab */
+/* global DropzoneInput */
+/* global autosize */
+
 (function() {
   this.GLForm = (function() {
     function GLForm(form) {
@@ -26,7 +30,7 @@
         this.form.addClass('gfm-form');
         // remove notify commit author checkbox for non-commit notes
         gl.utils.disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
-        GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
+        gl.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
         new DropzoneInput(this.form);
         autosize(this.textarea);
         // form and textarea event listeners
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index c3a132b3c7561e3a453c280a42964bf6345a438e..2d08a7c6ac3c2b3aa945bec30b93d1fb617a9512 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,4 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, no-undef, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, camelcase, one-var-declaration-per-line, quotes, no-param-reassign, quote-props, comma-dangle, prefer-template, max-len, no-return-assign, padded-blocks */
+/* global ContributorsGraph */
+/* global ContributorsAuthorGraph */
+/* global ContributorsMasterGraph */
+/* global ContributorsStatGraphUtil */
+/* global d3 */
 
 /*= require d3 */
 
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index cb2448e8cc7b326bcd39969091f82dec466d1427..9c5e9381e5221335dd645a8099f326418dded0e3 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, no-undef, newline-per-chained-call, no-else-return, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, padded-blocks, newline-per-chained-call, no-else-return */
+/* global d3 */
+/* global ContributorsGraph */
 
 /*= require d3 */
 
diff --git a/app/assets/javascripts/group_label_subscription.js.es6 b/app/assets/javascripts/group_label_subscription.js.es6
index eea6cd40859d897a5fe1d6356150cae460537356..8e10e4244126c004e7d762fef8643e12c07e95f5 100644
--- a/app/assets/javascripts/group_label_subscription.js.es6
+++ b/app/assets/javascripts/group_label_subscription.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable func-names, object-shorthand, comma-dangle, wrap-iife, space-before-function-paren, no-param-reassign, padded-blocks, max-len */
+
 (function(global) {
   class GroupLabelSubscription {
     constructor(container) {
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 3dc6f05ca2050500ef86b12bfece1b7b205c275a..99700e7562ade758bf7af868bca75423cf697ce7 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, wrap-iife, one-var, camelcase, one-var-declaration-per-line, quotes, object-shorthand, prefer-arrow-callback, comma-dangle, consistent-return, yoda, prefer-rest-params, prefer-spread, no-unused-vars, prefer-template, padded-blocks, max-len */
+/* global Api */
+
 (function() {
   var slice = [].slice;
 
@@ -14,7 +16,7 @@
             multiple: $(select).hasClass('multiselect'),
             minimumInputLength: 0,
             query: function(query) {
-              options = { all_available: all_available, skip_groups: skip_groups };
+              var options = { all_available: all_available, skip_groups: skip_groups };
               return Api.groups(query.term, options, function(groups) {
                 var data;
                 data = {
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 9425b6ed9d4a2d6c13e344e2662424ed3f12bdd9..fa795be07ed95b5d1c37f5fc923e92dab1c13e1a 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, camelcase, no-var, one-var, one-var-declaration-per-line, prefer-template, quotes, object-shorthand, comma-dangle, no-unused-vars, prefer-arrow-callback, no-else-return, padded-blocks, vars-on-top, no-new, max-len */
+
 (function() {
-  this.ImporterStatus = (function() {
+  window.ImporterStatus = (function() {
     function ImporterStatus(jobs_url, import_url) {
       this.jobs_url = jobs_url;
       this.import_url = import_url;
@@ -75,7 +76,7 @@
       var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
       var importPath = $('.js-importer-status').data('import-path');
 
-      new ImporterStatus(jobsImportPath, importPath);
+      new window.ImporterStatus(jobsImportPath, importPath);
     }
   });
 }).call(this);
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 46503c290aed9ca9b6e9843602905a181f5a4f63..1c10a7445bb0e4d543e4fa10cdc9668755d849e4 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable func-names, no-var, camelcase, no-unused-vars, object-shorthand, space-before-function-paren, no-return-assign, comma-dangle, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, prefer-arrow-callback, prefer-const, padded-blocks, wrap-iife, max-len */
+/* global Issuable */
+/* global Turbolinks */
+
 (function() {
   var issuable_created;
 
@@ -141,6 +144,9 @@
       const $issuesOtherFilters = $('.issues-other-filters');
       const $issuesBulkUpdate = $('.issues_bulk_update');
 
+      this.issuableBulkActions.willUpdateLabels = false;
+      this.issuableBulkActions.setOriginalDropdownData();
+
       if ($checkedIssues.length > 0) {
         let ids = $.map($checkedIssues, function(value) {
           return $(value).data('id');
@@ -152,7 +158,6 @@
         $updateIssuesIds.val([]);
         $issuesBulkUpdate.hide();
         $issuesOtherFilters.show();
-        this.issuableBulkActions.willUpdateLabels = false;
       }
       return true;
     },
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 317818951fd5053caa8d25378127e709250a9eb0..4aaad111082f516ecded68a7b0ac0e80ac24306d 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, no-undef, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-new, comma-dangle, quotes, prefer-arrow-callback, consistent-return, one-var, no-var, one-var-declaration-per-line, no-underscore-dangle, padded-blocks, max-len */
+/* global UsersSelect */
+
 (function() {
   this.IssuableContext = (function() {
     function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 50fdbc89c7c5cb66ade9f371e5500c19b5122bc9..1c4086517fea934c7d33f68595b09d1ae5a0c158 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,4 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-undef, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-useless-escape, no-new, quotes, object-shorthand, no-unused-vars, comma-dangle, radix, no-alert, consistent-return, no-else-return, prefer-template, one-var, one-var-declaration-per-line, curly, padded-blocks, max-len */
+/* global GitLab */
+/* global UsersSelect */
+/* global ZenMode */
+/* global Autosave */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
@@ -14,7 +19,7 @@
       this.renderWipExplanation = bind(this.renderWipExplanation, this);
       this.resetAutosave = bind(this.resetAutosave, this);
       this.handleSubmit = bind(this.handleSubmit, this);
-      GitLab.GfmAutoComplete.setup();
+      gl.GfmAutoComplete.setup();
       new UsersSelect();
       new ZenMode();
       this.titleField = this.form.find("input[name*='[title]']");
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 8540b199abaca7a682e1d83851cd55667e4d0259..63b70d4be17b8c76b22f40e79f432477751efcfb 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-undef, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, no-underscore-dangle, one-var-declaration-per-line, object-shorthand, no-unused-vars, no-new, comma-dangle, consistent-return, quotes, dot-notation, quote-props, prefer-arrow-callback, padded-blocks, max-len */
+/* global Flash */
 
 /*= require flash */
 /*= require jquery.waitforimages */
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 9697fb33566b3a534c9950a79a8764e4811287cc..52fd5d71b188b6c3a4b28f7f818cc39825d6a824 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,10 +1,14 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, quotes, consistent-return, func-names, array-callback-return, space-before-function-paren, prefer-arrow-callback, radix, max-len, padded-blocks, no-unused-expressions, no-sequences, no-underscore-dangle, no-unused-vars, no-param-reassign */
+/* global Issuable */
+/* global Flash */
+
 ((global) => {
 
   class IssuableBulkActions {
-    constructor({ container, form, issues } = {}) {
-      this.container = container || $('.content'),
+    constructor({ container, form, issues, prefixId } = {}) {
+      this.prefixId = prefixId || 'issue_';
       this.form = form || this.getElement('.bulk-update');
+      this.$labelDropdown = this.form.find('.js-label-select');
       this.issues = issues || this.getElement('.issues-list .issue');
       this.form.data('bulkActions', this);
       this.willUpdateLabels = false;
@@ -13,10 +17,6 @@
       Issuable.initChecks();
     }
 
-    getElement(selector) {
-      return this.container.find(selector);
-    }
-
     bindEvents() {
       return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
     }
@@ -70,10 +70,7 @@
 
     getUnmarkedIndeterminedLabels() {
       const result = [];
-      const labelsToKeep = [];
-
-      this.getElement('.labels-filter .is-indeterminate')
-        .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+      const labelsToKeep = this.$labelDropdown.data('indeterminate');
 
       this.getLabelsFromSelection().forEach((id) => {
         if (labelsToKeep.indexOf(id) === -1) {
@@ -103,45 +100,65 @@
         }
       };
       if (this.willUpdateLabels) {
-        this.getLabelsToApply().map(function(id) {
-          return formData.update.add_label_ids.push(id);
-        });
-        this.getLabelsToRemove().map(function(id) {
-          return formData.update.remove_label_ids.push(id);
-        });
+        formData.update.add_label_ids = this.$labelDropdown.data('marked');
+        formData.update.remove_label_ids = this.$labelDropdown.data('unmarked');
       }
       return formData;
     }
 
-    getLabelsToApply() {
+    setOriginalDropdownData() {
+      const $labelSelect = $('.bulk-update .js-label-select');
+      $labelSelect.data('common', this.getOriginalCommonIds());
+      $labelSelect.data('marked', this.getOriginalMarkedIds());
+      $labelSelect.data('indeterminate', this.getOriginalIndeterminateIds());
+    }
+
+    // From issuable's initial bulk selection
+    getOriginalCommonIds() {
       const labelIds = [];
-      const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
-      $labels.each(function(k, label) {
-        if (label) {
-          return labelIds.push(parseInt($(label).val()));
-        }
+
+      this.getElement('.selected_issue:checked').each((i, el) => {
+        labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
       });
-      return labelIds;
+      return _.intersection.apply(this, labelIds);
     }
 
+    // From issuable's initial bulk selection
+    getOriginalMarkedIds() {
+      const labelIds = [];
+      this.getElement('.selected_issue:checked').each((i, el) => {
+        labelIds.push(this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels'));
+      });
+      return _.intersection.apply(this, labelIds);
+    }
 
-    /**
-     * Returns Label IDs that will be removed from issue selection
-     * @return {Array} Array of labels IDs
-     */
-
-    getLabelsToRemove() {
-      const result = [];
-      const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
-      const labelsToApply = this.getLabelsToApply();
-      indeterminatedLabels.map(function(id) {
-        // We need to exclude label IDs that will be applied
-        // By not doing this will cause issues from selection to not add labels at all
-        if (labelsToApply.indexOf(id) === -1) {
-          return result.push(id);
-        }
+    // From issuable's initial bulk selection
+    getOriginalIndeterminateIds() {
+      const uniqueIds = [];
+      const labelIds = [];
+      let issuableLabels = [];
+
+      // Collect unique label IDs for all checked issues
+      this.getElement('.selected_issue:checked').each((i, el) => {
+        issuableLabels = this.getElement(`#${this.prefixId}${el.dataset.id}`).data('labels');
+        issuableLabels.forEach((labelId) => {
+          // Store unique IDs
+          if (uniqueIds.indexOf(labelId) === -1) {
+            uniqueIds.push(labelId);
+          }
+        });
+        // Store array of IDs per issuable
+        labelIds.push(issuableLabels);
       });
-      return result;
+      // Add uniqueIds to add it as argument for _.intersection
+      labelIds.unshift(uniqueIds);
+      // Return IDs that are present but not in all selected issueables
+      return _.difference(uniqueIds, _.intersection.apply(this, labelIds));
+    }
+
+    getElement(selector) {
+      this.scopeEl = this.scopeEl || $('.content');
+      return this.scopeEl.find(selector);
     }
   }
 
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index 175623e74483b0c1d4e710e4fba6642254575b1a..33c5e35324d856ea3b449cd619b34b32863c7029 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, padded-blocks, max-len */
+/* global Flash */
+
 ((global) => {
 
   class LabelManager {
@@ -104,4 +106,3 @@
   gl.LabelManager = LabelManager;
 
 })(window.gl || (window.gl = {}));
-
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index f334f35594da739e2fc476fd1d931b6c6657eb4e..ec2fc87bece939f9509e45a4162f00ee6da69a86 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,12 +1,16 @@
-/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, no-undef, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks, max-len */
+/* eslint-disable no-useless-return, func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, no-unused-vars, one-var-declaration-per-line, prefer-template, no-new, consistent-return, object-shorthand, comma-dangle, no-shadow, no-param-reassign, brace-style, vars-on-top, quotes, no-lonely-if, no-else-return, semi, dot-notation, no-empty, no-return-assign, camelcase, prefer-spread, padded-blocks */
+/* global Issuable */
+/* global ListLabel */
+
 (function() {
   this.LabelsSelect = (function() {
     function LabelsSelect() {
       var _this;
       _this = this;
       $('.js-label-select').each(function(i, dropdown) {
-        var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove;
+        var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
         $dropdown = $(dropdown);
+        $dropdownContainer = $dropdown.closest('.labels-filter');
         $toggleText = $dropdown.find('.dropdown-toggle-text');
         namespacePath = $dropdown.data('namespace-path');
         projectPath = $dropdown.data('project-path');
@@ -122,7 +126,7 @@
             });
           });
         };
-        return $dropdown.glDropdown({
+        $dropdown.glDropdown({
           showMenuAbove: showMenuAbove,
           data: function(term, callback) {
             return $.ajax({
@@ -169,33 +173,40 @@
             });
           },
           renderRow: function(label, instance) {
-            var $a, $li, active, color, colorEl, indeterminate, removesAll, selectedClass, spacing;
+            var $a, $li, color, colorEl, indeterminate, removesAll, selectedClass, spacing, i, marked, dropdownName, dropdownValue;
             $li = $('<li>');
             $a = $('<a href="#">');
             selectedClass = [];
             removesAll = label.id <= 0 || (label.id == null);
             if ($dropdown.hasClass('js-filter-bulk-update')) {
-              indeterminate = instance.indeterminateIds;
-              active = instance.activeIds;
+              indeterminate = $dropdown.data('indeterminate') || [];
+              marked = $dropdown.data('marked') || [];
+
               if (indeterminate.indexOf(label.id) !== -1) {
                 selectedClass.push('is-indeterminate');
               }
-              if (active.indexOf(label.id) !== -1) {
+
+              if (marked.indexOf(label.id) !== -1) {
                 // Remove is-indeterminate class if the item will be marked as active
                 i = selectedClass.indexOf('is-indeterminate');
                 if (i !== -1) {
                   selectedClass.splice(i, 1);
                 }
                 selectedClass.push('is-active');
-                // Add input manually
-                instance.addInput(this.fieldName, label.id);
               }
-            }
-            if (this.id(label) && $form.find("input[type='hidden'][name='" + ($dropdown.data('fieldName')) + "'][value='" + this.id(label).toString().replace(/'/g, '\\\'') + "']").length) {
-              selectedClass.push('is-active');
-            }
-            if ($dropdown.hasClass('js-multiselect') && removesAll) {
-              selectedClass.push('dropdown-clear-active');
+            } else {
+              if (this.id(label)) {
+                dropdownName = $dropdown.data('fieldName');
+                dropdownValue = this.id(label).toString().replace(/'/g, '\\\'');
+
+                if ($form.find("input[type='hidden'][name='" + dropdownName + "'][value='" + dropdownValue + "']").length) {
+                  selectedClass.push('is-active');
+                }
+              }
+
+              if ($dropdown.hasClass('js-multiselect') && removesAll) {
+                selectedClass.push('dropdown-clear-active');
+              }
             }
             if (label.duplicate) {
               spacing = 100 / label.color.length;
@@ -231,7 +242,6 @@
             // Return generated html
             return $li.html($a).prop('outerHTML');
           },
-          persistWhenHide: $dropdown.data('persistWhenHide'),
           search: {
             fields: ['title']
           },
@@ -310,18 +320,15 @@
                 }
               }
             }
-            if ($dropdown.hasClass('js-filter-bulk-update')) {
-              // If we are persisting state we need the classes
-              if (!this.options.persistWhenHide) {
-                return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
-              }
-            }
           },
           multiSelect: $dropdown.hasClass('js-multiselect'),
           vue: $dropdown.hasClass('js-issue-board-sidebar'),
-          clicked: function(label, $el, e) {
+          clicked: function(label, $el, e, isMarking) {
             var isIssueIndex, isMRIndex, page;
-            _this.enableBulkLabelDropdown();
+
+            page = $('body').data('page');
+            isIssueIndex = page === 'projects:issues:index';
+            isMRIndex = page === 'projects:merge_requests:index';
 
             if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
               $dropdown.parent()
@@ -330,12 +337,11 @@
             }
 
             if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+              _this.enableBulkLabelDropdown();
+              _this.setDropdownData($dropdown, isMarking, this.id(label));
               return;
             }
 
-            page = $('body').data('page');
-            isIssueIndex = page === 'projects:issues:index';
-            isMRIndex = page === 'projects:merge_requests:index';
             if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
               if (label.isAny) {
                 gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
@@ -397,17 +403,10 @@
               }
             }
           },
-          setIndeterminateIds: function() {
-            if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
-              return this.indeterminateIds = _this.getIndeterminateIds();
-            }
-          },
-          setActiveIds: function() {
-            if (this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
-              return this.activeIds = _this.getActiveIds();
-            }
-          }
         });
+
+        // Set dropdown data
+        _this.setOriginalDropdownData($dropdownContainer, $dropdown);
       });
       this.bindEvents();
     }
@@ -420,34 +419,9 @@
       if ($('.selected_issue:checked').length) {
         return;
       }
-      // Remove inputs
-      $('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
-      // Also restore button text
       return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
     };
 
-    LabelsSelect.prototype.getIndeterminateIds = function() {
-      var label_ids;
-      label_ids = [];
-      $('.selected_issue:checked').each(function(i, el) {
-        var issue_id;
-        issue_id = $(el).data('id');
-        return label_ids.push($("#issue_" + issue_id).data('labels'));
-      });
-      return _.flatten(label_ids);
-    };
-
-    LabelsSelect.prototype.getActiveIds = function() {
-      var label_ids;
-      label_ids = [];
-      $('.selected_issue:checked').each(function(i, el) {
-        var issue_id;
-        issue_id = $(el).data('id');
-        return label_ids.push($("#issue_" + issue_id).data('labels'));
-      });
-      return _.intersection.apply(_, label_ids);
-    };
-
     LabelsSelect.prototype.enableBulkLabelDropdown = function() {
       var issuableBulkActions;
       if ($('.selected_issue:checked').length) {
@@ -456,8 +430,59 @@
       }
     };
 
-    return LabelsSelect;
+    LabelsSelect.prototype.setDropdownData = function($dropdown, isMarking, value) {
+      var i, markedIds, unmarkedIds, indeterminateIds;
+      var issuableBulkActions = $('.bulk-update').data('bulkActions');
 
+      markedIds = $dropdown.data('marked') || [];
+      unmarkedIds = $dropdown.data('unmarked') || [];
+      indeterminateIds = $dropdown.data('indeterminate') || [];
+
+      if (isMarking) {
+        markedIds.push(value);
+
+        i = indeterminateIds.indexOf(value);
+        if (i > -1) {
+          indeterminateIds.splice(i, 1);
+        }
+
+        i = unmarkedIds.indexOf(value);
+        if (i > -1) {
+          unmarkedIds.splice(i, 1);
+        }
+      } else {
+        // If marked item (not common) is unmarked
+        i = markedIds.indexOf(value);
+        if (i > -1) {
+          markedIds.splice(i, 1);
+        }
+
+        // If an indeterminate item is being unmarked
+        if (issuableBulkActions.getOriginalIndeterminateIds().indexOf(value) > -1) {
+          unmarkedIds.push(value);
+        }
+
+        // If a marked item is being unmarked
+        // (a marked item could also be a label that is present in all selection)
+        if (issuableBulkActions.getOriginalCommonIds().indexOf(value) > -1) {
+          unmarkedIds.push(value);
+        }
+      }
+
+      $dropdown.data('marked', markedIds);
+      $dropdown.data('unmarked', unmarkedIds);
+      $dropdown.data('indeterminate', indeterminateIds);
+    };
+
+    LabelsSelect.prototype.setOriginalDropdownData = function($container, $dropdown) {
+      var labels = [];
+      $container.find('[name="label_name[]"]').map(function() {
+        return labels.push(this.value);
+      });
+      $dropdown.data('marked', labels);
+    };
+
+    return LabelsSelect;
   })();
 
 }).call(this);
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
index b1718e89d3deeb018467fed3547fd566b0d3bca2..4cdf99cae72578e74b28e564563a5db555039fa8 100644
--- a/app/assets/javascripts/lib/ace.js
+++ b/app/assets/javascripts/lib/ace.js
@@ -1,3 +1,2 @@
-/* eslint-disable */
 /*= require ace-rails-ap */
 /*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index d0fe69260a53496bd7ec58c7a03d357a6a5eead9..3c9ad0e67c85e8c491418cf6e2fce21a0ea9f93b 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, no-undef, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, consistent-return, prefer-arrow-callback, no-return-assign, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, max-len */
+
 (function() {
   (function(w) {
     var notificationGranted, notifyMe, notifyPermissions;
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index b0f834705c3f90ff266768bd5fc58f9aa805b435..9af89b79f84bd5f0f18bd382f5a3b1b2e2c3a2b2 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, no-undef, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, no-underscore-dangle, no-param-reassign, prefer-template, quotes, comma-dangle, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, spaced-comment, radix, no-else-return, max-len, no-plusplus, padded-blocks */
+
 // LineHighlighter
 //
 // Handles single- and multi-line selection and highlight for blob views.
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index 9e4ffd07dbd64358d3d69c8223a350e98baba038..f95b079c972194861a90cef76faf6861034817ea 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -1,4 +1,8 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, quote-props, no-useless-computed-key, object-shorthand, prefer-const, no-new, padded-blocks, no-param-reassign, semi, max-len */
+/* global Vue */
+/* global ace */
+/* global Flash */
+
 ((global) => {
 
   global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
index 23c4618af7048852a8a83ef39634d445ba8bae1d..74544b7d0c73dbd09a982b7dbf2687fceab67044 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Vue */
+
 ((global) => {
 
   global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
index 4ccbdcd6daa2024bd7dfa44506fda28654475f5d..78c00c31c16b1c2cd7dbd0540f38ea73fa98f725 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, no-param-reassign, comma-dangle */
+/* global Vue */
+
 ((global) => {
 
   global.mergeConflicts = global.mergeConflicts || {};
@@ -19,7 +21,7 @@
             </td>
             <td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td>
             <td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td>
-          </template> 
+          </template>
         </tr>
       </table>
     `,
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
index 8a7519b07869e540e1989568c6383b9a63b6aa36..8df3170edac9de93ca0d4901acdbbbe559707e32 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle, no-extra-semi, padded-blocks */
+
 ((global) => {
   global.mergeConflicts = global.mergeConflicts || {};
 
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
index f94e51e783cbfe88d6214e8f65006b642c39fc84..53b44007510c4cdf0540fa682aec02b9dcee1c4e 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, object-shorthand, no-dupe-keys, no-param-reassign, no-plusplus, camelcase, prefer-const, no-nested-ternary, no-continue, semi, func-call-spacing, no-spaced-func, padded-blocks, max-len */
+/* global Cookies */
+/* global Vue */
+
 ((global) => {
   global.mergeConflicts = global.mergeConflicts || {};
 
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
index 815443fb54e3b6c7c9fddc2826706fd2957c56c3..83520702f9b67e6ecf26e1469eed84474d6491b4 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable new-cap, comma-dangle, no-new, semi */
+/* global Vue */
+/* global Flash */
+
 //= require vue
 //= require ./merge_conflict_store
 //= require ./merge_conflict_service
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
index c8de586aa212720d182badd13ba3ff647f714038..e89b35d5407f4f04ef54b0f3c94664f027f679d1 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, comma-dangle, padded-blocks */
+
 ((global) => {
   global.mergeConflicts = global.mergeConflicts || {};
 
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
index 88c3a20ce132b8ba2fa2c6c207b6d06fca51db81..a4aca85d46013060623f68390a2ca2c7c98246f8 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-param-reassign, quote-props, comma-dangle, padded-blocks */
+
 ((global) => {
   global.mergeConflicts = global.mergeConflicts || {};
 
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 88c3636be6cf84394355e4a40210ce7e7cec48b6..244c2f6746c9c6cc676e5cf16cb38b7eff1ea1c9 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-underscore-dangle, one-var, one-var-declaration-per-line, consistent-return, dot-notation, quote-props, comma-dangle, object-shorthand, padded-blocks, max-len, prefer-arrow-callback */
+/* global MergeRequestTabs */
 
 /*= require jquery.waitforimages */
 /*= require task_list */
@@ -26,6 +27,7 @@
       // Prevent duplicate event bindings
       this.disableTaskList();
       this.initMRBtnListeners();
+      this.initCommitMessageListeners();
       if ($("a.btn-close").length) {
         this.initTaskList();
       }
@@ -107,6 +109,26 @@
     // note so that we can re-use its form here
     };
 
+    MergeRequest.prototype.initCommitMessageListeners = function() {
+      var textarea = $('textarea.js-commit-message');
+
+      $('a.js-with-description-link').on('click', function(e) {
+        e.preventDefault();
+
+        textarea.val(textarea.data('messageWithDescription'));
+        $('p.js-with-description-hint').hide();
+        $('p.js-without-description-hint').show();
+      });
+
+      $('a.js-without-description-link').on('click', function(e) {
+        e.preventDefault();
+
+        textarea.val(textarea.data('messageWithoutDescription'));
+        $('p.js-with-description-hint').show();
+        $('p.js-without-description-hint').hide();
+      });
+    };
+
     return MergeRequest;
 
   })();
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 7022aa1263b8ee55b57c2dd12c754c137d9316c3..e47047c4cca355e8a6508c0c57d421934ee83b6d 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,5 +1,10 @@
-/* eslint-disable */
- ((global) => {
+/* eslint-disable max-len, no-var, func-names, space-before-function-paren, vars-on-top, no-plusplus, comma-dangle, no-return-assign, consistent-return, no-param-reassign, one-var, one-var-declaration-per-line, quotes, prefer-template, no-else-return, prefer-arrow-callback, no-unused-vars, no-underscore-dangle, no-shadow, no-mixed-operators, template-curly-spacing, camelcase, default-case, wrap-iife, semi, padded-blocks */
+/* global notify */
+/* global notifyPermissions */
+/* global merge_request_widget */
+/* global Turbolinks */
+
+((global) => {
   var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
 
   const DEPLOYMENT_TEMPLATE = `<div class="mr-widget-heading" id="<%- id %>">
@@ -27,7 +32,7 @@
        </div>
      </div>`;
 
-   global.MergeRequestWidget = (function() {
+  global.MergeRequestWidget = (function() {
     function MergeRequestWidget(opts) {
       // Initialize MergeRequestWidget behavior
       //
@@ -82,10 +87,10 @@
     };
 
     MergeRequestWidget.prototype.retrieveSuccessIcon = function() {
-       const $ciSuccessIcon = $('.js-success-icon');
-       this.$ciSuccessIcon = $ciSuccessIcon.html();
-       $ciSuccessIcon.remove();
-     }
+      const $ciSuccessIcon = $('.js-success-icon');
+      this.$ciSuccessIcon = $ciSuccessIcon.html();
+      $ciSuccessIcon.remove();
+    }
 
     MergeRequestWidget.prototype.mergeInProgress = function(deleteSourceBranch) {
       if (deleteSourceBranch == null) {
@@ -204,7 +209,7 @@
         const template = _.template(templateString)(environment)
         this.$widgetBody.before(template);
       }
-     };
+    };
 
     MergeRequestWidget.prototype.showCIStatus = function(state) {
       var allowed_states;
@@ -246,4 +251,4 @@
 
   })();
 
- })(window.gl || (window.gl = {}));
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 15a12c3d98521276f4c27e2b6afc521217bfb32b..9f8af46c7158b0860a165dfb2a27753a2a646eac 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, padded-blocks, max-len */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index db7561a3a7561b4532c017926982bbfb1d342d88..42152362e60ee63e58b6d81c3aa52947ce3d3fa3 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, no-undef, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-use-before-define, camelcase, quotes, object-shorthand, no-shadow, no-unused-vars, comma-dangle, no-var, prefer-template, no-underscore-dangle, consistent-return, one-var, one-var-declaration-per-line, default-case, prefer-arrow-callback, padded-blocks, max-len */
+/* global Flash */
+
 (function() {
   this.Milestone = (function() {
     Milestone.updateIssue = function(li, issue_url, data) {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 67796083790a8d13584a9a6fb5956015b268559c..28054b7824992874ae2e26a1af95cf2c1bee79e3 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-undef, no-param-reassign, no-shadow, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-underscore-dangle, prefer-arrow-callback, max-len, one-var, one-var-declaration-per-line, no-unused-vars, object-shorthand, comma-dangle, no-else-return, no-self-compare, consistent-return, no-param-reassign, no-shadow, padded-blocks */
+/* global Vue */
+/* global Issuable */
+/* global ListMilestone */
+
 (function() {
   this.MilestoneSelect = (function() {
     function MilestoneSelect(currentProject) {
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 87c903ec5763c502c34d23f3f4ba0b90352b1dab..6633f2c27095134092d4a9d341585c204eba6404 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,8 +1,10 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, no-undef, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, vars-on-top, one-var-declaration-per-line, comma-dangle, object-shorthand, no-else-return, prefer-template, quotes, prefer-arrow-callback, padded-blocks, no-param-reassign, no-cond-assign, max-len */
+/* global Api */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
-  this.NamespaceSelect = (function() {
+  window.NamespaceSelect = (function() {
     function NamespaceSelect(opts) {
       this.onSelectItem = bind(this.onSelectItem, this);
       var fieldName, showAny;
@@ -64,7 +66,7 @@
 
   })();
 
-  this.NamespaceSelects = (function() {
+  window.NamespaceSelects = (function() {
     function NamespaceSelects(opts) {
       var ref;
       if (opts == null) {
@@ -74,7 +76,7 @@
       this.$dropdowns.each(function(i, dropdown) {
         var $dropdown;
         $dropdown = $(dropdown);
-        return new NamespaceSelect({
+        return new window.NamespaceSelect({
           dropdown: $dropdown
         });
       });
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index e3dc599b90ab9d58e378c9075e35fe1bd5769928..64b19a548932252175cc971507fafeace1d21bb6 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-undef, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, comma-dangle, one-var, one-var-declaration-per-line, no-mixed-operators, new-cap, no-plusplus, no-loop-func, no-floating-decimal, consistent-return, no-unused-vars, prefer-template, prefer-arrow-callback, camelcase, max-len, padded-blocks */
+/* global Raphael */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 5a8f723a27b678f460084146bfb5182ddb4a60f0..2367d2497b2268202f18be22323fe32817280570 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, no-undef, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, padded-blocks, max-len */
+/* global BranchGraph */
+
 (function() {
   this.Network = (function() {
     function Network(opts) {
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 732d92845cbf658d7823819c142192b8cbb61631..17833d3419a956bf1831c8cf11b2dacd25867114 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, no-undef, comma-dangle, consistent-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, padded-blocks, max-len */
+/* global Network */
+/* global ShortcutsNetwork */
+
 // This is a manifest file that'll be compiled into including all the files listed below.
 // Add new JavaScript code in separate files in this directory and they'll automatically
 // be included in the compiled file accessible from http://example.com/assets/application.js
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 0ca0e2555951efe44b9c87c006a3f0cd98220de0..a8b9a352870b50d5bfc9c128e72c9b49417fba89 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,4 +1,8 @@
-/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, no-undef, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks, max-len */
+/* eslint-disable no-restricted-properties, func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-use-before-define, camelcase, no-unused-expressions, quotes, max-len, one-var, one-var-declaration-per-line, default-case, prefer-template, consistent-return, no-alert, no-return-assign, no-param-reassign, prefer-arrow-callback, no-else-return, comma-dangle, no-new, brace-style, no-lonely-if, vars-on-top, no-unused-vars, semi, indent, no-sequences, no-shadow, newline-per-chained-call, no-useless-escape, radix, padded-blocks */
+/* global Flash */
+/* global GLForm */
+/* global Autosave */
+/* global ResolveService */
 
 /*= require autosave */
 /*= require autosize */
@@ -305,7 +309,7 @@
       }
       row = form.closest("tr");
       note_html = $(note.html);
-      note_html.syntaxHighlight();
+      note_html.renderGFM();
       // is this the first note of discussion?
       discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
       if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
@@ -322,7 +326,7 @@
         discussionContainer.append(note_html);
         // Init discussion on 'Discussion' page if it is merge request page
         if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
-          $('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
+          $('ul.main-notes-list').append(note.discussion_html).renderGFM();
         }
       } else {
         // append new note to all matching discussions
@@ -463,7 +467,7 @@
       // Convert returned HTML to a jQuery object so we can modify it further
       $html = $(note.html);
       gl.utils.localTimeAgo($('.js-timeago', $html));
-      $html.syntaxHighlight();
+      $html.renderGFM();
       $html.find('.js-task-list-container').taskList('enable');
       // Find the note's `li` element by ID and replace it with the updated HTML
       $note_li = $('.note-row-' + note.id);
@@ -671,7 +675,7 @@
      */
 
     Notes.prototype.addDiffNote = function(e) {
-      var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, replyButton, row, rowCssToAdd, targetContent;
+      var $link, addForm, hasNotes, lineType, newForm, nextRow, noteForm, notesContent, notesContentSelector, replyButton, row, rowCssToAdd, targetContent;
       e.preventDefault();
       $link = $(e.currentTarget);
       row = $link.closest("tr");
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index b152d26733f7bc0a141aae81c96b4fbd00c2054e..324b68a7efc4c7155c263ebffc66b82540590691 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, one-var, no-var, one-var-declaration-per-line, no-unused-vars, consistent-return, prefer-arrow-callback, no-else-return, padded-blocks, max-len */
+/* global Flash */
+
 (function() {
   this.NotificationsDropdown = (function() {
     function NotificationsDropdown() {
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index fb95648e1c74cf5432796f484fc461215ade89c8..0b09ad113a34e9cdaf3adc641715ccb3f06ebcfb 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,6 +1,7 @@
+/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, padded-blocks, no-param-reassign, max-len */
+
 //= require lib/utils/bootstrap_linked_tabs
 
-/* eslint-disable */
 ((global) => {
 
   class Pipelines {
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 3723aa24942b86b52201bf325a874174ae82b442..1e261cd49c2575119741b7acaa86f7c5a86191ab 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, no-undef, camelcase, prefer-arrow-callback, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, one-var, one-var-declaration-per-line, wrap-iife, no-else-return, consistent-return, object-shorthand, comma-dangle, no-param-reassign, padded-blocks, camelcase, prefer-arrow-callback, max-len */
+
 // MarkdownPreview
 //
 // Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
@@ -7,7 +8,7 @@
 (function() {
   var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
 
-  this.MarkdownPreview = (function() {
+  window.MarkdownPreview = (function() {
     function MarkdownPreview() {}
 
     // Minimum number of users referenced before triggering a warning
@@ -27,7 +28,7 @@
         return this.renderMarkdown(mdText, (function(_this) {
           return function(response) {
             preview.html(response.body);
-            preview.syntaxHighlight();
+            preview.renderGFM();
             return _this.renderReferencedUsers(response.references.users, form);
           };
         })(this));
@@ -83,7 +84,7 @@
 
   })();
 
-  markdownPreview = new MarkdownPreview();
+  markdownPreview = new window.MarkdownPreview();
 
   previewButtonSelector = '.js-md-preview-button';
 
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
index 6da6c1d02954d2d6ca9484d74bb20c90e40f79b9..b4b6da41f6321bbbd87b15b921945eeeb4096ce7 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-useless-escape, max-len, padded-blocks, quotes, no-var, no-underscore-dangle, func-names, space-before-function-paren, no-unused-vars, no-return-assign, object-shorthand, one-var, one-var-declaration-per-line, comma-dangle, consistent-return, class-methods-use-this, no-plusplus, new-parens, semi */
+
 ((global) => {
 
   // Matches everything but the file name
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index 3eb81808bd62c9ec90ee2a1bc88f36d3db2e255a..eb2fe3477a2fa0cac1d5a37de856f1acdbf321b7 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len, padded-blocks */
+/* global Flash */
+
 ((global) => {
 
   class Profile {
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 016d999d77ee4675003ba90be7da9509481dfa3e..fcf3a4af9566d35bc3a5c2ae65c405e4763b2fa3 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-undef, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, consistent-return, no-new, prefer-arrow-callback, no-return-assign, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-else-return, newline-per-chained-call, no-shadow, semi, vars-on-top, indent, prefer-template, padded-blocks, max-len */
+/* global Cookies */
+/* global Turbolinks */
+/* global ProjectSelect */
+
 (function() {
   this.Project = (function() {
     function Project() {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 804306a329350844b2bb0644d16347cd60297b4d..1bd232314d0d989c9e31d38704c437b5ef07e450 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, no-undef, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, consistent-return, one-var, one-var-declaration-per-line, no-cond-assign, max-len, object-shorthand, no-param-reassign, comma-dangle, no-plusplus, prefer-template, no-unused-vars, no-return-assign, padded-blocks */
+/* global fuzzaldrinPlus */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index c99e55234cfcc69f74a5d3cfa31f3e43e0f2ba92..02dafcfb8652438882ae9cb9860243f6be69e2da 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, padded-blocks, max-len */
+/* global Turbolinks */
+
 (function() {
   this.ProjectImport = (function() {
     function ProjectImport() {
diff --git a/app/assets/javascripts/project_label_subscription.js.es6 b/app/assets/javascripts/project_label_subscription.js.es6
index 03a115cb35b2c51a7c714296f0ea75681c35035f..b8d6a1989963b237c5e47e088d3098ae206c7e3a 100644
--- a/app/assets/javascripts/project_label_subscription.js.es6
+++ b/app/assets/javascripts/project_label_subscription.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable wrap-iife, func-names, space-before-function-paren, object-shorthand, comma-dangle, one-var, one-var-declaration-per-line, no-restricted-syntax, prefer-const, max-len, no-param-reassign, padded-blocks */
+
 (function(global) {
   class ProjectLabelSubscription {
     constructor(container) {
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index fe1f96872f37c0b42e507fbaa0279e22cd29b2b3..650996700ba0c0b1e39c59a31f104ba181642675 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-undef, no-else-return, quotes, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-var, comma-dangle, object-shorthand, one-var, one-var-declaration-per-line, no-else-return, quotes, padded-blocks, max-len */
+/* global Api */
+
 (function() {
   this.ProjectSelect = (function() {
     function ProjectSelect() {
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index dbf530bed41a1e134cd8786e3c1e33267f02b581..4548dc68fe1f532b251eda07e93f41c7c1b29ca2 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,6 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, no-undef, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-var, one-var, one-var-declaration-per-line, prefer-arrow-callback, consistent-return, no-unused-vars, camelcase, prefer-template, comma-dangle, padded-blocks, max-len */
+
 (function() {
-  this.ProjectsList = {
+  window.ProjectsList = {
     init: function() {
       $(".projects-list-filter").off('keyup');
       this.initSearch();
@@ -9,7 +10,7 @@
     initSearch: function() {
       var debounceFilter, projectsListFilter;
       projectsListFilter = $('.projects-list-filter');
-      debounceFilter = _.debounce(ProjectsList.filterResults, 500);
+      debounceFilter = _.debounce(window.ProjectsList.filterResults, 500);
       return projectsListFilter.on('keyup', function(e) {
         if (projectsListFilter.val() !== '') {
           return debounceFilter();
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 2d60947a666e29dd114a839ce85a99906dfd0b4f..4aef1c84b569d5bc474ee6bebf94114f8c784ff1 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, object-shorthand, no-else-return, comma-dangle, semi, padded-blocks, max-len */
+
 (global => {
   global.gl = global.gl || {};
 
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index c45c9d8ff227a50b59545e6670b1e4e9390c6c86..f26fba979a4c50d16221354f511d5bae80944c60 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, comma-dangle, padded-blocks, semi, max-len */
+/* global ProtectedBranchDropdown */
+
 (global => {
   global.gl = global.gl || {};
 
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index e3f226e9a2a3b537e4d6ac16f8ee06b8db40aef7..08264ad3d2f05246d166c8235fbea032bfe7ce92 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars */
+
 class ProtectedBranchDropdown {
   constructor(options) {
     this.onSelect = options.onSelect;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index ac3142ffb07a97f82c4eccfcdd0b96350e639dd2..4ff2fa5a80f758f881e91109c7943e48ded3067a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable no-new, arrow-parens, no-param-reassign, no-irregular-whitespace, padded-blocks, comma-dangle, no-trailing-spaces, semi, max-len */
+/* global Flash */
+
 (global => {
   global.gl = global.gl || {};
 
@@ -33,7 +35,7 @@
       const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
 
       // Do not update if one dropdown has not selected any option
-      if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; 
+      if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
 
       $.ajax({
         type: 'POST',
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index 705378a364d0fddd27afd38b94ae19d6d0b9d803..b6972ef2e16233e4dcf2f81a75d43777df82fdcf 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, no-irregular-whitespace, no-new, comma-dangle, semi, padded-blocks, max-len */
+
 (global => {
   global.gl = global.gl || {};
 
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index 17e3416383162ad82c2acff987004c6e185fda5b..15b3affd4696bc6533e42a36ff8168b351f137c1 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1,2 +1 @@
-/* eslint-disable */
 /*= require_tree . */
diff --git a/app/assets/javascripts/render_gfm.js b/app/assets/javascripts/render_gfm.js
new file mode 100644
index 0000000000000000000000000000000000000000..bbb2f186655e6ad0ea3ba6e46fa2c7ef74d004c7
--- /dev/null
+++ b/app/assets/javascripts/render_gfm.js
@@ -0,0 +1,16 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+// Render Gitlab flavoured Markdown
+//
+// Delegates to syntax highlight and render math
+//
+(function() {
+  $.fn.renderGFM = function() {
+    this.find('.js-syntax-highlight').syntaxHighlight();
+    this.find('.js-render-math').renderMath();
+  };
+
+  $(document).on('ready page:load', function() {
+    return $('body').renderGFM();
+  });
+
+}).call(this);
diff --git a/app/assets/javascripts/render_math.js b/app/assets/javascripts/render_math.js
new file mode 100644
index 0000000000000000000000000000000000000000..209e7a8661d5da3656a997c4c01da828ce9afa19
--- /dev/null
+++ b/app/assets/javascripts/render_math.js
@@ -0,0 +1,55 @@
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len, no-console */
+// Renders math using KaTeX in any element with the
+// `js-render-math` class
+//
+// ### Example Markup
+//
+//   <code class="js-render-math"></div>
+//
+(function() {
+  // Only load once
+  var katexLoaded = false;
+
+  // Loop over all math elements and render math
+  var renderWithKaTeX = function (elements) {
+    elements.each(function () {
+      var mathNode = $('<span></span>');
+      var $this = $(this);
+
+      var display = $this.attr('data-math-style') === 'display';
+      try {
+        katex.render($this.text(), mathNode.get(0), { displayMode: display });
+        mathNode.insertAfter($this);
+        $this.remove();
+      } catch (err) {
+        // What can we do??
+        console.log(err.message);
+      }
+    });
+  };
+
+  $.fn.renderMath = function() {
+    var $this = this;
+    if ($this.length === 0) return;
+
+    if (katexLoaded) renderWithKaTeX($this);
+    else {
+      // Request CSS file so it is in the cache
+      $.get(gon.katex_css_url, function() {
+        var css = $('<link>',
+          { rel: 'stylesheet',
+            type: 'text/css',
+            href: gon.katex_css_url,
+          });
+        css.appendTo('head');
+
+        // Load KaTeX js
+        $.getScript(gon.katex_js_url, function() {
+          katexLoaded = true;
+          renderWithKaTeX($this); // Run KaTeX
+        });
+      });
+    }
+  };
+
+}).call(this);
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 440b5da756db66001d28a57417ec88e04e8e5ff6..b1e844b730268e1b72db040077089edcac3b679c 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, no-undef, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-unused-vars, semi, consistent-return, one-var, one-var-declaration-per-line, quotes, prefer-template, object-shorthand, comma-dangle, no-else-return, no-param-reassign, padded-blocks, max-len */
+/* global Cookies */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 1d208f1494ca9b2621dedcfc7b786969f34ca92d..4b6ebadeac791f379c7179ca3bb449455ec8597c 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, no-undef, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, one-var, one-var-declaration-per-line, object-shorthand, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-else-return, padded-blocks, max-len */
+/* global Api */
+
 (function() {
   this.Search = (function() {
     function Search() {
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index 5fa94556501560a74c0fa081a2bee0f5a8654cea..437f5dbbf7d60dd605279b06d904b8db5dc4a5cc 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-return-assign, one-var, no-var, no-underscore-dangle, one-var-declaration-per-line, no-unused-vars, no-cond-assign, consistent-return, object-shorthand, prefer-arrow-callback, func-names, space-before-function-paren, no-plusplus, prefer-template, quotes, class-methods-use-this, no-unused-expressions, no-sequences, wrap-iife, no-lonely-if, no-else-return, no-param-reassign, vars-on-top, padded-blocks, no-extra-semi, indent, max-len */
+
 ((global) => {
 
   const KEYCODE = {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index fa2168723bed0424dc460758dddf62f019f1503d..5ea00f408f49f8bce37a541b657d3f6d17cffa38 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, no-undef, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-plusplus, no-else-return, comma-dangle, padded-blocks, max-len */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global findFileURL */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index 65305b8c22fc809d7244b23d88c75d469c4af08c..c26903038b4ce04f0750e881551be09cdc541a43 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, consistent-return, padded-blocks */
+/* global Shortcuts */
+/* global Mousetrap */
 
 /*= require shortcuts */
 
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index 1b9a265ba39041a4cb0fa28947c23ed9cd153b8f..4549742bbcb54478fa03c77edd3c65b406754e79 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* global Mousetrap */
+/* global Shortcuts */
 
 /*= require shortcuts */
 
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 68cd6fad04e6902152050f8054793dcc8a160ac0..3a81380eef0a38e7ec858d10416800e4e8981f17 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
 
 /*= require shortcuts_navigation */
 
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index c4899f3566a786ac4190d9515bc6753326ffcae7..b892fbc33933c42e698433e0ee9a84edd9d72e81 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, one-var-declaration-per-line, quotes, prefer-arrow-callback, consistent-return, prefer-template, no-mixed-operators, padded-blocks */
+/* global Mousetrap */
+/* global Turbolinks */
+/* global ShortcutsNavigation */
+/* global sidebar */
 
 /*= require mousetrap */
 /*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index 7d4d6364c706d4c90ec2d5265b7c16e274ccc4c4..0776d0a9b67348fc091b3fcee988e3b9a219498f 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign, padded-blocks */
+/* global Mousetrap */
+/* global Shortcuts */
 
 /*= require shortcuts */
 
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index a4095d2c06b07c75789f7daac5285d0465b3abdd..ecc3fab84c35d4697b1a41bb1c993bee1c7c949c 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, padded-blocks, max-len */
+/* global Mousetrap */
+/* global ShortcutsNavigation */
 
 /*= require shortcuts_navigation */
 
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index a23ca449c4b32913ed0408832622b52dd85c2ead..9790a44972d6c98025c4c44c271350f7fe017cd5 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign, padded-blocks */
+/* global Cookies */
+
 ((global) => {
   let singleton;
 
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 0d48e69cce9acd1f81044bb086ccee6856c5b389..ac8603ccd1076954a7e776b44ca3b4456a61629d 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,8 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, no-undef, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, one-var, one-var-declaration-per-line, consistent-return, no-param-reassign, padded-blocks, max-len */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
-  this.SingleFileDiff = (function() {
+  window.SingleFileDiff = (function() {
     var COLLAPSED_HTML, ERROR_HTML, LOADING_HTML, WRAPPER;
 
     WRAPPER = '<div class="diff-content diff-wrap-lines"></div>';
@@ -93,7 +94,7 @@
   $.fn.singleFileDiff = function(forceLoad, cb) {
     return this.each(function() {
       if (!$.data(this, 'singleFileDiff') || forceLoad) {
-        return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
+        return $.data(this, 'singleFileDiff', new window.SingleFileDiff(this, forceLoad, cb));
       }
     });
   };
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 2c8ecba7de430e90ad3d558c8d318f81cf708c8a..18512d179b3423c3fe66348991702319dc7b1b91 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, no-undef, quotes, semi, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, semi, padded-blocks, max-len */
+/* global ace */
+
 /*= require_tree . */
 
 (function() {
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
index c3afc3f2246f2fa1acf7cc9740a7cf2abdbad41f..6f913326a3adc31acea7c3302ef4548725be08d6 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable arrow-parens, no-param-reassign, space-before-function-paren, func-names, no-var, semi, max-len */
+
 (global => {
   global.gl = global.gl || {};
 
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 32803fa790b4ef5caf8583d47f45647230c62598..f1fc526bf2e015fd7aed1425877ef3a5542f6c9f 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, padded-blocks, max-len */
+/* global Flash */
+
 (function() {
   this.Star = (function() {
     function Star() {
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index bd37d69165f692fcb2cb0ee376a4bb381f7b1324..5d0fa62c50a00425ce5fc5ea3650e6726f7c4115 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,4 +1,5 @@
-/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-undef, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, consistent-return, no-var, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
+
 // Syntax Highlighter
 //
 // Applies a syntax highlighting color scheme CSS class to any element with the
@@ -9,8 +10,10 @@
 //   <div class="js-syntax-highlight"></div>
 //
 (function() {
+
   $.fn.syntaxHighlight = function() {
     var $children;
+
     if ($(this).hasClass('js-syntax-highlight')) {
       // Given the element itself, apply highlighting
       return $(this).addClass(gon.user_color_scheme);
@@ -23,8 +26,4 @@
     }
   };
 
-  $(document).on('ready page:load', function() {
-    return $('.js-syntax-highlight').syntaxHighlight();
-  });
-
 }).call(this);
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index 93a3d67ee9fb1ed80a757403e41e1e71700f3d3c..d2b152045b43fdef8293bfd6137d86584b9aea25 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable prefer-const, comma-dangle, max-len, no-useless-return, object-curly-spacing, no-param-reassign, max-len */
+/* global Api */
+
 /*= require ../blob/template_selector */
 
 ((global) => {
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index 0a3890e85feeb0fae6a3cd1a7f6bee21d7b6a552..7310b9de074e50e86b06f119259a11d018ccf10d 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-new, comma-dangle, class-methods-use-this, prefer-const, no-param-reassign */
+
 ((global) => {
   class IssuableTemplateSelectors {
     constructor({ $dropdowns, editor } = {}) {
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index 213e80825b726137ade2b32a563b619be6e1e09b..d8713600030e71444a97ef1b963100e7c4dcbc02 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable padded-blocks, class-methods-use-this, no-new, func-names, prefer-template, no-unneeded-ternary, object-shorthand, space-before-function-paren, comma-dangle, quote-props, consistent-return, no-else-return, semi, no-param-reassign, max-len */
+/* global UsersSelect */
+/* global Turbolinks */
+
 ((global) => {
 
   class Todos {
@@ -72,7 +75,7 @@
     allDoneClicked(e) {
       e.preventDefault();
       e.stopImmediatePropagation();
-      $target = $(e.currentTarget);
+      const $target = $(e.currentTarget);
       $target.disable();
       return $.ajax({
         type: 'POST',
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 5d991542b512f8e98f35404055fe888e22494020..d2aa3c7a841016e928a5d5261dfa35b49f563080 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, prefer-arrow-callback, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
 // Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
 //
 // State Flow #1: setup -> in_progress -> authenticated -> POST to server
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index 4c70a6e9bb6e727a6ba67d0bfc0295a94c36ae5e..69f98c9c0adc6a3630551fea28102e2f30c973ee 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,4 +1,6 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, no-undef, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-console, quotes, prefer-template, padded-blocks, max-len */
+/* global u2f */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 97d8993cac22dc6e5b81399985f5df104e7e32f6..4f5d68f546b886522f73d144b8fa3bf96b8ac392 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-undef, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, no-else-return, quotes, quote-props, comma-dangle, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global u2f */
+/* global U2FError */
+/* global U2FUtil */
+
 // Register U2F (universal 2nd factor) devices for users to authenticate with.
 //
 // State Flow #1: setup -> in_progress -> registered -> POST to server
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 5e869e99fdb1a657c26f4b3064861ce6c37a7869..0a2db7c05fe18b80ae9863fd48958b3166a7a90d 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable class-methods-use-this, comma-dangle, arrow-parens, no-param-reassign, semi */
+/* global Cookies */
+
 ((global) => {
   global.User = class {
     constructor({ action }) {
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index 5a625611987aa0407ac4b073c20812e4b1eb899f..b9c23b51b4da02c577954080b72b71c9e50fd397 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable max-len, space-before-function-paren, no-underscore-dangle, array-bracket-spacing, consistent-return, comma-dangle, no-unused-vars, dot-notation, no-new, no-return-assign, camelcase, semi, no-param-reassign */
+
 /*
 UserTabs
 
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6
index c4dde575c6ee88ec685df0e74e98aaea028abb8c..137cefa3b8e872d77106ca4159cd12970ce73205 100644
--- a/app/assets/javascripts/username_validator.js.es6
+++ b/app/assets/javascripts/username_validator.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
+
 ((global) => {
   const debounceTimeoutDuration = 1000;
   const invalidInputClass = 'gl-field-error-outline';
@@ -77,7 +78,7 @@
         this.renderState();
         return $.ajax({
           type: 'GET',
-          url: `/users/${username}/exists`,
+          url: `${gon.relative_url_root}/users/${username}/exists`,
           dataType: 'json',
           success: (res) => this.setAvailabilityState(res.exists)
         });
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index ba7f533c3490275c7d769dfc963df99f11d93138..578be7c3590a2360df865092637341624dbe5319 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, no-undef, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, no-var, space-before-blocks, prefer-rest-params, wrap-iife, camelcase, vars-on-top, semi, keyword-spacing, no-plusplus, object-shorthand, comma-dangle, eqeqeq, no-mixed-operators, no-return-assign, newline-per-chained-call, prefer-arrow-callback, consistent-return, one-var, one-var-declaration-per-line, prefer-template, quotes, no-unused-vars, no-else-return, padded-blocks, max-len */
+/* global d3 */
+/* global dateFormat */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
 
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index c6e18fad832284e0857da34530a6439648dda198..d4b5e03aa358dd531966544cc70feb0f14ec5e2f 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,4 +1,8 @@
-/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, no-undef, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, one-var, no-var, space-before-blocks, prefer-rest-params, wrap-iife, quotes, max-len, one-var-declaration-per-line, vars-on-top, prefer-arrow-callback, consistent-return, comma-dangle, object-shorthand, no-shadow, no-unused-vars, no-plusplus, no-else-return, no-self-compare, prefer-template, no-unused-expressions, no-lonely-if, yoda, prefer-spread, no-void, camelcase, keyword-spacing, no-param-reassign, padded-blocks */
+/* global Vue */
+/* global Issuable */
+/* global ListUser */
+
 (function() {
   var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
     slice = [].slice;
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 82eb761442ad1dc72b55d17289d8a29495b18e8d..e09b59dd5aaac7890e5497494787ed6cfae489e5 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,4 +1,7 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, no-undef, camelcase, comma-dangle, padded-blocks, max-len */
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, prefer-arrow-callback, no-unused-vars, consistent-return, camelcase, comma-dangle, padded-blocks, max-len */
+/* global Dropzone */
+/* global Mousetrap */
+
 // Zen Mode (full screen) textarea
 //
 /*= provides zen_mode:enter */
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index c82a9a2b9e32abde2c47f560934549176de82fc3..40bc05793932a9c6fa4011b1d554997552a6edcc 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -44,3 +44,6 @@
 @import "framework/awards.scss";
 @import "framework/images.scss";
 @import "framework/broadcast-messages";
+@import "framework/emojis.scss";
+@import "framework/icons.scss";
+@import "framework/snippets.scss";
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index dece5c3202b33d9ea6a328be638fb43681d55438..9fc9bcebc4498a3a96f1524aa269226b99f4f3ed 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -12,8 +12,8 @@
   z-index: 9;
   width: 300px;
   font-size: 14px;
-  background-color: $award-emoji-menu-bg;
-  border: 1px solid $award-emoji-menu-border;
+  background-color: $white-light;
+  border: 1px solid $border-white-light;
   border-radius: $border-radius-base;
   box-shadow: 0 6px 12px $award-emoji-menu-shadow;
   pointer-events: none;
@@ -135,7 +135,7 @@
   }
 
   .award-control-icon {
-    color: $award-emoji-new-btn-icon-color;
+    color: $border-gray-normal;
     margin-top: 1px;
   }
 }
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 95c024992712e52b7a611512f1771cb33042a03a..9f02749f5abc6614bb3725860656b3c5b6f495d6 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -24,7 +24,7 @@
 .row-content-block {
   margin-top: 0;
   margin-bottom: -$gl-padding;
-  background-color: $background-color;
+  background-color: $gray-light;
   padding: $gl-padding;
   margin-bottom: 0;
   border-top: 1px solid $white-dark;
@@ -118,7 +118,7 @@
 
 .cover-block {
   text-align: center;
-  background: $background-color;
+  background: $gray-light;
   padding-top: 44px;
   position: relative;
 
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 1c7b2f4df7cc01882f52a1548ae9a5ab74cd9916..59ff17ad2c1c6903837205ace6af854e1a90d8f6 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -88,11 +88,11 @@
 }
 
 @mixin btn-gray {
-  @include btn-color($gray-light, $border-gray-light, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
+  @include btn-color($gray-light, $border-gray-normal, $gray-normal, $border-gray-normal, $gray-dark, $border-gray-dark, $gl-gray-dark);
 }
 
 @mixin btn-white {
-  @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-dark, $gl-text-color);
+  @include btn-color($white-light, $border-color, $white-normal, $border-white-normal, $white-dark, $border-gray-dark, $gl-text-color);
 }
 
 @mixin btn-with-margin {
@@ -235,7 +235,7 @@
 }
 
 .btn-transparent {
-  color: $btn-transparent-color;
+  color: $gl-gray-light;
   background-color: transparent;
   border: 0;
 
@@ -289,7 +289,7 @@
   .active {
     box-shadow: $gl-btn-active-background;
 
-    border: 1px solid $border-white-dark !important;
+    border: 1px solid $border-gray-dark !important;
     background-color: $btn-active-gray-light !important;
   }
 }
@@ -309,8 +309,8 @@
   text-align: left;
   padding: 6px 16px;
   border-color: $border-color;
-  color: $btn-placeholder-gray;
-  background-color: $background-color;
+  color: $gray-darkest;
+  background-color: $gray-light;
 
   &:hover,
   &:active,
@@ -318,8 +318,8 @@
     cursor: text;
     box-shadow: none;
     border-color: $border-color;
-    color: $btn-placeholder-gray;
-    background-color: $background-color;
+    color: $gray-darkest;
+    background-color: $gray-light;
   }
 }
 
@@ -331,7 +331,7 @@
   margin-left: 10px;
 
   i {
-    color: $gl-icon-color;
+    color: $gl-gray-light;
   }
 }
 
@@ -344,8 +344,8 @@
 }
 
 .btn-static {
-  background-color: $background-color !important;
-  border: 1px solid $border-gray-light;
+  background-color: $gray-light !important;
+  border: 1px solid $border-gray-normal;
   cursor: default;
 
   &:active {
diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss
index 2a100980aca8a169f1a3344cbdddee8e862fa788..e0e46dd73aff33c18e3868ef3930b20c0962cd26 100644
--- a/app/assets/stylesheets/framework/callout.scss
+++ b/app/assets/stylesheets/framework/callout.scss
@@ -11,7 +11,7 @@
   padding: $gl-padding;
   border-left: 3px solid $border-color;
   color: $text-color;
-  background: $background-color;
+  background: $gray-light;
 }
 
 .bs-callout h4 {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d5914b900e217ee5086876cc4a1f5ddf94801fdf..889366d6ddf496127817f0efaa597c5e36ac417a 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -10,7 +10,7 @@
 
 @mixin chevron-active {
   .fa-chevron-down {
-    color: $dropdown-toggle-hover-icon-color;
+    color: $gray-darkest;
   }
 }
 
@@ -28,14 +28,14 @@
   .dropdown-toggle,
   .dropdown-menu-toggle {
     @include chevron-active;
-    border-color: $dropdown-toggle-hover-border-color;
+    border-color: $gray-darkest;
   }
 }
 
 .dropdown-toggle {
   padding: 6px 8px 6px 10px;
-  background-color: $dropdown-toggle-bg;
-  color: $dropdown-toggle-color;
+  background-color: $white-light;
+  color: $gl-text-color;
   font-size: 14px;
   text-align: left;
   border: 1px solid $border-color;
@@ -73,7 +73,7 @@
   }
 
   .fa {
-    color: $dropdown-toggle-icon-color;
+    color: $gray-darkest;
   }
 
   .fa-chevron-down {
@@ -85,7 +85,7 @@
 
   &:hover {
     @include chevron-active;
-    border-color: $dropdown-toggle-hover-border-color;
+    border-color: $gray-darkest;
   }
 
   &:focus:active {
@@ -98,7 +98,7 @@
   @extend .dropdown-toggle;
   padding-right: 20px;
   position: relative;
-  width: 160px;
+  width: 163px;
   text-overflow: ellipsis;
   overflow: hidden;
 
@@ -131,7 +131,7 @@
   font-size: 14px;
   font-weight: normal;
   padding: 8px 0;
-  background-color: $dropdown-bg;
+  background-color: $white-light;
   border: 1px solid $dropdown-border-color;
   border-radius: $border-radius-base;
   box-shadow: 0 2px 4px $dropdown-shadow-color;
@@ -188,7 +188,6 @@
     &.is-focused {
       background-color: $dropdown-link-hover-bg;
       text-decoration: none;
-      outline: 0;
     }
 
     &.dropdown-menu-empty-link {
@@ -202,7 +201,7 @@
     }
 
     .icon-play {
-      fill: $table-text-gray;
+      fill: $gl-gray-light;
       margin-right: 6px;
       height: 12px;
       width: 11px;
@@ -210,7 +209,7 @@
   }
 
   .dropdown-header {
-    color: $dropdown-header-color;
+    color: $gl-gray-light;
     font-size: 13px;
     line-height: 22px;
     padding: 0 10px;
@@ -223,7 +222,7 @@
   .unclickable {
     cursor: not-allowed;
     padding: 5px 8px;
-    color: $dropdown-header-color;
+    color: $gl-gray-light;
   }
 }
 
@@ -602,14 +601,14 @@
 
   th {
     padding: 2px 0;
-    color: $calendar-header-color;
+    color: $note-disabled-comment-color;
     font-weight: normal;
     text-transform: lowercase;
     border-top: 1px solid $calendar-border-color;
   }
 
   .ui-datepicker-unselectable {
-    background-color: $calendar-unselectable-bg;
+    background-color: $gray-light;
   }
 }
 
@@ -621,11 +620,11 @@
 
 .dropdown-menu-inner-content {
   display: block;
-  color: $gl-placeholder-color;
+  color: $gl-gray-light;
 }
 
 .dropdown-toggle-text {
   &.is-default {
-    color: $gl-placeholder-color;
+    color: $gl-gray-light;
   }
 }
diff --git a/app/assets/stylesheets/pages/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
similarity index 99%
rename from app/assets/stylesheets/pages/emojis.scss
rename to app/assets/stylesheets/framework/emojis.scss
index f17797b23819706aa4b129e5e6afb253b9dbfe8f..7158de65143003079fef00eb00a77339c7a8230e 100644
--- a/app/assets/stylesheets/pages/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -1,4 +1,4 @@
-.emoji-0023-20E3 { background-position: 0 0px; }
+.emoji-0023-20E3 { background-position: 0 0; }
 .emoji-002A-20E3 { background-position: -20px 0; }
 .emoji-0030-20E3 { background-position: 0 -20px; }
 .emoji-0031-20E3 { background-position: -20px -20px; }
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index ab0b81f77f7569f3614a15489ce5fbc6b51abe0f..88ed0a4a17e055e2ec71e7cff381db1bad9a749c 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -19,7 +19,7 @@
 
   .file-title {
     position: relative;
-    background-color: $background-color;
+    background-color: $gray-light;
     border-bottom: 1px solid $border-color;
     margin: 0;
     text-align: left;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 25a2b38baaadf19bd244beee5c05f7b34aeeff20..940807fc39904da3cfefda0caa2cbeab2cb48efa 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -22,7 +22,7 @@ input[type='text'].danger {
   margin-top: 0;
   margin-bottom: -$gl-padding;
   padding: $gl-padding;
-  background-color: $background-color;
+  background-color: $gray-light;
   border-top: 1px solid $border-color;
 }
 
@@ -149,7 +149,7 @@ label {
 }
 
 .form-control::-webkit-input-placeholder {
-  color: $gl-placeholder-color;
+  color: $gl-gray-light;
 }
 
 .input-group {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index cc2286038c01caf6236e4d6743119e434aeec21f..971940773f73b57bb06d5ad7f77a47fdbd90d8b6 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -9,7 +9,7 @@ header {
   &.navbar-empty {
     height: $header-height;
     background: $white-light;
-    border-bottom: 1px solid $btn-gray-hover;
+    border-bottom: 1px solid $white-normal;
 
     .center-logo {
       margin: 8px 0;
@@ -27,7 +27,7 @@ header {
     z-index: 100;
     margin-bottom: 0;
     height: $header-height;
-    background-color: $background-color;
+    background-color: $gray-light;
     border: none;
     border-bottom: 1px solid $border-color;
 
@@ -45,7 +45,7 @@ header {
       padding: 0;
 
       .nav > li > a {
-        color: $gl-icon-color;
+        color: $gl-gray-light;
         font-size: 18px;
         padding: 0;
         margin: ($header-height - 28) / 2 0;
@@ -62,8 +62,8 @@ header {
         &:hover,
         &:focus,
         &:active {
-          background-color: $background-color;
-          color: darken($gl-icon-color, 30%);
+          background-color: $gray-light;
+          color: darken($gl-gray-light, 30%);
 
           .todos-pending-count {
             background: darken($todo-alert-blue, 10%);
@@ -84,11 +84,11 @@ header {
         padding: 6px 10px;
 
         &:hover {
-          background-color: $btn-gray-hover;
+          background-color: $white-normal;
         }
 
         &.active {
-          color: $gl-icon-color;
+          color: $gl-gray-light;
         }
       }
     }
@@ -100,10 +100,10 @@ header {
       font-size: 18px;
       padding: 6px 10px;
       border: none;
-      background-color: $background-color;
+      background-color: $gray-light;
 
       &:hover {
-        background-color: $btn-gray-hover;
+        background-color: $white-normal;
       }
     }
   }
diff --git a/app/assets/stylesheets/pages/icons.scss b/app/assets/stylesheets/framework/icons.scss
similarity index 100%
rename from app/assets/stylesheets/pages/icons.scss
rename to app/assets/stylesheets/framework/icons.scss
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index 878f44116ba11ab6653740144ba853efd6f37b62..09a569ad415c45ee4960f95b571bbd6f40d5be83 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -4,7 +4,7 @@
 }
 
 .appearance-light-logo-preview {
-  background-color: $background-color;
+  background-color: $gray-light;
   max-width: 72px;
   padding: 10px;
   margin-bottom: 10px;
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 44834a84234a7b39909e2561c0d38b09227c26fb..298913108ee7e6f5ee9219fcd78fe7bc3d1bff93 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -41,6 +41,6 @@
   }
 
   &.status-box-upcoming {
-    background: $issue-box-upcoming-bg;
+    background: $gl-gray-light;
   }
 }
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index dfaf2f7f1d369857b9eadc8822db627db85f2a7d..59fae61a44fcc0ed7c4093bf633a29ecd65685cc 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -26,6 +26,45 @@ body {
 
 .container-limited {
   max-width: $fixed-layout-width;
+
+  &.limit-container-width {
+    max-width: $limited-layout-width;
+  }
+}
+
+.alert-wrapper {
+  margin-bottom: $gl-padding;
+
+  .alert {
+    margin-bottom: 0;
+  }
+
+  /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
+  .alert-warning {
+    transition: background-color 0.15s, border-color 0.15s;
+    background-color: lighten($gl-warning, 4%);
+    border-color: lighten($gl-warning, 4%);
+  }
+
+  .alert-warning + .alert-warning {
+    background-color: $gl-warning;
+    border-color: $gl-warning;
+  }
+
+  .alert-warning + .alert-warning + .alert-warning {
+    background-color: darken($gl-warning, 4%);
+    border-color: darken($gl-warning, 4%);
+  }
+
+  .alert-warning + .alert-warning + .alert-warning + .alert-warning {
+    background-color: darken($gl-warning, 8%);
+    border-color: darken($gl-warning, 8%);
+  }
+
+  .alert-warning:only-of-type {
+    background-color: $gl-warning;
+    border-color: $gl-warning;
+  }
 }
 
 
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index ed4b60faf92c88cf58ff3fe508bb07d1a50b20dc..e96cd671e3470227359d66e5962c510e5055794c 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -36,7 +36,7 @@
       color: $list-warning-row-color;
     }
 
-    &.smoke { background-color: $background-color; }
+    &.smoke { background-color: $gray-light; }
 
     &:not(.ui-sort-disabled):hover {
       background: $row-hover;
@@ -46,7 +46,7 @@
       border-bottom: none;
 
       &.bottom {
-        background: $background-color;
+        background: $gray-light;
       }
     }
 
@@ -59,7 +59,7 @@
     p {
       padding-top: 1px;
       margin: 0;
-      color: $gray-dark;
+      color: $white-normal;
 
       img {
         position: relative;
@@ -113,7 +113,7 @@ ul.content-list {
   padding: 0;
 
   li {
-    border-color: $table-border-color;
+    border-color: $white-normal;
     font-size: $list-font-size;
     color: $list-text-color;
 
@@ -186,7 +186,7 @@ ul.content-list {
 
     &.list-placeholder {
       background-color: $gray-light;
-      border: dotted 1px $gray-dark;
+      border: dotted 1px $white-normal;
       margin: 1px 0;
       min-height: 52px;
     }
@@ -224,7 +224,7 @@ ul.content-list {
   }
 
   .label-default {
-    color: $btn-transparent-color;
+    color: $gl-gray-light;
   }
 }
 
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 59a30d31ac7d22c2ebaae20aa8c7d8b9de1a7956..e30d81d09f093e7057348f711370fb0fcb80264d 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -114,7 +114,7 @@
 
   // Border around images in issue and MR comments.
   img:not(.emoji) {
-    border: 1px solid $table-border-gray;
+    border: 1px solid $white-normal;
     padding: 5px;
     margin: 5px 0;
     // Ensure that image does not exceed viewport
@@ -135,7 +135,7 @@
 .toolbar-btn {
   float: left;
   padding: 0 5px;
-  color: $note-toolbar-color;
+  color: $gl-gray-light;
   background: transparent;
   border: 0;
   outline: 0;
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea77348633df5312fc15e0f877913d4dac0b2672..e4affbb1be1dd0d349d35d9f4c1ef9f7ae976870 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -51,7 +51,7 @@
       margin-bottom: -1px;
       font-size: 14px;
       line-height: 28px;
-      color: $note-toolbar-color;
+      color: $gl-gray-light;
       border-bottom: 2px solid transparent;
 
       &:hover,
@@ -80,17 +80,17 @@
     .badge {
       font-weight: normal;
       background-color: $nav-badge-bg;
-      color: $btn-transparent-color;
+      color: $gl-gray-light;
       vertical-align: baseline;
     }
   }
 
   &.sub-nav {
     text-align: center;
-    background-color: $dark-background-color;
+    background-color: $gray-normal;
 
     .container-fluid {
-      background-color: $dark-background-color;
+      background-color: $gray-normal;
       margin-bottom: 0;
     }
 
@@ -117,7 +117,7 @@
 
 .top-area {
   @include clearfix;
-  border-bottom: 1px solid $btn-gray-hover;
+  border-bottom: 1px solid $white-normal;
 
   .nav-text {
     padding-top: 16px;
@@ -289,7 +289,7 @@
   top: $header-height;
   width: 100%;
   z-index: 11;
-  background: $background-color;
+  background: $gray-light;
   border-bottom: 1px solid $border-color;
   transition: padding $sidebar-transition-duration;
   text-align: center;
@@ -317,7 +317,7 @@
 
     .fa-caret-down {
       margin-left: 5px;
-      color: $gl-icon-color;
+      color: $gl-gray-light;
     }
 
     .dropdown {
@@ -352,7 +352,7 @@
   }
 
   .fade-right {
-    @include fade(left, $background-color);
+    @include fade(left, $gray-light);
     right: -5px;
 
     .fa {
@@ -361,7 +361,7 @@
   }
 
   .fade-left {
-    @include fade(right, $background-color);
+    @include fade(right, $gray-light);
     left: -5px;
 
     .fa {
@@ -372,7 +372,7 @@
   &.sub-nav-scroll {
 
     .fade-right {
-      @include fade(left, $dark-background-color);
+      @include fade(left, $gray-normal);
       right: 0;
 
       .fa {
@@ -381,7 +381,7 @@
     }
 
     .fade-left {
-      @include fade(right, $dark-background-color);
+      @include fade(right, $gray-normal);
       left: 0;
 
       .fa {
diff --git a/app/assets/stylesheets/framework/page-header.scss b/app/assets/stylesheets/framework/page-header.scss
index 85c1385d5d93d507670475528f3027c5245a563c..fff7d7f7524c7d1c28060fce86a0b665628a21d3 100644
--- a/app/assets/stylesheets/framework/page-header.scss
+++ b/app/assets/stylesheets/framework/page-header.scss
@@ -14,7 +14,7 @@
 
   .header-action-buttons {
     i {
-      color: $gl-icon-color;
+      color: $gl-gray-light;
       font-size: 13px;
       margin-right: 3px;
     }
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index fde1431b13e42147742d419636c977e514958dde..9ab17e67d4c542c6a8137c6af128c37b2344a7d0 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -39,7 +39,7 @@
     }
 
     &:hover {
-      background-color: $gray-dark;
+      background-color: $white-normal;
       border-color: $border-white-normal;
       color: $gl-text-color;
     }
@@ -108,7 +108,7 @@
       border-color: $input-border;
       color: $gl-text-color;
       line-height: 15px;
-      background-color: $background-color;
+      background-color: $gray-light;
       background-image: none;
 
       .select2-search-choice-close {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 0aa609b8dd55b660e189a8e396b524a8a9bfb135..46a06cd7eabefeae9c01685c14ab9ae39f5ff610 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -23,7 +23,7 @@
 
 .sidebar-wrapper {
   z-index: 1000;
-  background: $background-color;
+  background: $gray-light;
 
   .nicescroll-rails-hr {
     // TODO: Figure out why nicescroll doesn't hide horizontal bar
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
similarity index 77%
rename from app/assets/stylesheets/pages/snippets.scss
rename to app/assets/stylesheets/framework/snippets.scss
index ff13b86acf0da338756afe9fc8b728b12aaf211c..5f7e1b17cc700a0eb7f22b7c4621b51f65e84fc2 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -22,16 +22,6 @@
 
 .snippet-file-content {
   border-radius: 3px;
-  margin-bottom: $gl-padding;
-
-  .btn-clipboard {
-    @extend .btn;
-  }
-}
-
-.project-snippets .awards {
-  border-bottom: 1px solid $table-border-color;
-  padding-bottom: $gl-padding;
 }
 
 .snippet-header {
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 5d0ca63ea08e853bb65689f8902d49c24723276b..6d9fa74a030f646acf7179d338197d7cce6564a7 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -31,7 +31,7 @@ table {
       }
 
       th {
-        background-color: $background-color;
+        background-color: $gray-light;
         font-weight: normal;
         border-bottom: none;
 
@@ -41,7 +41,7 @@ table {
       }
 
       td {
-        border-color: $table-border-color;
+        border-color: $white-normal;
       }
     }
   }
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index 875cded8b4e7adb0b091b9ef6bc8228508725487..6078505807eef9f6d1da3b3e0f19b41598a9b45d 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -6,7 +6,7 @@
 
   .timeline-entry {
     padding: $gl-padding $gl-btn-padding 11px;
-    border-color: $table-border-color;
+    border-color: $white-normal;
     color: $gl-gray;
     border-bottom: 1px solid $border-white-light;
 
diff --git a/app/assets/stylesheets/framework/tw_bootstrap.scss b/app/assets/stylesheets/framework/tw_bootstrap.scss
index 55bc325b858c35c251b211006cdda4135ce48771..718dbbfea27825623eb7af42c017ff59b94b3f29 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap.scss
@@ -33,7 +33,7 @@
 @import "bootstrap/labels";
 @import "bootstrap/badges";
 @import "bootstrap/alerts";
-@import "bootstrap/progress-bars";
+// @import "bootstrap/progress-bars";
 @import "bootstrap/list-group";
 @import "bootstrap/wells";
 @import "bootstrap/close";
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index c731a8f222f8d0fbf076d88adddef081c980c05c..876adf7f71204eaa541a17e81f1e423d8505939c 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -78,7 +78,7 @@ $pagination-active-bg: $white-light;
 $pagination-active-border: $border-color;
 
 $pagination-disabled-color: #cdcdcd;
-$pagination-disabled-bg: $background-color;
+$pagination-disabled-bg: $gray-light;
 $pagination-disabled-border: $border-color;
 
 
@@ -117,8 +117,8 @@ $alert-border-radius: 0;
 $panel-border-radius: 2px;
 $panel-default-text: $text-color;
 $panel-default-border: $border-color;
-$panel-default-heading-bg: $background-color;
-$panel-footer-bg: $background-color;
+$panel-default-heading-bg: $gray-light;
+$panel-footer-bg: $gray-light;
 $panel-inner-border: $border-color;
 
 //== Wells
@@ -153,8 +153,8 @@ $nav-link-padding: 13px $gl-padding;
 //== Code
 //
 //##
-$pre-bg: $background-color !default;
+$pre-bg: $gray-light !default;
 $pre-color: $gl-gray !default;
 $pre-border-color: $border-color;
 
-$table-bg-accent: $background-color;
+$table-bg-accent: $gray-light;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index a1d5f6427f4365628604da107cb7b013c6224b3c..d0c27d642399958615a63855de89208de5331758 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -17,16 +17,15 @@ $darken-dark-factor: 10%;
 $darken-border-factor: 5%;
 
 $white-light: #fff;
-$white-normal: darken($white-light, $darken-normal-factor);
-$white-dark: darken($white-light, $darken-dark-factor);
+$white-normal: #f0f0f0;
+$white-dark: #eaeaea;
 
 $gray-lightest: #fdfdfd;
 $gray-light: #fafafa;
 $gray-lighter: #f9f9f9;
-$gray-normal: darken($gray-light, $darken-normal-factor);
-$gray-dark: darken($gray-light, $darken-dark-factor);
+$gray-normal: #f5f5f5;
 $gray-darker: #eee;
-$gray-darkest: #c9c9c9;
+$gray-darkest: #c4c4c4;
 
 $green-light: #3cbd70;
 $green-normal: darken($green-light, $darken-normal-factor);
@@ -55,11 +54,9 @@ $black-transparent: rgba(0, 0, 0, 0.3);
 
 $border-white-light: darken($white-light, $darken-border-factor);
 $border-white-normal: darken($white-normal, $darken-border-factor);
-$border-white-dark: darken($white-dark, $darken-border-factor);
 
-$border-gray-light: darken($gray-light, $darken-border-factor);
 $border-gray-normal: darken($gray-normal, $darken-border-factor);
-$border-gray-dark: darken($gray-dark, $darken-border-factor);
+$border-gray-dark: darken($white-normal, $darken-border-factor);
 
 $border-green-extra-light: #9adb84;
 $border-green-light: darken($green-light, $darken-border-factor);
@@ -78,9 +75,6 @@ $border-red-light: darken($red-light, $darken-border-factor);
 $border-red-normal: darken($red-normal, $darken-border-factor);
 $border-red-dark: darken($red-dark, $darken-border-factor);
 
-$help-well-bg: $gray-light;
-$help-well-border: #e5e5e5;
-
 $warning-message-bg: #fbf2d9;
 $warning-message-color: #9e8e60;
 $warning-message-border: #f0e2bb;
@@ -90,10 +84,6 @@ $warning-message-border: #f0e2bb;
  */
 $border-color: #e5e5e5;
 $focus-border-color: #3aabf0;
-$table-border-color: #f0f0f0;
-$background-color: $gray-light;
-$dark-background-color: #f5f5f5;
-$table-text-gray: #8f8f8f;
 $well-expand-item: #e8f2f7;
 $well-inner-border: #eef0f2;
 $well-light-border: #f1f1f1;
@@ -113,12 +103,10 @@ $gl-text-orange: #d90;
 $gl-link-color: #3777b0;
 $gl-diff-text-color: #555;
 $gl-dark-link-color: #333;
-$gl-placeholder-color: #8f8f8f;
-$gl-icon-color: $gl-placeholder-color;
+$gl-gray-light: #8f8f8f;
 $gl-grayish-blue: #7f8fa4;
 $gl-gray: $gl-text-color;
 $gl-gray-dark: #313236;
-$gl-gray-light: $gl-placeholder-color;
 $gl-header-color: #4c4e54;
 
 /*
@@ -166,11 +154,11 @@ $row-hover-border: #b2d7ff;
 $progress-color: #c0392b;
 $header-height: 50px;
 $fixed-layout-width: 1280px;
+$limited-layout-width: 990px;
+$gl-avatar-size: 40px;
 $error-exclamation-point: #e62958;
 $border-radius-default: 2px;
-$btn-transparent-color: #8f8f8f;
 $settings-icon-size: 18px;
-$provider-btn-group-border: #e5e5e5;
 $provider-btn-not-active-color: #4688f1;
 $link-underline-blue: #4a8bee;
 $active-item-blue: #4a8bee;
@@ -193,7 +181,6 @@ $count-arrow-border: #dce0e5;
 $save-project-loader-color: #555;
 $divergence-graph-bar-bg: #ccc;
 $divergence-graph-separator-bg: #ccc;
-$issue-box-upcoming-bg: #8f8f8f;
 
 /*
 * Common component specific colors
@@ -246,8 +233,6 @@ $line-removed-dark: #fac5cd;
 $line-number-old: #f9d7dc;
 $line-number-new: #ddfbe6;
 $line-number-select: #fbf2da;
-$match-line: $gray-light;
-$table-border-gray: #f0f0f0;
 $line-target-blue: #f6faff;
 $line-select-yellow: #fcf8e7;
 $line-select-yellow-dark: #f0e2bd;
@@ -257,7 +242,6 @@ $file-mode-changed: #777;
 $file-mode-changed: #777;
 $diff-image-bg: #ddd;
 $diff-image-info-color: grey;
-$diff-image-img-bg: #e5e5e5;
 $diff-swipe-border: #999;
 $diff-view-modes-color: grey;
 $diff-view-modes-border: #c1c1c1;
@@ -272,14 +256,12 @@ $regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-San
 * Dropdowns
 */
 $dropdown-width: 300px;
-$dropdown-bg: #fff;
 $dropdown-link-color: #555;
 $dropdown-link-hover-bg: $row-hover;
 $dropdown-empty-row-bg: rgba(#000, .04);
 $dropdown-border-color: $border-color;
 $dropdown-shadow-color: rgba(#000, .1);
 $dropdown-divider-color: rgba(#000, .1);
-$dropdown-header-color: #959494;
 $dropdown-title-btn-color: #bfbfbf;
 $dropdown-input-color: #555;
 $dropdown-input-fa-color: #c7c7c7;
@@ -287,31 +269,19 @@ $dropdown-input-focus-border: $focus-border-color;
 $dropdown-input-focus-shadow: rgba($dropdown-input-focus-border, .4);
 $dropdown-loading-bg: rgba(#fff, .6);
 $dropdown-chevron-size: 10px;
+$dropdown-toggle-active-border-color: darken($border-color, 14%);
 
-$dropdown-toggle-bg: #fff;
-$dropdown-toggle-color: #5c5c5c;
-$dropdown-toggle-border-color: #e5e5e5;
-$dropdown-toggle-hover-border-color: darken($dropdown-toggle-border-color, 13%);
-$dropdown-toggle-active-border-color: darken($dropdown-toggle-border-color, 14%);
-$dropdown-toggle-icon-color: #c4c4c4;
-$dropdown-toggle-hover-icon-color: darken($dropdown-toggle-icon-color, 7%);
 
 /*
 * Buttons
 */
 $btn-active-gray: #ececec;
 $btn-active-gray-light: e4e7ed;
-$btn-placeholder-gray: #c7c7c7;
-$btn-white-active: #848484;
-$btn-gray-hover: #eee;
 
 /*
  *  Award emoji
  */
-$award-emoji-menu-bg: #fff;
-$award-emoji-menu-border: #f1f2f4;
 $award-emoji-menu-shadow: rgba(0,0,0,.175);
-$award-emoji-new-btn-icon-color: #dcdcdc;
 
 /*
  * Search Box
@@ -319,22 +289,15 @@ $award-emoji-new-btn-icon-color: #dcdcdc;
 $search-input-border-color: rgba(#4688f1, .8);
 $search-input-focus-shadow-color: $dropdown-input-focus-shadow;
 $search-input-width: 220px;
-$location-badge-color: #aaa;
-$location-badge-bg: $dark-background-color;
 $location-badge-active-bg: #4f91f8;
 $location-icon-color: #e7e9ed;
-$location-icon-active-color: #807e7e;
 
 /*
  *  Notes
  */
 $notes-light-color: #8e8e8e;
-$notes-action-color: #c3c3c3;
 $notes-role-color: #8e8e8e;
-$notes-role-border-color: #e4e4e4;
 $note-disabled-comment-color: #b2b2b2;
-$note-form-border-color: #e5e5e5;
-$note-toolbar-color: #959494;
 $note-targe3-outside: #fffff0;
 $note-targe3-inside: #ffffd3;
 $note-line2-border: #ddd;
@@ -344,15 +307,12 @@ $note-line2-border: #ddd;
 * Zen
 */
 $zen-control-color: #555;
-$zen-control-hover-color: #111;
 
 /*
 * Calendar
 */
-$calendar-header-color: #b8b8b8;
 $calendar-hover-bg: #ecf3fe;
 $calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: $gray-light;
 $calendar-user-contrib-text: #959494;
 
 /*
@@ -365,16 +325,9 @@ $cycle-analytics-dark-text: $gl-title-color;
 $cycle-analytics-light-gray: #bfbfbf;
 $cycle-analytics-dismiss-icon-color: #b2b2b2;
 
-/*
- * Personal Access Tokens
- */
-$personal-access-tokens-disabled-label-color: #bbb;
-
 /*
 * CI
 */
-$ci-output-bg: #1d1f21;
-$ci-text-color: #c5c8c6;
 $ci-skipped-color: #888;
 
 /*
@@ -470,7 +423,6 @@ $help-shortcut-header-color: #333;
 /*
 * Issues
 */
-$issues-border: #e5e5e5;
 $issues-today-bg: #f3fff2;
 $issues-today-border: #e1e8d5;
 
@@ -486,7 +438,7 @@ $jq-ui-default-color: #777;
 $label-gray-bg: #f8fafc;
 $label-inverse-bg: #333;
 $label-remove-border: rgba(0, 0, 0, .1);
-$label-border-radius: 14px;
+$label-border-radius: 100px;
 
 /*
 * Lint
@@ -522,7 +474,6 @@ $project-option-descr-color: #54565b;
 $project-breadcrumb-color: #999;
 $project-private-forks-notice-odd: #2aa056;
 $project-network-controls-color: #888;
-$project-limit-message-bg: #f28d35;
 
 /*
 * Runners
@@ -572,3 +523,9 @@ $body-text-shadow: rgba(255,255,255,0.01);
 */
 $ui-dev-kit-example-color: #bbb;
 $ui-dev-kit-example-border: #ddd;
+
+/*
+Pipeline Graph
+*/
+$stage-hover-bg: #eaf3fc;
+$stage-hover-border: #d1e7fc;
diff --git a/app/assets/stylesheets/framework/wells.scss b/app/assets/stylesheets/framework/wells.scss
index f2860dfe84d3194716136667429c7686085d7164..f9c850ebdc82de384d4aae45d85c3ac8bb7e8232 100644
--- a/app/assets/stylesheets/framework/wells.scss
+++ b/app/assets/stylesheets/framework/wells.scss
@@ -1,5 +1,5 @@
 .info-well {
-  background: $background-color;
+  background: $gray-light;
   color: $gl-gray;
   border: 1px solid $border-color;
   border-radius: $border-radius-default;
@@ -45,7 +45,7 @@
 }
 
 .light-well {
-  background-color: $background-color;
+  background-color: $gray-light;
   padding: 15px;
 }
 
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index e5c7d70d45a098b971f4380e76b93ec1b1873e99..84b639fabf51f1c70e4e056ed1540a706d6287d3 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -40,7 +40,7 @@
 }
 
 .zen-control-full {
-  color: $note-toolbar-color;
+  color: $gl-gray-light;
 
   &:hover {
     color: $gl-link-color;
@@ -57,6 +57,6 @@
   font-size: 36px;
 
   &:hover {
-    color: $zen-control-hover-color;
+    color: $black;
   }
 }
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index 1adab3ffd94f1d82635a7b48622f938e4c19abfa..54a5664a8741f4442fb69e209c01dac402e1cd9e 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -69,14 +69,14 @@ $white-gc-bg: #eaf2f5;
 
 @mixin matchLine {
   color: $black-transparent;
-  background-color: $match-line;
+  background-color: $gray-light;
 }
 
 .code.white {
   // Line numbers
   .line-numbers,
   .diff-line-num {
-    background-color: $background-color;
+    background-color: $gray-light;
   }
 
   .diff-line-num,
@@ -87,7 +87,7 @@ $white-gc-bg: #eaf2f5;
   // Code itself
   pre.code,
   .diff-line-num {
-    border-color: $table-border-gray;
+    border-color: $white-normal;
   }
 
   &,
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index 024b4df6bd09bcc28f8fe536cd5a714041dac715..60ff72c703e2309caf64da5f229b2084ee21828a 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -91,9 +91,9 @@ $highlighted-gc-bg: #eaf2f5;
   padding: 0 5px;
   text-align: right;
   width: 35px;
-  background-color: $background-color;
+  background-color: $gray-light;
   color: $black-transparent;
-  border-right: 1px solid $table-border-gray;
+  border-right: 1px solid $white-normal;
 
   &.old {
     background-color: $line-number-old;
@@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5;
 
   &.match {
     color: $black-transparent;
-    background-color: $match-line;
+    background-color: $gray-light;
   }
 }
 
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 0d9cf679e7c6459b3b65cf9b50440d0ebaf55016..c735f104c20e637965f5626fd491c2c0cdbdf7f8 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -98,7 +98,7 @@
 .board-inner {
   height: 100%;
   font-size: $issue-boards-font-size;
-  background: $background-color;
+  background: $gray-light;
   border: 1px solid $border-color;
   border-radius: $border-radius-default;
 }
@@ -253,7 +253,7 @@
 
 .board-list-count {
   padding: 10px 0;
-  color: $gl-placeholder-color;
+  color: $gl-gray-light;
   font-size: 13px;
 
   > .fa {
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index dcc13f6d74abe72a3c5d8261000fb9d4e50a0dbe..66f7e7f97c8414351462c439f639a2b6a85f8d54 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -42,7 +42,7 @@
   }
 
   .environment-information {
-    background-color: $background-color;
+    background-color: $gray-light;
     border: 1px solid $border-color;
     padding: 12px $gl-padding;
     border-radius: $border-radius-default;
@@ -96,8 +96,8 @@
 }
 
 .build-trace {
-  background: $ci-output-bg;
-  color: $ci-text-color;
+  background: $black;
+  color: $gray-darkest;
   white-space: pre;
   overflow-x: auto;
   font-size: 12px;
@@ -257,7 +257,7 @@
 }
 
 .build-light-text {
-  color: $gl-placeholder-color;
+  color: $gl-gray-light;
 }
 
 .build-gutter-toggle {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index c29b5fdea78a33dfe067fbd7da48f6be17a0f306..e76e1a73b2526663b5078783cdd74dcdfac78226 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -7,7 +7,7 @@
 
 .commit-header {
   padding: 5px 10px;
-  background-color: $background-color;
+  background-color: $gray-light;
   border-top: 1px solid $gray-darker;
   border-bottom: 1px solid $gray-darker;
   font-size: 14px;
@@ -38,7 +38,7 @@
 .text-expander {
   display: inline-block;
   background: $gray-light;
-  color: $gl-placeholder-color;
+  color: $gl-gray-light;
   padding: 0 5px;
   cursor: pointer;
   border: 1px solid $border-gray-dark;
@@ -117,7 +117,7 @@
 
   .commit-row-description {
     font-size: 14px;
-    border-left: 1px solid $btn-gray-hover;
+    border-left: 1px solid $white-normal;
     padding: 10px 15px;
     margin: 10px 0;
     background: $gray-light;
@@ -174,7 +174,7 @@
       height: 14px;
       width: 14px;
       vertical-align: middle;
-      fill: $table-text-gray;
+      fill: $gl-gray-light;
     }
   }
 
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 57146e1fccdc4d0810eb0db4733dc685aaf869c4..5aef31724e166065c5d3cfdc396a3e4374d7f8e1 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -232,6 +232,7 @@
       &:hover:not(.active) {
         background-color: $gray-lightest;
         box-shadow: inset 2px 0 0 0 $border-color;
+        cursor: pointer;
       }
 
       &:first-child {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 737f6e0f4bed212e0ba9dffe8af1904147c03269..f30795fd2c28fd1bb0b9b89c28db5dac522c96a3 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -11,7 +11,7 @@
 
   .diff-header {
     position: relative;
-    background: $background-color;
+    background: $gray-light;
     border-bottom: 1px solid $border-color;
     padding: 10px 16px;
     color: $gl-diff-text-color;
@@ -38,7 +38,7 @@
     cursor: pointer;
 
     &:hover {
-      background-color: $dark-background-color;
+      background-color: $gray-normal;
     }
 
     .diff-toggle-caret {
@@ -187,8 +187,8 @@
 
       img {
         border: 1px solid $white-light;
-        background-image: linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%),
-        linear-gradient(45deg, $diff-image-img-bg 25%, transparent 25%, transparent 75%, $diff-image-img-bg 75%, $diff-image-img-bg 100%);
+        background-image: linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%),
+        linear-gradient(45deg, $border-color 25%, transparent 25%, transparent 75%, $border-color 75%, $border-color 100%);
         background-size: 10px 10px;
         background-position: 0 0, 5px 5px;
         max-width: 100%;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 6cde9c592de67b9f04b6ee3f2d591cc9ba50f4bc..4af267403d8841c77af6b1a5bc55d778be399d18 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -10,7 +10,7 @@
   }
 
   .ace_gutter-cell {
-    background-color: $background-color;
+    background-color: $gray-light;
   }
 
   .cancel-btn {
@@ -34,7 +34,7 @@
   }
 
   .editor-ref {
-    background: $background-color;
+    background: $gray-light;
     padding-right: $gl-padding;
     border-right: 1px solid $border-color;
     display: block;
@@ -51,8 +51,16 @@
 
   .new-file-name {
     display: inline-block;
-    width: 450px;
+    max-width: 450px;
     float: left;
+
+    @media(max-width: $screen-md-max) {
+      width: 280px;
+    }
+
+    @media(max-width: $screen-sm-max) {
+      width: 180px;
+    }
   }
 
   .file-buttons {
@@ -67,7 +75,8 @@
   .soft-wrap-toggle,
   .license-selector,
   .gitignore-selector,
-  .gitlab-ci-yml-selector {
+  .gitlab-ci-yml-selector,
+  .dockerfile-selector {
     display: inline-block;
     vertical-align: top;
     font-family: $regular_font;
@@ -97,7 +106,8 @@
 
   .gitignore-selector,
   .license-selector,
-  .gitlab-ci-yml-selector {
+  .gitlab-ci-yml-selector,
+  .dockerfile-selector {
     .dropdown {
       line-height: 21px;
     }
@@ -114,3 +124,42 @@
     }
   }
 }
+
+@media(max-width: $screen-xs-max){
+  .file-editor {
+    .file-title {
+      .pull-right {
+        height: auto;
+      }
+    }
+
+    .new-file-name {
+      max-width: none;
+      width: 100%;
+      margin-bottom: 3px;
+    }
+
+    .file-buttons {
+      display: block;
+      width: 100%;
+      margin-bottom: 10px;
+
+      .soft-wrap-toggle {
+        width: 100%;
+        margin: 3px 0;
+      }
+
+      .encoding-selector,
+      .license-selector,
+      .gitignore-selector,
+      .gitlab-ci-yml-selector {
+        display: block;
+        margin: 3px 0;
+
+        button {
+          width: 100%;
+        }
+      }
+    }
+  }
+}
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index e716f24c8e295ace650af5940abbb5c9189e7446..3d60426de01e2c45f69cf0bfbbf92405c5019a7d 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -30,19 +30,25 @@
     display: table-cell;
   }
 
+  .environments-name,
   .environments-commit,
   .environments-actions {
     width: 20%;
   }
 
-  .environments-deploy,
-  .environments-build,
   .environments-date {
     width: 10%;
   }
 
-  .environments-name {
-    width: 30%;
+  .environments-deploy,
+  .environments-build {
+    width: 15%;
+  }
+
+  .environment-name,
+  .environments-build-cell,
+  .deployment-column {
+    word-break: break-all;
   }
 
   .deployment-column {
@@ -66,14 +72,14 @@
 
   .external-url,
   .dropdown-new {
-    color: $table-text-gray;
+    color: $gl-gray-light;
   }
 
   .dropdown-menu {
 
     .fa {
       margin-right: 6px;
-      color: $table-text-gray;
+      color: $gl-gray-light;
     }
   }
 
@@ -84,7 +90,7 @@
 
   .stop-env-link,
   .external-url {
-    color: $table-text-gray;
+    color: $gl-gray-light;
 
     .stop-env-icon {
       font-size: 14px;
@@ -119,7 +125,7 @@
     .badge {
       font-weight: normal;
       background-color: $gray-darker;
-      color: $gl-placeholder-color;
+      color: $gl-gray-light;
       vertical-align: baseline;
     }
   }
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index dc67d411c7197277cd4ed86df30575f5871daad9..98925c2d0cb81e2ddbca2afdb0c2d341078a8424 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -5,7 +5,7 @@
 .event-item {
   font-size: $gl-font-size;
   padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top);
-  border-bottom: 1px solid $table-border-color;
+  border-bottom: 1px solid $white-normal;
   color: $list-text-color;
 
   &.event-inline {
diff --git a/app/assets/stylesheets/pages/explore.scss b/app/assets/stylesheets/pages/explore.scss
deleted file mode 100644
index 9b92128624c1053661040c1b2c6f2abc4d295f2c..0000000000000000000000000000000000000000
--- a/app/assets/stylesheets/pages/explore.scss
+++ /dev/null
@@ -1,8 +0,0 @@
-.explore-title {
-  text-align: center;
-
-  h3 {
-    font-weight: normal;
-    font-size: 30px;
-  }
-}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index e2e644dc23bfb4bf6fc7176574d8c9079f9daf25..dae8ccdef6ca623efdd72cbf337fd390c5a0b722 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -60,7 +60,7 @@
 
   // Border around images in the help pages.
   img:not(.emoji) {
-    border: 1px solid $table-border-gray;
+    border: 1px solid $white-normal;
     padding: 5px;
     margin: 5px;
     max-height: calc(100vh - 100px);
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 407c0afbac8d4e66270ce3bf45e8db54ed032879..4fac0cfb0baff6774a732d84120bcf5393499d8c 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -1,3 +1,50 @@
+// Limit MR description for side-by-side diff view
+.limit-container-width {
+  .detail-page-header {
+    max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .issuable-details {
+    .detail-page-description,
+    .mr-source-target,
+    .mr-state-widget,
+    .merge-manually {
+      max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+      margin-left: auto;
+      margin-right: auto;
+    }
+
+    .merge-request-tabs-holder {
+      &.affix {
+        border-bottom: 1px solid $border-color;
+
+        .nav-links {
+          border: 0;
+        }
+      }
+
+      .container-fluid {
+        padding-left: 0;
+        padding-right: 0;
+        max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+        margin-left: auto;
+        margin-right: auto;
+      }
+    }
+  }
+
+  .diffs {
+    .mr-version-controls,
+    .files-changed {
+      max-width: calc(#{$limited-layout-width} - (#{$gl-padding} * 2));
+      margin-left: auto;
+      margin-right: auto;
+    }
+  }
+}
+
 .issuable-details {
   section {
     .issuable-discussion {
@@ -7,9 +54,8 @@
 
   // Border around images in issue and MR descriptions.
   .description img:not(.emoji) {
-    border: 1px solid $table-border-gray;
+    border: 1px solid $white-normal;
     padding: 5px;
-    margin: 5px;
     max-height: calc(100vh - 100px);
   }
 }
@@ -51,7 +97,7 @@
   .block {
     @include clearfix;
     padding: $gl-padding 0;
-    border-bottom: 1px solid $border-gray-light;
+    border-bottom: 1px solid $border-gray-normal;
     // This prevents the mess when resizing the sidebar
     // of elements repositioning themselves..
     width: $gutter_inner_width;
@@ -169,7 +215,7 @@
     }
 
     .no-value {
-      color: $gl-placeholder-color;
+      color: $gl-gray-light;
     }
 
     .sidebar-collapsed-icon {
@@ -178,7 +224,7 @@
 
     .gutter-toggle {
       margin-top: 7px;
-      border-left: 1px solid $border-gray-light;
+      border-left: 1px solid $border-gray-normal;
     }
 
     .assignee .avatar {
@@ -216,7 +262,7 @@
     }
 
     .participants {
-      border-bottom: 1px solid $border-gray-light;
+      border-bottom: 1px solid $border-gray-normal;
     }
 
     .hide-collapsed {
@@ -333,7 +379,7 @@
   margin-left: 5px;
 
   a {
-    color: $gl-placeholder-color;
+    color: $gl-gray-light;
   }
 }
 
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3b47f99df2c5db9450c890520cfc478f4c0b2880..8734a3b159838cc1fa0ed1ed87f4f24b10abc31c 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -88,12 +88,12 @@ ul.related-merge-requests > li {
 
   &.closed {
     background: $gray-light;
-    border-color: $issues-border;
+    border-color: $border-color;
   }
 
   &.merged {
     background: $gray-light;
-    border-color: $issues-border;
+    border-color: $border-color;
   }
 }
 
@@ -144,7 +144,7 @@ ul.related-merge-requests > li {
   }
 
   .btn {
-    background-color: $background-color;
-    border: 1px solid $border-gray-light;
+    background-color: $gray-light;
+    border: 1px solid $border-gray-normal;
   }
 }
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 25c91203ff48808ec8d677f749c4f45d168766ab..d129eb12a4515d38a10e332015b05de2a4072985 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -98,7 +98,7 @@
   }
 
   .label {
-    padding: 9px;
+    padding: 8px 9px 9px;
     font-size: 14px;
   }
 }
@@ -201,6 +201,8 @@
   .label-remove {
     border-left: 1px solid $label-remove-border;
     z-index: 3;
+    border-radius: $label-border-radius;
+    padding: 6px 10px 6px 9px;
   }
 
   .btn {
diff --git a/app/assets/stylesheets/pages/members.scss b/app/assets/stylesheets/pages/members.scss
index 5f3bbb40ba08a6e8c9dc516632d6843cdf88c1da..36ee5d17211abcae41d134fdadb86a7f46d6f006 100644
--- a/app/assets/stylesheets/pages/members.scss
+++ b/app/assets/stylesheets/pages/members.scss
@@ -78,6 +78,21 @@
     float: right;
   }
 
+  .dropdown {
+    width: 100%;
+    margin-top: 5px;
+
+    .dropdown-menu-toggle {
+      vertical-align: middle;
+      width: 100%;
+    }
+
+    @media (min-width: $screen-sm-min) {
+      margin-top: 0;
+      width: 155px;
+    }
+  }
+
   .form-control {
     width: 100%;
     padding-right: 35px;
@@ -85,12 +100,22 @@
     @media (min-width: $screen-sm-min) {
       width: 350px;
     }
+
+    &.input-short {
+      @media (min-width: $screen-md-min) {
+        width: 170px;
+      }
+
+      @media (min-width: $screen-lg-min) {
+        width: 210px;
+      }
+    }
   }
 }
 
 .member-search-btn {
   position: absolute;
-  right: 0;
+  right: 4px;
   top: 0;
   height: 35px;
   padding-left: 10px;
@@ -99,4 +124,8 @@
   background: transparent;
   border: 0;
   outline: 0;
+
+  @media (min-width: $screen-sm-min) {
+    right: 160px;
+  }
 }
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 7a90713dd3f62ce1d2beef58d86ce863c69e22f2..5a9f199fb34ba4940db0056341b83f05409a5fb9 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -274,7 +274,7 @@ $colors: (
   }
 
   .discard-changes-alert {
-    background-color: $background-color;
+    background-color: $gray-light;
     text-align: right;
     padding: $gl-padding-top $gl-padding;
     color: $gl-text-color;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 6234779ac1913b6295ff7cad101c2fa56db690b4..e779e65eca3d50e000a8935cc91c2f669de587bd 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,7 +3,7 @@
  *
  */
 .mr-state-widget {
-  background: $background-color;
+  background: $gray-light;
   color: $gl-gray;
   border: 1px solid $border-color;
   border-radius: 2px;
@@ -359,7 +359,7 @@
 
     th {
       background-color: $white-light;
-      color: $gl-placeholder-color;
+      color: $gl-gray-light;
     }
   }
 }
@@ -375,7 +375,7 @@
 }
 
 .mr-version-controls {
-  background: $background-color;
+  background: $gray-light;
   border-bottom: 1px solid $border-color;
   color: $gl-text-color;
 
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index dfc6079bd151df951b5bc9034bcf90bcedae5992..77c523d731047b2ea2222f3ec6ed90a423069d23 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -108,7 +108,7 @@
     margin-top: 7px;
 
     .issuable-number {
-      color: $gl-placeholder-color;
+      color: $gl-gray-light;
       margin-right: 5px;
     }
 
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c35d71f9e7ba887b60b5002f8619215f7229c61a..074abec769268c69598537e2f626046b189f98a2 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -62,7 +62,7 @@
 .common-note-form {
   .md-area {
     padding: $gl-padding-top $gl-padding;
-    border: 1px solid $note-form-border-color;
+    border: 1px solid $border-color;
     border-radius: $border-radius-base;
     transition: border-color ease-in-out 0.15s,
                 box-shadow ease-in-out 0.15s;
@@ -204,7 +204,7 @@
 
 .comment-toolbar {
   padding-top: $gl-padding-top;
-  color: $note-toolbar-color;
+  color: $gl-gray-light;
   border-top: 1px solid $border-color;
 }
 
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 10eb3d4203ee9e2d30f5dbc2f22038c92b73ebd5..106c5d4d3903a089179c449e04774cc710667f5f 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -166,7 +166,7 @@ ul.notes {
   .note {
     display: block;
     position: relative;
-    border-bottom: 1px solid $table-border-gray;
+    border-bottom: 1px solid $white-normal;
 
     &.note-discussion {
       &.timeline-entry {
@@ -291,14 +291,14 @@ ul.notes {
   font-family: $regular_font;
 
   td {
-    border: 1px solid $table-border-gray;
+    border: 1px solid $white-normal;
     border-left: none;
 
     &.notes_line {
       vertical-align: middle;
       text-align: center;
       padding: 10px 0;
-      background: $background-color;
+      background: $gray-light;
       color: $text-color;
     }
 
@@ -309,7 +309,7 @@ ul.notes {
     }
 
     &.notes_content {
-      background-color: $background-color;
+      background-color: $gray-light;
       border-width: 1px 0;
       padding: 0;
       vertical-align: top;
@@ -372,7 +372,7 @@ ul.notes {
 .note-actions {
   float: right;
   margin-left: 10px;
-  color: $notes-action-color;
+  color: $gray-darkest;
 }
 
 .note-actions {
@@ -383,10 +383,6 @@ ul.notes {
   .note-action-button {
     margin-left: 10px;
   }
-
-  @media (min-width: $screen-sm-min) {
-    position: relative;
-  }
 }
 
 .discussion-actions {
@@ -411,7 +407,7 @@ ul.notes {
   }
 
   .fa {
-    color: $notes-action-color;
+    color: $gray-darkest;
     position: relative;
     font-size: 17px;
   }
@@ -448,7 +444,7 @@ ul.notes {
   color: $notes-role-color;
   font-size: 12px;
   line-height: 20px;
-  border: 1px solid $notes-role-border-color;
+  border: 1px solid $border-color;
   border-radius: $border-radius-base;
 }
 
@@ -529,7 +525,7 @@ ul.notes {
 .line-resolve-all {
   display: inline-block;
   padding: 5px 10px;
-  background-color: $background-color;
+  background-color: $gray-light;
   border: 1px solid $border-color;
   border-radius: $border-radius-default;
 
@@ -573,10 +569,10 @@ ul.notes {
 
   svg {
     position: relative;
-    color: $notes-action-color;
+    color: $gray-darkest;
 
     path {
-      fill: $notes-action-color;
+      fill: $gray-darkest;
     }
   }
 }
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 7d61390a439915cebad2313c3a223864d77632ed..bdf07a99dafb3d03999df68a2d5c72651b17ec59 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -10,19 +10,7 @@
   position: relative;
   top: 1px;
 
-  > .fa {
+  .fa {
     font-size: 18px;
   }
 }
-
-.ns-part {
-  color: $gl-text-green;
-}
-
-.ns-watch {
-  color: $gl-success;
-}
-
-.ns-mute {
-  color: $gl-danger;
-}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 6822f916cc5b7b882ae7becf928d918895ef532a..be22e7bdc79d78c3ce0b4e5618bb2bd4ab6e641e 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -118,7 +118,7 @@
       height: 14px;
       width: 14px;
       vertical-align: middle;
-      fill: $table-text-gray;
+      fill: $gl-gray-light;
     }
 
     .fa {
@@ -201,7 +201,7 @@
 
   .duration,
   .finished-at {
-    color: $table-text-gray;
+    color: $gl-gray-light;
     margin: 4px 0;
 
     .fa {
@@ -222,7 +222,7 @@
 
     .btn {
       margin: 0;
-      color: $table-text-gray;
+      color: $gl-gray-light;
     }
 
     .cancel-retry-btns {
@@ -235,10 +235,10 @@
 
     .dropdown-toggle,
     .dropdown-menu {
-      color: $table-text-gray;
+      color: $gl-gray-light;
 
       .fa {
-        color: $table-text-gray;
+        color: $gl-gray-light;
         font-size: 14px;
       }
 
@@ -288,69 +288,162 @@
 }
 
 // Pipeline visualization
+.pipeline-actions {
+  border-bottom: none;
+}
+
+.tab-pane {
+  &.pipelines {
+    .ci-table {
+      min-width: 900px;
+    }
 
-.toggle-pipeline-btn {
-  background-color: $gray-dark;
+    .content-list.pipelines {
+      overflow: auto;
+    }
 
-  &.graph-collapsed {
-    background-color: $white-light;
+    .stage {
+      max-width: 100px;
+      width: 100px;
+    }
+
+    .pipeline-actions {
+      min-width: initial;
+    }
+  }
+
+  &.builds {
+    .ci-table {
+      tr {
+        height: 71px;
+      }
+    }
   }
 }
 
+// Pipeline graph
 .pipeline-graph {
   width: 100%;
-  background-color: $background-color;
+  background-color: $gray-light;
   padding: $gl-padding;
   overflow: auto;
   white-space: nowrap;
   transition: max-height 0.3s, padding 0.3s;
 
-  &.graph-collapsed {
-    max-height: 0;
-    padding: 0 16px;
-  }
-}
-
-.pipeline-visualization {
-  position: relative;
-
   ul {
     padding: 0;
   }
-}
 
-.stage-column {
-  display: inline-block;
-  vertical-align: top;
+  a {
+    text-decoration: none;
+    color: $gl-text-color-light;
+  }
 
-  &:not(:last-child) {
-    margin-right: 44px;
+  svg {
+    vertical-align: middle;
+    margin-right: 3px;
   }
 
-  &.left-margin {
-    &:not(:first-child) {
-      margin-left: 44px;
+  .stage-column {
+    display: inline-block;
+    vertical-align: top;
 
-      .left-connector {
-        &::before {
-          content: '';
-          position: absolute;
-          top: 48%;
-          left: -48px;
-          border-top: 2px solid $border-color;
-          width: 48px;
-          height: 1px;
+    &:not(:last-child) {
+      margin-right: 44px;
+    }
+
+    &.left-margin {
+      &:not(:first-child) {
+        margin-left: 44px;
+
+        .left-connector {
+          &::before {
+            content: '';
+            position: absolute;
+            top: 48%;
+            left: -48px;
+            border-top: 2px solid $border-color;
+            width: 48px;
+            height: 1px;
+          }
         }
       }
     }
-  }
 
-  &.no-margin {
-    margin: 0;
-  }
+    &.no-margin {
+      margin: 0;
+    }
+
+    li {
+      list-style: none;
+    }
 
-  li {
-    list-style: none;
+    &:last-child {
+      .build {
+        // Remove right connecting horizontal line from first build in last stage
+        &:first-child {
+          &::after {
+            border: none;
+          }
+        }
+        // Remove right curved connectors from all builds in last stage
+        &:not(:first-child) {
+          &::after {
+            border: none;
+          }
+        }
+        // Remove opposite curve
+        .curve {
+          &::before {
+            display: none;
+          }
+        }
+      }
+    }
+
+    &:first-child {
+      .build {
+        // Remove left curved connectors from all builds in first stage
+        &:not(:first-child) {
+          &::before {
+            border: none;
+          }
+        }
+        // Remove opposite curve
+        .curve {
+          &::after {
+            display: none;
+          }
+        }
+      }
+    }
+
+    // Curve first child connecting lines in opposite direction
+    .curve {
+      display: none;
+
+      &::before,
+      &::after {
+        content: '';
+        width: 21px;
+        height: 25px;
+        position: absolute;
+        top: -32px;
+        border-top: 2px solid $border-color;
+      }
+
+      &::after {
+        left: -44px;
+        border-right: 2px solid $border-color;
+        border-radius: 0 20px;
+      }
+
+      &::before {
+        right: -44px;
+        border-left: 2px solid $border-color;
+        border-radius: 20px 0 0;
+      }
+    }
   }
 
   .stage-name {
@@ -364,175 +457,78 @@
 
   .build {
     border: 1px solid $border-color;
+    border-radius: 30px;
     background-color: $white-light;
     position: relative;
-    padding: 7px 10px 8px;
-    border-radius: 30px;
+    padding: 8px 4px 9px 10px;
     width: 186px;
     margin-bottom: 10px;
+    white-space: normal;
 
     &:hover {
-      background-color: $gray-lighter;
-    }
-
-    &.playable {
-
-      svg {
-        height: 13px;
-        width: 20px;
-        position: relative;
-        top: 1px;
+      background-color: $stage-hover-bg;
+      border: 1px solid $stage-hover-border;
 
-        path {
-          fill: $layout-link-gray;
-        }
+      a,
+      .dropdown-counter-badge,
+      .dropdown-menu-toggle {
+        color: $gl-text-color;
       }
-    }
 
-    .build-content {
-      display: -ms-flexbox;
-      display: -webkit-flex;
-      display: flex;
-      width: 164px;
+      .grouped-pipeline-dropdown a {
+        color: $gl-text-color-light;
 
-      .ci-status-icon {
-        svg {
-          height: 20px;
-          width: 20px;
+        &:hover {
+          color: $gl-text-color;
         }
       }
+    }
 
-      .tooltip {
-        white-space: nowrap;
+    .ci-status-icon {
+      position: relative;
+      top: 1px;
+    }
 
-        .tooltip-inner {
-          overflow: hidden;
-          text-overflow: ellipsis;
-        }
-      }
+    .ci-status-icon svg {
+      height: 20px;
+      width: 20px;
+    }
 
-      .ci-status-text {
-        width: 135px;
-        white-space: nowrap;
-        overflow: hidden;
-        text-overflow: ellipsis;
-        vertical-align: middle;
+    .arrow {
+      &::before,
+      &::after {
+        content: '';
         display: inline-block;
-        position: relative;
-        top: -1px;
-      }
-
-      a {
-        color: $gl-text-color-light;
-        text-decoration: none;
-      }
-
-      .dropdown-menu-toggle {
-        background-color: transparent;
-        border: none;
-        width: auto;
-        padding: 0;
-        color: $gl-text-color-light;
-        flex-grow: 1;
-
-        .ci-status-text {
-          max-width: 112px;
-          width: auto;
-        }
+        position: absolute;
+        width: 0;
+        height: 0;
+        border-color: transparent;
+        border-style: solid;
+        top: 18px;
       }
 
-      .grouped-pipeline-dropdown {
-        padding: 0;
-        width: 186px;
-        left: auto;
-        right: -197px;
-        top: -9px;
-
-        ul {
-          max-height: 245px;
-          overflow: auto;
-
-          li:first-child {
-            padding-top: 8px;
-          }
-
-          li:last-child {
-            padding-bottom: 8px;
-          }
-        }
-
-        a {
-          color: $gl-text-color;
-          padding: 7px 8px 8px;
-
-          &:hover {
-            background-color: $blue-light-transparent;
-            border-radius: 3px;
-
-            .ci-status-text {
-              text-decoration: none;
-            }
-          }
-        }
-
-        svg {
-          width: 14px;
-          height: 14px;
-        }
-
-        .ci-status-text {
-          width: 112px;
-        }
-
-        .arrow {
-          &::before,
-          &::after {
-            content: '';
-            display: inline-block;
-            position: absolute;
-            width: 0;
-            height: 0;
-            border-color: transparent;
-            border-style: solid;
-            top: 18px;
-          }
-
-          &::before {
-            left: -5px;
-            margin-top: -6px;
-            border-width: 7px 5px 7px 0;
-            border-right-color: $border-color;
-          }
-
-          &::after {
-            left: -4px;
-            margin-top: -9px;
-            border-width: 10px 7px 10px 0;
-            border-right-color: $white-light;
-          }
-        }
+      &::before {
+        left: -5px;
+        margin-top: -6px;
+        border-width: 7px 5px 7px 0;
+        border-right-color: $border-color;
       }
 
-      .badge {
-        background-color: $gray-darker;
-        color: $gl-text-color-light;
-        font-weight: normal;
-        margin-left: $btn-xs-side-margin;
+      &::after {
+        left: -4px;
+        margin-top: -9px;
+        border-width: 10px 7px 10px 0;
+        border-right-color: $white-light;
       }
     }
 
-    svg {
-      vertical-align: middle;
-      margin-right: 5px;
-    }
-
     // Connect first build in each stage with right horizontal line
     &:first-child {
       &::after {
         content: '';
         position: absolute;
         top: 48%;
-        right: -48px;
+        right: -49px;
         border-top: 2px solid $border-color;
         width: 48px;
         height: 1px;
@@ -580,109 +576,161 @@
     }
   }
 
-  &:last-child {
-    .build {
-      // Remove right connecting horizontal line from first build in last stage
-      &:first-child {
-        &::after {
-          border: none;
-        }
-      }
-      // Remove right curved connectors from all builds in last stage
-      &:not(:first-child) {
-        &::after {
-          border: none;
-        }
-      }
-      // Remove opposite curve
-      .curve {
-        &::before {
-          display: none;
-        }
-      }
-    }
+  .ci-status-text {
+    max-width: 110px;
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    vertical-align: bottom;
+    display: inline-block;
+    position: relative;
+    font-weight: 100;
   }
 
-  &:first-child {
-    .build {
-      // Remove left curved connectors from all builds in first stage
-      &:not(:first-child) {
-        &::before {
-          border: none;
-        }
-      }
-      // Remove opposite curve
-      .curve {
-        &::after {
-          display: none;
-        }
+  .dropdown-menu-toggle {
+    background-color: transparent;
+    border: none;
+    padding: 0;
+    color: $gl-text-color-light;
+    white-space: normal;
+    overflow: visible;
+
+    &:focus {
+      outline: none;
+    }
+
+    &:hover {
+      color: $gl-text-color;
+
+      .dropdown-counter-badge {
+        color: $gl-text-color;
       }
     }
   }
 
-  // Curve first child connecting lines in opposite direction
-  .curve {
-    display: none;
+  .dropdown-counter-badge {
+    float: right;
+    clear: right;
+    color: $border-color;
+    font-weight: 100;
+    font-size: 15px;
+    margin-right: 2px;
+  }
 
-    &::before,
-    &::after {
-      content: '';
-      width: 21px;
-      height: 25px;
-      position: absolute;
-      top: -32px;
-      border-top: 2px solid $border-color;
-    }
+  .grouped-pipeline-dropdown {
+    padding: 0;
+    width: 191px;
+    left: auto;
+    right: -206px;
+    top: -11px;
+    box-shadow: 0 1px 5px $black-transparent;
+
+    a {
+      display: inline-block;
 
-    &::after {
-      left: -44px;
-      border-right: 2px solid $border-color;
-      border-radius: 0 20px;
+      &:hover {
+        background-color: $stage-hover-bg;
+      }
     }
 
-    &::before {
-      right: -44px;
-      border-left: 2px solid $border-color;
-      border-radius: 20px 0 0;
+    ul {
+      max-height: 245px;
+      overflow: auto;
+      margin: 5px 0;
+
+      li {
+        margin: 0 5px;
+        padding-left: 0;
+        padding-bottom: 0;
+        margin-bottom: 0;
+        line-height: 1.2;
+      }
     }
-  }
-}
 
-.pipeline-actions {
-  border-bottom: none;
-}
+    .dropdown-build {
+      color: $gl-text-color-light;
 
-.toggle-pipeline-btn {
+      a.ci-action-icon-container {
+        padding: 0;
+        font-size: 11px;
+        float: right;
+        margin-top: 4px;
+        display: inline-block;
+        position: relative;
 
-  .fa {
-    color: $dropdown-header-color;
-  }
-}
+        i {
+          font-size: 11px;
+          margin-top: 0;
+        }
+      }
 
-.tab-pane {
+      &:hover {
+        background-color: $stage-hover-bg;
+        border-radius: 3px;
+        color: $gl-text-color;
+      }
 
-  &.pipelines {
+      .ci-action-icon-container {
+        i {
+          width: 25px;
+          height: 25px;
 
-    .ci-table {
-      min-width: 900px;
-    }
+          &::before {
+            top: 1px;
+            left: 1px;
+          }
+        }
+      }
 
-    .stage {
-      max-width: 100px;
-      width: 100px;
-    }
+      .stage {
+        max-width: 100px;
+        width: 100px;
+      }
 
-    .pipeline-actions {
-      min-width: initial;
+      .ci-status-icon svg {
+        height: 18px;
+        width: 18px;
+      }
+
+      .ci-status-text {
+        max-width: 95px;
+        padding-bottom: 3px;
+        position: relative;
+        top: 3px;
+      }
     }
   }
+}
 
-  &.builds {
+// Action Icons
+.ci-action-icon-container .ci-action-icon-wrapper {
+  float: right;
+  margin-top: -4px;
 
-    .ci-table {
-      tr {
-        height: 71px;
-      }
+  i {
+    color: $border-color;
+    border-radius: 100%;
+    border: 1px solid $border-color;
+    padding: 5px 6px;
+    font-size: 13px;
+    background: $white-light;
+    height: 30px;
+    width: 30px;
+
+    &::before {
+      position: relative;
+      top: 3px;
+      left: 3px;
     }
+
+    &:hover {
+      color: $gl-text-color;
+      background-color: $stage-hover-bg;
+      border: 1px solid $stage-hover-bg;
+    }
+  }
+
+  .ci-play-icon {
+    padding: 5px 5px 5px 7px;
   }
 }
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index f8677f93fe045b8bb5124b91f2e6f163705421bb..8b1976bd92576fa8b8abd90fa6877ce4e293d2c9 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -60,8 +60,8 @@
 
 .account-well {
   padding: 10px;
-  background-color: $help-well-bg;
-  border: 1px solid $help-well-border;
+  background-color: $gray-light;
+  border: 1px solid $border-color;
   border-radius: $border-radius-base;
 
   ul {
@@ -136,7 +136,7 @@
 .provider-btn-group {
   display: inline-block;
   margin-right: 10px;
-  border: 1px solid $provider-btn-group-border;
+  border: 1px solid $border-color;
   border-radius: 3px;
 
   &:last-child {
@@ -147,7 +147,7 @@
 .provider-btn-image {
   display: inline-block;
   padding: 5px 10px;
-  border-right: 1px solid $provider-btn-group-border;
+  border-right: 1px solid $border-color;
 
   > img {
     width: 20px;
@@ -198,7 +198,7 @@
 }
 
 .personal-access-tokens-never-expires-label {
-  color: $personal-access-tokens-disabled-label-color;
+  color: $note-disabled-comment-color;
 }
 
 .datepicker.personal-access-tokens-expires-at .ui-state-disabled span {
@@ -262,3 +262,13 @@ table.u2f-registrations {
     border-right: solid 1px transparent;
   }
 }
+
+.oauth-application-show {
+  .scope-name {
+    font-weight: 600;
+  }
+
+  .scopes-list {
+    padding-left: 18px;
+  }
+}
\ No newline at end of file
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 6e0f6b1cd8130277e2abb4303387d6f93e1f35dd..3b1b375133d9d660dc78d3ce48135abf27757d13 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -6,12 +6,6 @@
   }
 }
 
-.no-ssh-key-message,
-.project-limit-message {
-  background-color: $project-limit-message-bg;
-  margin-bottom: 0;
-}
-
 .new_project,
 .edit-project {
 
@@ -420,13 +414,13 @@ a.deploy-project-label {
         width: 100px;
         height: 100px;
         background-color: $gray-light;
-        border: 1px solid $gray-dark;
+        border: 1px solid $white-normal;
         margin: 0 auto;
         border-radius: 50%;
 
         i {
           font-size: 100px;
-          color: $gray-dark;
+          color: $white-normal;
         }
       }
 
@@ -536,7 +530,7 @@ a.deploy-project-label {
   }
 
   li.missing {
-    border: 1px dashed $border-gray-light;
+    border: 1px dashed $border-gray-normal;
     border-radius: $border-radius-default;
 
     a {
@@ -591,7 +585,7 @@ pre.light-well {
   @include basic-list;
 
   .project-row {
-    border-color: $table-border-color;
+    border-color: $white-normal;
 
     .project-full-name {
       @include str-truncated;
@@ -643,7 +637,7 @@ pre.light-well {
   &.container-fluid {
     padding-top: 12px;
     padding-bottom: 12px;
-    background-color: $background-color;
+    background-color: $gray-light;
     border: 1px solid $border-color;
     border-right-width: 0;
     border-left-width: 0;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 63d0a34e610fa01687f484ab4ea041f0e6b53b0e..cedd4cb29878506e10e3993d746d3325ca8bcdd9 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -51,9 +51,9 @@
     border-radius: $border-radius-default;
     font-size: 14px;
     font-style: normal;
-    color: $location-badge-color;
+    color: $note-disabled-comment-color;
     display: inline-block;
-    background-color: $location-badge-bg;
+    background-color: $gray-normal;
     vertical-align: top;
     cursor: default;
   }
@@ -140,7 +140,7 @@
 
     .search-input-wrap {
       i {
-        color: $location-icon-active-color;
+        color: $layout-link-gray;
       }
     }
   }
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index 51c926608f9af300a93e4a804e85784b5201e6aa..ddee2c95247418e0ce337e15cafb29ad9cb9adc9 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -1,5 +1,5 @@
 .settings-list-icon {
-  color: $gl-placeholder-color;
+  color: $gl-gray-light;
   font-size: $settings-icon-size;
   line-height: 42px;
 }
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 5084b466722e7f9f23884561245327b829c58e6d..f3b0608e54535ce0ffa35761ce99fae00a0f5640 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -102,15 +102,15 @@
 
     &.ci-created,
     &.ci-skipped {
-      color: $table-text-gray;
-      border-color: $table-text-gray;
+      color: $gl-gray-light;
+      border-color: $gl-gray-light;
 
       &:not(span):hover {
-        background-color: rgba( $table-text-gray, .07);
+        background-color: rgba( $gl-gray-light, .07);
       }
 
       svg {
-        fill: $table-text-gray;
+        fill: $gl-gray-light;
       }
     }
   }
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 20ad63be0458014cd4ba662f373a177330a4bd15..c0341db7289017b33d474af7d6d53be929fcf02b 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -20,8 +20,8 @@
     margin-bottom: 0;
 
     tr {
-      border-bottom: 1px solid $table-border-gray;
-      border-top: 1px solid $table-border-gray;
+      border-bottom: 1px solid $white-normal;
+      border-top: 1px solid $white-normal;
 
       td,
       th {
@@ -39,7 +39,7 @@
 
       .commit-history-link-spacer {
         margin: 0 10px;
-        color: $table-border-color;
+        color: $white-normal;
       }
 
       &:hover {
@@ -53,7 +53,7 @@
 
       &.selected {
         td {
-          background: $gray-dark;
+          background: $white-normal;
           border-top: 1px solid $border-gray-dark;
           border-bottom: 1px solid $border-gray-dark;
         }
@@ -134,7 +134,7 @@
 .blob-commit-info {
   list-style: none;
   padding: $gl-padding;
-  background: $background-color;
+  background: $gray-light;
   border: 1px solid $border-color;
   border-bottom: none;
   margin: 0;
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 9f9d630978a6be5567a30550ec8b641b04a21879..b085c56390de8299d16aef8318a39fd3529475f7 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -18,7 +18,7 @@
   $l-blue: #81a2be;
   $l-magenta: #b294bb;
   $l-cyan: #8abeb7;
-  $l-white: $ci-text-color;
+  $l-white: $gray-darkest;
 
   /*
   * xterm colors
diff --git a/app/controllers/admin/applications_controller.rb b/app/controllers/admin/applications_controller.rb
index 471d24934a017a5e802018a4eca7c759bd80e0f8..62f62e99a97cfc1d668afbfeeb8d3e12cd682d33 100644
--- a/app/controllers/admin/applications_controller.rb
+++ b/app/controllers/admin/applications_controller.rb
@@ -1,5 +1,8 @@
 class Admin::ApplicationsController < Admin::ApplicationController
+  include OauthApplications
+
   before_action :set_application, only: [:show, :edit, :update, :destroy]
+  before_action :load_scopes, only: [:new, :edit]
 
   def index
     @applications = Doorkeeper::Application.where("owner_id IS NULL")
@@ -47,6 +50,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
 
   # Only allow a trusted parameter "white list" through.
   def application_params
-    params[:doorkeeper_application].permit(:name, :redirect_uri)
+    params[:doorkeeper_application].permit(:name, :redirect_uri, :scopes)
   end
 end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bcc0b17bce2cd4723c48c8c4f2ca11b79845a637..4df80195ae10ab38132671b9423744865180c198 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -262,7 +262,7 @@ class ApplicationController < ActionController::Base
   end
 
   def bitbucket_import_configured?
-    Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
+    Gitlab::OAuth::Provider.enabled?(:bitbucket)
   end
 
   def google_code_import_enabled?
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9849aa93fa61a29830c77d95ff368ced63edac7c
--- /dev/null
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -0,0 +1,19 @@
+module OauthApplications
+  extend ActiveSupport::Concern
+
+  included do
+    before_action :prepare_scopes, only: [:create, :update]
+  end
+
+  def prepare_scopes
+    scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
+
+    if scopes
+      params[:doorkeeper_application][:scopes] = scopes.join(' ')
+    end
+  end
+
+  def load_scopes
+    @scopes = Doorkeeper.configuration.scopes
+  end
+end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index c33d7eecb9f7941ba7f8d29d9436ee1abc652480..549a8526715a11d849167282b85916c13cc2d080 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -18,7 +18,7 @@ module ServiceParams
                     :add_pusher, :send_from_committer_email, :disable_diffs,
                     :external_wiki_url, :notify, :color,
                     :server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
-                    :jira_issue_transition_id, :url, :project_key]
+                    :jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace]
 
   # Parameters to ignore if no value is specified
   FILTER_BLANK_PARAMS = [:password]
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 940a3ad20ba5495d26d759bf9664a0e0f1de568e..4f273a8d4f0af583e2cf922a2f39df67546d7bfd 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -1,20 +1,20 @@
 class Groups::GroupMembersController < Groups::ApplicationController
   include MembershipActions
+  include SortingHelper
 
   # Authorize
   before_action :authorize_admin_group_member!, except: [:index, :leave, :request_access]
 
   def index
+    @sort = params[:sort].presence || sort_value_name
     @project = @group.projects.find(params[:project_id]) if params[:project_id]
+
     @members = @group.group_members
     @members = @members.non_invite unless can?(current_user, :admin_group, @group)
+    @members = @members.search(params[:search]) if params[:search].present?
+    @members = @members.sort(@sort)
+    @members = @members.page(params[:page]).per(50)
 
-    if params[:search].present?
-      users = @group.users.search(params[:search]).to_a
-      @members = @members.where(user_id: users)
-    end
-
-    @members = @members.order('access_level DESC').page(params[:page]).per(50)
     @requesters = AccessRequestsFinder.new(@group).execute(current_user)
 
     @group_member = @group.group_members.new
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 6ea54744da83982edbd0633bb7e86632e6aa2a96..8e42cdf415f55c0453c4e37c0af4e11e079c3566 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -2,50 +2,57 @@ class Import::BitbucketController < Import::BaseController
   before_action :verify_bitbucket_import_enabled
   before_action :bitbucket_auth, except: :callback
 
-  rescue_from OAuth::Error, with: :bitbucket_unauthorized
-  rescue_from Gitlab::BitbucketImport::Client::Unauthorized, with: :bitbucket_unauthorized
+  rescue_from OAuth2::Error, with: :bitbucket_unauthorized
+  rescue_from Bitbucket::Error::Unauthorized, with: :bitbucket_unauthorized
 
   def callback
-    request_token = session.delete(:oauth_request_token)
-    raise "Session expired!" if request_token.nil?
+    response = client.auth_code.get_token(params[:code], redirect_uri: callback_import_bitbucket_url)
 
-    request_token.symbolize_keys!
-
-    access_token = client.get_token(request_token, params[:oauth_verifier], callback_import_bitbucket_url)
-
-    session[:bitbucket_access_token] = access_token.token
-    session[:bitbucket_access_token_secret] = access_token.secret
+    session[:bitbucket_token]         = response.token
+    session[:bitbucket_expires_at]    = response.expires_at
+    session[:bitbucket_expires_in]    = response.expires_in
+    session[:bitbucket_refresh_token] = response.refresh_token
 
     redirect_to status_import_bitbucket_url
   end
 
   def status
-    @repos = client.projects
-    @incompatible_repos = client.incompatible_projects
+    bitbucket_client = Bitbucket::Client.new(credentials)
+    repos = bitbucket_client.repos
+
+    @repos, @incompatible_repos = repos.partition { |repo| repo.valid? }
 
-    @already_added_projects = current_user.created_projects.where(import_type: "bitbucket")
+    @already_added_projects = current_user.created_projects.where(import_type: 'bitbucket')
     already_added_projects_names = @already_added_projects.pluck(:import_source)
 
-    @repos.to_a.reject!{ |repo| already_added_projects_names.include? "#{repo["owner"]}/#{repo["slug"]}" }
+    @repos.to_a.reject! { |repo| already_added_projects_names.include?(repo.full_name) }
   end
 
   def jobs
-    jobs = current_user.created_projects.where(import_type: "bitbucket").to_json(only: [:id, :import_status])
-    render json: jobs
+    render json: current_user.created_projects
+                             .where(import_type: 'bitbucket')
+                             .to_json(only: [:id, :import_status])
   end
 
   def create
+    bitbucket_client = Bitbucket::Client.new(credentials)
+
     @repo_id = params[:repo_id].to_s
-    repo = client.project(@repo_id.gsub('___', '/'))
-    @project_name = repo['slug']
-    @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
+    name = @repo_id.gsub('___', '/')
+    repo = bitbucket_client.repo(name)
+    @project_name = params[:new_name].presence || repo.name
 
-    unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
-      render 'deploy_key' and return
-    end
+    repo_owner = repo.owner
+    repo_owner = current_user.username if repo_owner == bitbucket_client.user.username
+    @target_namespace = params[:new_namespace].presence || repo_owner
+
+    namespace = find_or_create_namespace(@target_namespace, current_user)
 
-    if current_user.can?(:create_projects, @target_namespace)
-      @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+    if current_user.can?(:create_projects, namespace)
+      # The token in a session can be expired, we need to get most recent one because
+      # Bitbucket::Connection class refreshes it.
+      session[:bitbucket_token] = bitbucket_client.connection.token
+      @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @project_name, namespace, current_user, credentials).execute
     else
       render 'unauthorized'
     end
@@ -54,8 +61,15 @@ class Import::BitbucketController < Import::BaseController
   private
 
   def client
-    @client ||= Gitlab::BitbucketImport::Client.new(session[:bitbucket_access_token],
-                                                    session[:bitbucket_access_token_secret])
+    @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+  end
+
+  def provider
+    Gitlab::OAuth::Provider.config_for('bitbucket')
+  end
+
+  def options
+    OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
   end
 
   def verify_bitbucket_import_enabled
@@ -63,26 +77,23 @@ class Import::BitbucketController < Import::BaseController
   end
 
   def bitbucket_auth
-    if session[:bitbucket_access_token].blank?
-      go_to_bitbucket_for_permissions
-    end
+    go_to_bitbucket_for_permissions if session[:bitbucket_token].blank?
   end
 
   def go_to_bitbucket_for_permissions
-    request_token = client.request_token(callback_import_bitbucket_url)
-    session[:oauth_request_token] = request_token
-
-    redirect_to client.authorize_url(request_token, callback_import_bitbucket_url)
+    redirect_to client.auth_code.authorize_url(redirect_uri: callback_import_bitbucket_url)
   end
 
   def bitbucket_unauthorized
     go_to_bitbucket_for_permissions
   end
 
-  def access_params
+  def credentials
     {
-      bitbucket_access_token: session[:bitbucket_access_token],
-      bitbucket_access_token_secret: session[:bitbucket_access_token_secret]
+      token: session[:bitbucket_token],
+      expires_at: session[:bitbucket_expires_at],
+      expires_in: session[:bitbucket_expires_in],
+      refresh_token: session[:bitbucket_refresh_token]
     }
   end
 end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index c736200a1040d4f3cadf0c6e9b6576cd8f25bdf3..c2e4d62b50be10d5f7ed700c72b070dd3200f3a2 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -26,7 +26,7 @@ class JwtController < ApplicationController
       @authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
 
       render_unauthorized unless @authentication_result.success? &&
-        (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
+          (@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
     end
   rescue Gitlab::Auth::MissingPersonalTokenError
     render_missing_personal_token
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 0f54dfa4efc898ada20bfb20dd0c3d1819692c05..2ae4785b12cc1b43c1336240d0b5948bb75f4e96 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -2,10 +2,12 @@ class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
   include Gitlab::CurrentSettings
   include Gitlab::GonHelper
   include PageLayoutHelper
+  include OauthApplications
 
   before_action :verify_user_oauth_applications_enabled
   before_action :authenticate_user!
   before_action :add_gon_variables
+  before_action :load_scopes, only: [:index, :create, :edit]
 
   layout 'profile'
 
diff --git a/app/controllers/profiles/personal_access_tokens_controller.rb b/app/controllers/profiles/personal_access_tokens_controller.rb
index 508b82a9a6c2c582fc9f781e3cc09aa088c89d38..6e007f17913350b0f92955218ffc2f93a4f2c25b 100644
--- a/app/controllers/profiles/personal_access_tokens_controller.rb
+++ b/app/controllers/profiles/personal_access_tokens_controller.rb
@@ -1,8 +1,6 @@
 class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
-  before_action :load_personal_access_tokens, only: :index
-
   def index
-    @personal_access_token = current_user.personal_access_tokens.build
+    set_index_vars
   end
 
   def create
@@ -12,7 +10,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
       flash[:personal_access_token] = @personal_access_token.token
       redirect_to profile_personal_access_tokens_path, notice: "Your new personal access token has been created."
     else
-      load_personal_access_tokens
+      set_index_vars
       render :index
     end
   end
@@ -32,10 +30,12 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
   private
 
   def personal_access_token_params
-    params.require(:personal_access_token).permit(:name, :expires_at)
+    params.require(:personal_access_token).permit(:name, :expires_at, scopes: [])
   end
 
-  def load_personal_access_tokens
+  def set_index_vars
+    @personal_access_token ||= current_user.personal_access_tokens.build
+    @scopes = Gitlab::Auth::SCOPES
     @active_personal_access_tokens = current_user.personal_access_tokens.active.order(:expires_at)
     @inactive_personal_access_tokens = current_user.personal_access_tokens.inactive
   end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 9eb75bb389185e5e4442882681ca46749434151f..18044ca78e2074285027ce2ba6babec795d786ca 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -22,6 +22,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
     end
 
     @qr_code = build_qr_code
+    @account_string = account_string
     setup_u2f_registration
   end
 
@@ -78,11 +79,14 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
   private
 
   def build_qr_code
-    issuer = "#{issuer_host} | #{current_user.email}"
-    uri = current_user.otp_provisioning_uri(current_user.email, issuer: issuer)
+    uri = current_user.otp_provisioning_uri(account_string, issuer: issuer_host)
     RQRCode::render_qrcode(uri, :svg, level: :m, unit: 3)
   end
 
+  def account_string
+    "#{issuer_host}:#{current_user.email}"
+  end
+
   def issuer_host
     Gitlab.config.gitlab.host
   end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d9dfa53466981896e96e495d295fff7452934cda
--- /dev/null
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -0,0 +1,48 @@
+class Projects::AutocompleteSourcesController < Projects::ApplicationController
+  before_action :load_autocomplete_service, except: [:emojis, :members]
+
+  def emojis
+    render json: Gitlab::AwardEmoji.urls
+  end
+
+  def members
+    render json: ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
+  end
+
+  def issues
+    render json: @autocomplete_service.issues
+  end
+
+  def merge_requests
+    render json: @autocomplete_service.merge_requests
+  end
+
+  def labels
+    render json: @autocomplete_service.labels
+  end
+
+  def milestones
+    render json: @autocomplete_service.milestones
+  end
+
+  def commands
+    render json: @autocomplete_service.commands(noteable, params[:type])
+  end
+
+  private
+
+  def load_autocomplete_service
+    @autocomplete_service = ::Projects::AutocompleteService.new(@project, current_user)
+  end
+
+  def noteable
+    case params[:type]
+    when 'Issue'
+      IssuesFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+    when 'MergeRequest'
+      MergeRequestsFinder.new(current_user, project_id: @project.id).execute.find_by(iid: params[:type_id])
+    when 'Commit'
+      @project.commit(params[:type_id])
+    end
+  end
+end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 15ca080c6964d616a05614e61ef0740cf99ecbbd..b71509f2c9bd3fb79d55eea2fe2c64964a7499eb 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -217,6 +217,6 @@ class Projects::NotesController < Projects::ApplicationController
   end
 
   def find_current_user_notes
-    @notes = NotesFinder.new.execute(project, current_user, params)
+    @notes = NotesFinder.new(project, current_user, params).execute.inc_author
   end
 end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 53308948f623ce6bdfc605ed08fc37212f94f6ea..3aec6f18e27a39ab234adf20b60f0ec526fda7f5 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -1,10 +1,12 @@
 class Projects::ProjectMembersController < Projects::ApplicationController
   include MembershipActions
+  include SortingHelper
 
   # Authorize
   before_action :authorize_admin_project_member!, except: [:index, :leave, :request_access]
 
   def index
+    @sort = params[:sort].presence || sort_value_name
     @group_links = @project.project_group_links
 
     @project_members = @project.project_members
@@ -35,12 +37,13 @@ class Projects::ProjectMembersController < Projects::ApplicationController
       @group_links = @project.project_group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
     end
 
-    wheres = ["id IN (#{@project_members.select(:id).to_sql})"]
-    wheres << "id IN (#{group_members.select(:id).to_sql})" if group_members
+    wheres = ["members.id IN (#{@project_members.select(:id).to_sql})"]
+    wheres << "members.id IN (#{group_members.select(:id).to_sql})" if group_members
 
     @project_members = Member.
       where(wheres.join(' OR ')).
-      order(access_level: :desc).page(params[:page])
+      sort(@sort).
+      page(params[:page])
 
     @requesters = AccessRequestsFinder.new(@project).execute(current_user)
 
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a8a18b4fa1673c7a48a9c6fd8aee27f564f12cca..d5ee503c44c6ec59600489add400c99ae5e14f0f 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -127,39 +127,6 @@ class ProjectsController < Projects::ApplicationController
     redirect_to edit_project_path(@project), alert: ex.message
   end
 
-  def autocomplete_sources
-    noteable =
-      case params[:type]
-      when 'Issue'
-        IssuesFinder.new(current_user, project_id: @project.id).
-          execute.find_by(iid: params[:type_id])
-      when 'MergeRequest'
-        MergeRequestsFinder.new(current_user, project_id: @project.id).
-          execute.find_by(iid: params[:type_id])
-      when 'Commit'
-        @project.commit(params[:type_id])
-      else
-        nil
-      end
-
-    autocomplete = ::Projects::AutocompleteService.new(@project, current_user)
-    participants = ::Projects::ParticipantsService.new(@project, current_user).execute(noteable)
-
-    @suggestions = {
-      emojis: Gitlab::AwardEmoji.urls,
-      issues: autocomplete.issues,
-      milestones: autocomplete.milestones,
-      mergerequests: autocomplete.merge_requests,
-      labels: autocomplete.labels,
-      members: participants,
-      commands: autocomplete.commands(noteable, params[:type])
-    }
-
-    respond_to do |format|
-      format.json { render json: @suggestions }
-    end
-  end
-
   def new_issue_address
     return render_404 unless Gitlab::IncomingEmail.supports_issue_creation?
 
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 8c698695202681347dcc3e332a3df2c1929a7f4d..93a180b903609f2e42246f53ce30d8bf56d98e7b 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -114,7 +114,7 @@ class SessionsController < Devise::SessionsController
 
   def valid_otp_attempt?(user)
     user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
-    user.invalidate_otp_backup_code!(user_params[:otp_attempt])
+      user.invalidate_otp_backup_code!(user_params[:otp_attempt])
   end
 
   def log_audit_event(user, options = {})
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index be00a219205de287be0729417a12374b63cfc682..707eddd4d2991138ee6fb9d2f279e25e8ba48416 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -23,10 +23,26 @@ class IssuesFinder < IssuableFinder
   private
 
   def init_collection
-    Issue.visible_to_user(current_user)
+    IssuesFinder.not_restricted_by_confidentiality(current_user)
   end
 
   def iid_pattern
     @iid_pattern ||= %r{\A#{Regexp.escape(Issue.reference_prefix)}(?<iid>\d+)\z}
   end
+
+  def self.not_restricted_by_confidentiality(user)
+    return Issue.where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
+
+    return Issue.all if user.admin?
+
+    Issue.where('
+      issues.confidential IS NULL
+      OR issues.confidential IS FALSE
+      OR (issues.confidential = TRUE
+        AND (issues.author_id = :user_id
+          OR issues.assignee_id = :user_id
+          OR issues.project_id IN(:project_ids)))',
+      user_id: user.id,
+      project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
+  end
 end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 2484339e3a4cf2bc66a4a079c4db9d6c1cfb8956..4bd8c83081a479238298b8554dca3a876710a93c 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -1,27 +1,102 @@
 class NotesFinder
   FETCH_OVERLAP = 5.seconds
 
-  def execute(project, current_user, params)
-    target_type = params[:target_type]
-    target_id   = params[:target_id]
-    # Default to 0 to remain compatible with old clients
-    last_fetched_at = Time.at(params.fetch(:last_fetched_at, 0).to_i)
-
-    notes =
-      case target_type
-      when "commit"
-        project.notes.for_commit_id(target_id).non_diff_notes
-      when "issue"
-        IssuesFinder.new(current_user, project_id: project.id).find(target_id).notes.inc_author
-      when "merge_request"
-        MergeRequestsFinder.new(current_user, project_id: project.id).find(target_id).mr_and_commit_notes.inc_author
-      when "snippet", "project_snippet"
-        project.snippets.find(target_id).notes
+  # Used to filter Notes
+  # When used with target_type and target_id this returns notes specifically for the controller
+  #
+  # Arguments:
+  #   current_user - which user check authorizations with
+  #   project - which project to look for notes on
+  #   params:
+  #     target_type: string
+  #     target_id: integer
+  #     last_fetched_at: time
+  #     search: string
+  #
+  def initialize(project, current_user, params = {})
+    @project = project
+    @current_user = current_user
+    @params = params
+    init_collection
+  end
+
+  def execute
+    @notes = since_fetch_at(@params[:last_fetched_at]) if @params[:last_fetched_at]
+    @notes
+  end
+
+  private
+
+  def init_collection
+    if @params[:target_id]
+      @notes = on_target(@params[:target_type], @params[:target_id])
+    else
+      @notes = notes_of_any_type
+    end
+  end
+
+  def notes_of_any_type
+    types = %w(commit issue merge_request snippet)
+    note_relations = types.map { |t| notes_for_type(t) }
+    note_relations.map!{ |notes| search(@params[:search], notes) } if @params[:search]
+    UnionFinder.new.find_union(note_relations, Note)
+  end
+
+  def noteables_for_type(noteable_type)
+    case noteable_type
+    when "issue"
+      IssuesFinder.new(@current_user, project_id: @project.id).execute
+    when "merge_request"
+      MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
+    when "snippet", "project_snippet"
+      SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+    else
+      raise 'invalid target_type'
+    end
+  end
+
+  def notes_for_type(noteable_type)
+    if noteable_type == "commit"
+      if Ability.allowed?(@current_user, :download_code, @project)
+        @project.notes.where(noteable_type: 'Commit')
+      else
+        Note.none
+      end
+    else
+      finder = noteables_for_type(noteable_type)
+      @project.notes.where(noteable_type: finder.base_class.name, noteable_id: finder.reorder(nil))
+    end
+  end
+
+  def on_target(target_type, target_id)
+    if target_type == "commit"
+      notes_for_type('commit').for_commit_id(target_id)
+    else
+      target = noteables_for_type(target_type).find(target_id)
+
+      if target.respond_to?(:related_notes)
+        target.related_notes
       else
-        raise 'invalid target_type'
+        target.notes
       end
+    end
+  end
+
+  # Searches for notes matching the given query.
+  #
+  # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+  #
+  def search(query, notes_relation = @notes)
+    pattern = "%#{query}%"
+    notes_relation.where(Note.arel_table[:note].matches(pattern))
+  end
+
+  # Notes changed since last fetch
+  # Uses overlapping intervals to avoid worrying about race conditions
+  def since_fetch_at(fetch_time)
+    # Default to 0 to remain compatible with old clients
+    last_fetched_at = Time.at(@params.fetch(:last_fetched_at, 0).to_i)
 
-    # Use overlapping intervals to avoid worrying about race conditions
-    notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
+    @notes.where('updated_at > ?', last_fetched_at - FETCH_OVERLAP).fresh
   end
 end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 07ff6fb94889c58503a881861c64f31d3a18f805..f31d4fb897d3a237879a06c34726d3b44e1c6514 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -191,6 +191,10 @@ module BlobHelper
     @gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
   end
 
+  def dockerfile_names
+    @dockerfile_names ||= Gitlab::Template::DockerfileTemplate.dropdown_names
+  end
+
   def blob_editor_paths
     {
       'relative-url-root' => Rails.application.config.relative_url_root,
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 6a43be2cf3ef14049e07f6b38ed9a4ab05e0fe13..1182939f656958db73c42a4673a0b1a4304e5ba8 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -7,12 +7,12 @@ module FormHelper
 
     content_tag(:div, class: 'alert alert-danger', id: 'error_explanation') do
       content_tag(:h4, headline) <<
-      content_tag(:ul) do
-        model.errors.full_messages.
-          map { |msg| content_tag(:li, msg) }.
-          join.
-          html_safe
-      end
+        content_tag(:ul) do
+          model.errors.full_messages.
+            map { |msg| content_tag(:li, msg) }.
+            join.
+            html_safe
+        end
     end
   end
 end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index f6d4ea4659a56e50bad14857f332d86249ba6ed1..77dc9e7d538dab25c78f13c502b0c934fcee6482 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -12,11 +12,18 @@ module GroupsHelper
   end
 
   def group_title(group, name = nil, url = nil)
-    full_title = link_to(simple_sanitize(group.name), group_path(group))
+    full_title = ''
+
+    group.parents.each do |parent|
+      full_title += link_to(simple_sanitize(parent.name), group_path(parent))
+      full_title += ' / '.html_safe
+    end
+
+    full_title += link_to(simple_sanitize(group.name), group_path(group))
     full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
 
     content_tag :span do
-      full_title
+      full_title.html_safe
     end
   end
 
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 877c77050bed0979d2814dacb549fd279f3d89e0..41d471cc92f973bad92e6cb74d784e88f706f7ae 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -36,4 +36,12 @@ module MembersHelper
     "Are you sure you want to leave the " \
     "\"#{member_source.human_name}\" #{member_source.class.to_s.humanize(capitalize: false)}?"
   end
+
+  def filter_group_project_member_path(options = {})
+    options = params.slice(:search, :sort).merge(options)
+
+    path = request.path
+    path << "?#{options.to_param}"
+    path
+  end
 end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a6659ea2fd1d0806d5da53b4bc7b7c51561e8df2..202187756591a7991a965c8442432973616842c2 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -59,6 +59,10 @@ module MergeRequestsHelper
     @mr_closes_issues ||= @merge_request.closes_issues
   end
 
+  def mr_issues_mentioned_but_not_closing
+    @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing
+  end
+
   def mr_change_branches_path(merge_request)
     new_namespace_project_merge_request_path(
       @project.namespace, @project,
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index a3331dc80cb5f91c5e25f8812880328c005b2b6a..e21178c7377139fa37ea4059f3b05aac52144e09 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -7,12 +7,12 @@ module NavHelper
 
   def page_gutter_class
     if current_path?('merge_requests#show') ||
-      current_path?('merge_requests#diffs') ||
-      current_path?('merge_requests#commits') ||
-      current_path?('merge_requests#builds') ||
-      current_path?('merge_requests#conflicts') ||
-      current_path?('merge_requests#pipelines') ||
-      current_path?('issues#show')
+        current_path?('merge_requests#diffs') ||
+        current_path?('merge_requests#commits') ||
+        current_path?('merge_requests#builds') ||
+        current_path?('merge_requests#conflicts') ||
+        current_path?('merge_requests#pipelines') ||
+        current_path?('issues#show')
       if cookies[:collapsed_gutter] == 'true'
         "page-gutter right-sidebar-collapsed"
       else
@@ -21,9 +21,9 @@ module NavHelper
     elsif current_path?('builds#show')
       "page-gutter build-sidebar right-sidebar-expanded"
     elsif current_path?('wikis#show') ||
-      current_path?('wikis#edit') ||
-      current_path?('wikis#history') ||
-      current_path?('wikis#git_access')
+        current_path?('wikis#edit') ||
+        current_path?('wikis#history') ||
+        current_path?('wikis#git_access')
       "page-gutter wiki-sidebar right-sidebar-expanded"
     end
   end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 9cda3b787613db6f04db163c788dc47cba16c40e..d2177f683a1a9caef7e3dd747b71f3d39df3efed 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -52,7 +52,7 @@ module ProjectsHelper
   def project_title(project)
     namespace_link =
       if project.group
-        link_to(simple_sanitize(project.group.name), group_path(project.group))
+        group_title(project.group)
       else
         owner = project.namespace.owner
         link_to(simple_sanitize(owner.name), user_path(owner))
@@ -390,7 +390,7 @@ module ProjectsHelper
       "success"
     end
   end
-  
+
   def readme_cache_key
     sha = @project.commit.try(:sha) || 'nil'
     [@project.path_with_namespace, sha, "readme"].join('-')
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 8b138a8e69f6d4015fcf415b1dad7e68f07616c7..f03c46270503659a2b5549a860a9e3e83fb7bf89 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -25,7 +25,7 @@ module SortingHelper
       sort_value_recently_updated => sort_title_recently_updated,
       sort_value_oldest_updated => sort_title_oldest_updated,
       sort_value_recently_created => sort_title_recently_created,
-      sort_value_oldest_created => sort_title_oldest_created,
+      sort_value_oldest_created => sort_title_oldest_created
     }
 
     if current_controller?('admin/projects')
@@ -35,6 +35,19 @@ module SortingHelper
     options
   end
 
+  def member_sort_options_hash
+    {
+      sort_value_access_level_asc => sort_title_access_level_asc,
+      sort_value_access_level_desc => sort_title_access_level_desc,
+      sort_value_last_joined => sort_title_last_joined,
+      sort_value_oldest_joined => sort_title_oldest_joined,
+      sort_value_name => sort_title_name_asc,
+      sort_value_name_desc => sort_title_name_desc,
+      sort_value_recently_signin => sort_title_recently_signin,
+      sort_value_oldest_signin => sort_title_oldest_signin
+    }
+  end
+
   def sort_title_priority
     'Priority'
   end
@@ -95,6 +108,50 @@ module SortingHelper
     'Most popular'
   end
 
+  def sort_title_last_joined
+    'Last joined'
+  end
+
+  def sort_title_oldest_joined
+    'Oldest joined'
+  end
+
+  def sort_title_access_level_asc
+    'Access level, ascending'
+  end
+
+  def sort_title_access_level_desc
+    'Access level, descending'
+  end
+
+  def sort_title_name_asc
+    'Name, ascending'
+  end
+
+  def sort_title_name_desc
+    'Name, descending'
+  end
+
+  def sort_value_last_joined
+    'last_joined'
+  end
+
+  def sort_value_oldest_joined
+    'oldest_joined'
+  end
+
+  def sort_value_access_level_asc
+    'access_level_asc'
+  end
+
+  def sort_value_access_level_desc
+    'access_level_desc'
+  end
+
+  def sort_value_name_desc
+    'name_desc'
+  end
+
   def sort_value_priority
     'priority'
   end
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index 563ddd2a5118194c866e0e863a928fd1546d13f6..547f62589097198fd112d4ca26033ea15e2903e0 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -106,9 +106,9 @@ module TabHelper
 
   def branches_tab_class
     if current_controller?(:protected_branches) ||
-      current_controller?(:branches) ||
-      current_page?(namespace_project_repository_path(@project.namespace,
-                                                      @project))
+        current_controller?(:branches) ||
+        current_page?(namespace_project_repository_path(@project.namespace,
+                                                        @project))
       'active'
     end
   end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e7cf606a7aed36e767ebd03f38751d2377023a6f..591aba6bdc993fc8e6f603b42d04cd41facd1358 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -9,6 +9,14 @@ module Ci
 
     has_many :deployments, as: :deployable
 
+    # The "environment" field for builds is a String, and is the unexpanded name
+    def persisted_environment
+      @persisted_environment ||= Environment.find_by(
+        name: expanded_environment_name,
+        project_id: gl_project_id
+      )
+    end
+
     serialize :options
     serialize :yaml_variables
 
@@ -143,11 +151,11 @@ module Ci
     end
 
     def expanded_environment_name
-      ExpandVariables.expand(environment, variables) if environment
+      ExpandVariables.expand(environment, simple_variables) if environment
     end
 
     def has_environment?
-      self.environment.present?
+      environment.present?
     end
 
     def starts_environment?
@@ -195,12 +203,25 @@ module Ci
       project.build_timeout
     end
 
-    def variables
+    # A slugified version of the build ref, suitable for inclusion in URLs and
+    # domain names. Rules:
+    #
+    #   * Lowercased
+    #   * Anything not matching [a-z0-9-] is replaced with a -
+    #   * Maximum length is 63 bytes
+    def ref_slug
+      slugified = ref.to_s.downcase
+      slugified.gsub(/[^a-z0-9]/, '-')[0..62]
+    end
+
+    # Variables whose value does not depend on other variables
+    def simple_variables
       variables = predefined_variables
       variables += project.predefined_variables
       variables += pipeline.predefined_variables
       variables += runner.predefined_variables if runner
       variables += project.container_registry_variables
+      variables += project.deployment_variables if has_environment?
       variables += yaml_variables
       variables += user_variables
       variables += project.secret_variables
@@ -208,6 +229,13 @@ module Ci
       variables
     end
 
+    # All variables, including those dependent on other variables
+    def variables
+      variables = simple_variables
+      variables += persisted_environment.predefined_variables if persisted_environment.present?
+      variables
+    end
+
     def merge_request
       merge_requests = MergeRequest.includes(:merge_request_diff)
                                    .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
@@ -529,6 +557,7 @@ module Ci
         { key: 'CI_BUILD_REF', value: sha, public: true },
         { key: 'CI_BUILD_BEFORE_SHA', value: before_sha, public: true },
         { key: 'CI_BUILD_REF_NAME', value: ref, public: true },
+        { key: 'CI_BUILD_REF_SLUG', value: ref_slug, public: true },
         { key: 'CI_BUILD_NAME', value: name, public: true },
         { key: 'CI_BUILD_STAGE', value: stage, public: true },
         { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 54f73171fd467a027e9b93b17c20f15f5c80f706..48354cdbefb495e0f805a4cb717f0de9a8cd9a82 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -88,8 +88,24 @@ module Ci
     end
 
     # ref can't be HEAD or SHA, can only be branch/tag name
+    scope :latest, ->(ref = nil) do
+      max_id = unscope(:select)
+        .select("max(#{quoted_table_name}.id)")
+        .group(:ref, :sha)
+
+      if ref
+        where(id: max_id, ref: ref)
+      else
+        where(id: max_id)
+      end
+    end
+
+    def self.latest_status(ref = nil)
+      latest(ref).status
+    end
+
     def self.latest_successful_for(ref)
-      where(ref: ref).order(id: :desc).success.first
+      success.latest(ref).first
     end
 
     def self.truncate_sha(sha)
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 1831cc7e175586ac3270b6154776773db4ed520a..69cfc47f5bf0e06bbe561cdebb9422ed1bfb5800 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -228,13 +228,9 @@ class Commit
   def status(ref = nil)
     @statuses ||= {}
 
-    if @statuses.key?(ref)
-      @statuses[ref]
-    elsif ref
-      @statuses[ref] = pipelines.where(ref: ref).status
-    else
-      @statuses[ref] = pipelines.status
-    end
+    return @statuses[ref] if @statuses.key?(ref)
+
+    @statuses[ref] = pipelines.latest_status(ref)
   end
 
   def revert_branch_name
@@ -270,7 +266,7 @@ class Commit
     @merged_merge_request_hash ||= Hash.new do |hash, user|
       hash[user] = merged_merge_request_no_cache(user)
     end
-    
+
     @merged_merge_request_hash[current_user]
   end
 
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index 875e983448714ca1c0ca9993e6c6c5fe8eb0bb22..4359f1d7b06ed6852eed6d1139fe553859e35596 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -30,7 +30,7 @@ module Milestoneish
   end
 
   def issues_visible_to_user(user)
-    issues.visible_to_user(user)
+    IssuesFinder.new(user).execute.where(id: issues)
   end
 
   def upcoming?
diff --git a/app/models/concerns/token_authenticatable.rb b/app/models/concerns/token_authenticatable.rb
index 04d30f462101b4428c1006db830265a5661bad2a..1ca7f91dc037fdd582e32faffed91c92b89a7418 100644
--- a/app/models/concerns/token_authenticatable.rb
+++ b/app/models/concerns/token_authenticatable.rb
@@ -39,6 +39,10 @@ module TokenAuthenticatable
         current_token.blank? ? write_new_token(token_field) : current_token
       end
 
+      define_method("set_#{token_field}") do |token|
+        write_attribute(token_field, token) if token
+      end
+
       define_method("ensure_#{token_field}!") do
         send("reset_#{token_field}!") if read_attribute(token_field).blank?
         read_attribute(token_field)
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 96700143ddd9893eb4d930eb4f5e4a8e59670e65..8ef1c841ea32e2368fb8ac31ab264a9d910bef28 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -1,9 +1,15 @@
 class Environment < ActiveRecord::Base
+  # Used to generate random suffixes for the slug
+  NUMBERS = '0'..'9'
+  SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
   belongs_to :project, required: true, validate: true
 
   has_many :deployments
 
   before_validation :nullify_external_url
+  before_validation :generate_slug, if: ->(env) { env.slug.blank? }
+
   before_save :set_environment_type
 
   validates :name,
@@ -13,6 +19,13 @@ class Environment < ActiveRecord::Base
             format: { with: Gitlab::Regex.environment_name_regex,
                       message: Gitlab::Regex.environment_name_regex_message }
 
+  validates :slug,
+            presence: true,
+            uniqueness: { scope: :project_id },
+            length: { maximum: 24 },
+            format: { with: Gitlab::Regex.environment_slug_regex,
+                      message: Gitlab::Regex.environment_slug_regex_message }
+
   validates :external_url,
             uniqueness: { scope: :project_id },
             length: { maximum: 255 },
@@ -37,6 +50,13 @@ class Environment < ActiveRecord::Base
     state :stopped
   end
 
+  def predefined_variables
+    [
+      { key: 'CI_ENVIRONMENT_NAME', value: name, public: true },
+      { key: 'CI_ENVIRONMENT_SLUG', value: slug, public: true },
+    ]
+  end
+
   def recently_updated_on_branch?(ref)
     ref.to_s == last_deployment.try(:ref)
   end
@@ -107,4 +127,41 @@ class Environment < ActiveRecord::Base
       action.expanded_environment_name == environment
     end
   end
+
+  # An environment name is not necessarily suitable for use in URLs, DNS
+  # or other third-party contexts, so provide a slugified version. A slug has
+  # the following properties:
+  #   * contains only lowercase letters (a-z), numbers (0-9), and '-'
+  #   * begins with a letter
+  #   * has a maximum length of 24 bytes (OpenShift limitation)
+  #   * cannot end with `-`
+  def generate_slug
+    # Lowercase letters and numbers only
+    slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+    # Must start with a letter
+    slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+    # Maximum length: 24 characters (OpenShift limitation)
+    slugified = slugified[0..23]
+
+    # Cannot end with a "-" character (Kubernetes label limitation)
+    slugified = slugified[0..-2] if slugified[-1] == "-"
+
+    # Add a random suffix, shortening the current string if necessary, if it
+    # has been slugified. This ensures uniqueness.
+    slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+
+    self.slug = slugified
+  end
+
+  private
+
+  # Slugifying a name may remove the uniqueness guarantee afforded by it being
+  # based on name (which must be unique). To compensate, we add a random
+  # 6-byte suffix in those circumstances. This is not *guaranteed* uniqueness,
+  # but the chance of collisions is vanishingly small
+  def random_suffix
+    (0..5).map { SUFFIX_CHARS.sample }.join
+  end
 end
diff --git a/app/models/group.rb b/app/models/group.rb
index 4248e1162d85edcdd32fbae8de65db7547957d7d..ac8a82c8c1e253b27f3ec42ec1d0119e54063720 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -83,7 +83,7 @@ class Group < Namespace
   end
 
   def human_name
-    name
+    full_name
   end
 
   def visibility_level_field
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7fe92051037649e93dd34afbc32bb90e650ddcd1..738c96e4db3d3205d6c7e1c8e4a68d53feaf92b4 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -60,61 +60,6 @@ class Issue < ActiveRecord::Base
     attributes
   end
 
-  class << self
-    private
-
-    # Returns the project that the current scope belongs to if any, nil otherwise.
-    #
-    # Examples:
-    # - my_project.issues.without_due_date.owner_project => my_project
-    # - Issue.all.owner_project => nil
-    def owner_project
-      # No owner if we're not being called from an association
-      return unless all.respond_to?(:proxy_association)
-
-      owner = all.proxy_association.owner
-
-      # Check if the association is or belongs to a project
-      if owner.is_a?(Project)
-        owner
-      else
-        begin
-          owner.association(:project).target
-        rescue ActiveRecord::AssociationNotFoundError
-          nil
-        end
-      end
-    end
-  end
-
-  def self.visible_to_user(user)
-    return where('issues.confidential IS NULL OR issues.confidential IS FALSE') if user.blank?
-    return all if user.admin?
-
-    # Check if we are scoped to a specific project's issues
-    if owner_project
-      if owner_project.team.member?(user, Gitlab::Access::REPORTER)
-        # If the project is authorized for the user, they can see all issues in the project
-        return all
-      else
-        # else only non confidential and authored/assigned to them
-        return where('issues.confidential IS NULL OR issues.confidential IS FALSE
-          OR issues.author_id = :user_id OR issues.assignee_id = :user_id',
-          user_id: user.id)
-      end
-    end
-
-    where('
-      issues.confidential IS NULL
-      OR issues.confidential IS FALSE
-      OR (issues.confidential = TRUE
-        AND (issues.author_id = :user_id
-          OR issues.assignee_id = :user_id
-          OR issues.project_id IN(:project_ids)))',
-      user_id: user.id,
-      project_ids: user.authorized_projects(Gitlab::Access::REPORTER).select(:id))
-  end
-
   def self.reference_prefix
     '#'
   end
diff --git a/app/models/member.rb b/app/models/member.rb
index 3b65587c66b0add69feb96548d32fc120ad103aa..c585e0b450e97d8e22b29631b828eddaa19cfd39 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -57,6 +57,11 @@ class Member < ActiveRecord::Base
   scope :owners,  -> { active.where(access_level: OWNER) }
   scope :owners_and_masters,  -> { active.where(access_level: [OWNER, MASTER]) }
 
+  scope :order_name_asc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'ASC')) }
+  scope :order_name_desc, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.name', 'DESC')) }
+  scope :order_recent_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'DESC')) }
+  scope :order_oldest_sign_in, -> { left_join_users.reorder(Gitlab::Database.nulls_last_order('users.last_sign_in_at', 'ASC')) }
+
   before_validation :generate_invite_token, on: :create, if: -> (member) { member.invite_email.present? }
 
   after_create :send_invite, if: :invite?, unless: :importing?
@@ -72,6 +77,34 @@ class Member < ActiveRecord::Base
   default_value_for :notification_level, NotificationSetting.levels[:global]
 
   class << self
+    def search(query)
+      joins(:user).merge(User.search(query))
+    end
+
+    def sort(method)
+      case method.to_s
+      when 'access_level_asc' then reorder(access_level: :asc)
+      when 'access_level_desc' then reorder(access_level: :desc)
+      when 'recent_sign_in' then order_recent_sign_in
+      when 'oldest_sign_in' then order_oldest_sign_in
+      when 'last_joined' then order_created_desc
+      when 'oldest_joined' then order_created_asc
+      else
+        order_by(method)
+      end
+    end
+
+    def left_join_users
+      users = User.arel_table
+      members = Member.arel_table
+
+      member_users = members.join(users, Arel::Nodes::OuterJoin).
+                             on(members[:user_id].eq(users[:id])).
+                             join_sources
+
+      joins(member_users)
+    end
+
     def access_for_user_ids(user_ids)
       where(user_id: user_ids).has_access.pluck(:user_id, :access_level).to_h
     end
@@ -89,8 +122,8 @@ class Member < ActiveRecord::Base
       member =
         if user.is_a?(User)
           source.members.find_by(user_id: user.id) ||
-          source.requesters.find_by(user_id: user.id) ||
-          source.members.build(user_id: user.id)
+            source.requesters.find_by(user_id: user.id) ||
+            source.members.build(user_id: user.id)
         else
           source.members.build(invite_email: user)
         end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index ea3cf1cdaac46a47dfa24bb01a7e3486414e104b..b7c775777c7e68677b985ea04dadb8264c226dc3 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -452,7 +452,7 @@ class MergeRequest < ActiveRecord::Base
     should_remove_source_branch? || force_remove_source_branch?
   end
 
-  def mr_and_commit_notes
+  def related_notes
     # Fetch comments only from last 100 commits
     commits_for_notes_limit = 100
     commit_ids = commits.last(commits_for_notes_limit).map(&:id)
@@ -468,7 +468,7 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def discussions
-    @discussions ||= self.mr_and_commit_notes.
+    @discussions ||= self.related_notes.
       inc_relations_for_view.
       fresh.
       discussions
@@ -568,6 +568,19 @@ class MergeRequest < ActiveRecord::Base
     end
   end
 
+  def issues_mentioned_but_not_closing(current_user = self.author)
+    return [] unless target_branch == project.default_branch
+
+    ext = Gitlab::ReferenceExtractor.new(project, current_user)
+    ext.analyze(description)
+
+    issues = ext.issues
+    closing_issues = Gitlab::ClosingIssueExtractor.new(project, current_user).
+      closed_by_message(description)
+
+    issues - closing_issues
+  end
+
   def target_project_path
     if target_project
       target_project.path_with_namespace
@@ -612,13 +625,24 @@ class MergeRequest < ActiveRecord::Base
     self.target_project.repository.branch_names.include?(self.target_branch)
   end
 
-  def merge_commit_message
-    message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
-    message << "#{title}\n\n"
-    message << "#{description}\n\n" if description.present?
+  def merge_commit_message(include_description: false)
+    closes_issues_references = closes_issues.map do |issue|
+      issue.to_reference(target_project)
+    end
+
+    message = [
+      "Merge branch '#{source_branch}' into '#{target_branch}'",
+      title
+    ]
+
+    if !include_description && closes_issues_references.present?
+      message << "Closes #{closes_issues_references.to_sentence}"
+    end
+
+    message << "#{description}" if include_description && description.present?
     message << "See merge request #{to_reference}"
 
-    message
+    message.join("\n\n")
   end
 
   def reset_merge_when_build_succeeds
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 37374044551da69fe9a5e3f8d75ca06c4035c304..fd42f2328d88cf590abc9169cba951dc5994f056 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -17,14 +17,13 @@ class Namespace < ActiveRecord::Base
   validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
   validates :name,
     presence: true,
-    uniqueness: true,
+    uniqueness: { scope: :parent_id },
     length: { maximum: 255 },
     namespace_name: true
 
   validates :description, length: { maximum: 255 }
   validates :path,
     presence: true,
-    uniqueness: { case_sensitive: false },
     length: { maximum: 255 },
     namespace: true
 
@@ -162,6 +161,19 @@ class Namespace < ActiveRecord::Base
     end
   end
 
+  def full_name
+    @full_name ||=
+      if parent
+        parent.full_name + ' / ' + name
+      else
+        name
+      end
+  end
+
+  def parents
+    @parents ||= parent ? parent.parents + [parent] : []
+  end
+
   private
 
   def repository_storage_paths
diff --git a/app/models/network/graph.rb b/app/models/network/graph.rb
index 345041a6ad18b233cb77a5a016ab5cd5ba670a49..b524ca50ee820e041153a570700dd35436b4b308 100644
--- a/app/models/network/graph.rb
+++ b/app/models/network/graph.rb
@@ -161,8 +161,8 @@ module Network
     def is_overlap?(range, overlap_space)
       range.each do |i|
         if i != range.first &&
-          i != range.last &&
-          @commits[i].spaces.include?(overlap_space)
+            i != range.last &&
+            @commits[i].spaces.include?(overlap_space)
 
           return true
         end
diff --git a/app/models/note.rb b/app/models/note.rb
index 08bd08743efb56df08ec9a82d0fa9231fd17f5a6..0c1b05dabf28909b5aaedec606ebd1308b515a1d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -107,23 +107,6 @@ class Note < ActiveRecord::Base
       Discussion.for_diff_notes(active_notes).
         map { |d| [d.line_code, d] }.to_h
     end
-
-    # Searches for notes matching the given query.
-    #
-    # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
-    #
-    # query   - The search query as a String.
-    # as_user - Limit results to those viewable by a specific user
-    #
-    # Returns an ActiveRecord::Relation.
-    def search(query, as_user: nil)
-      table   = arel_table
-      pattern = "%#{query}%"
-
-      Note.joins('LEFT JOIN issues ON issues.id = noteable_id').
-        where(table[:note].matches(pattern)).
-        merge(Issue.visible_to_user(as_user))
-    end
   end
 
   def cross_reference?
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index c4b095e0c0471c82e03e5cf97cac05d4fdaa1e40..10a34c42fd8e828c5fb8c9c07d40e889b2c95c20 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -2,6 +2,8 @@ class PersonalAccessToken < ActiveRecord::Base
   include TokenAuthenticatable
   add_authentication_token_field :token
 
+  serialize :scopes, Array
+
   belongs_to :user
 
   scope :active, -> { where(revoked: false).where("expires_at >= NOW() OR expires_at IS NULL") }
diff --git a/app/models/project.rb b/app/models/project.rb
index 77d740081c6cb967042b8d8fa8235a009c7d18d0..5d5d6737dad30a1f14b3b981be1388a2d00c93cc 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -95,7 +95,8 @@ class Project < ActiveRecord::Base
   has_one :asana_service, dependent: :destroy
   has_one :gemnasium_service, dependent: :destroy
   has_one :mattermost_slash_commands_service, dependent: :destroy
-  has_one :slack_service, dependent: :destroy
+  has_one :mattermost_notification_service, dependent: :destroy
+  has_one :slack_notification_service, dependent: :destroy
   has_one :buildkite_service, dependent: :destroy
   has_one :bamboo_service, dependent: :destroy
   has_one :teamcity_service, dependent: :destroy
@@ -106,6 +107,7 @@ class Project < ActiveRecord::Base
   has_one :bugzilla_service, dependent: :destroy
   has_one :gitlab_issue_tracker_service, dependent: :destroy, inverse_of: :project
   has_one :external_wiki_service, dependent: :destroy
+  has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
 
   has_one  :forked_project_link,  dependent: :destroy, foreign_key: "forked_to_project_id"
   has_one  :forked_from_project,  through:   :forked_project_link
@@ -742,6 +744,14 @@ class Project < ActiveRecord::Base
     @ci_service ||= ci_services.reorder(nil).find_by(active: true)
   end
 
+  def deployment_services
+    services.where(category: :deployment)
+  end
+
+  def deployment_service
+    @deployment_service ||= deployment_services.reorder(nil).find_by(active: true)
+  end
+
   def jira_tracker?
     issues_tracker.to_param == 'jira'
   end
@@ -1220,6 +1230,12 @@ class Project < ActiveRecord::Base
     end
   end
 
+  def deployment_variables
+    return [] unless deployment_service
+
+    deployment_service.predefined_variables
+  end
+
   def append_or_update_attribute(name, value)
     old_values = public_send(name.to_s)
 
diff --git a/app/models/project_services/slack_service/base_message.rb b/app/models/project_services/chat_message/base_message.rb
similarity index 96%
rename from app/models/project_services/slack_service/base_message.rb
rename to app/models/project_services/chat_message/base_message.rb
index f1182824687885584ead06828ff30ab53ebd3f28..a03605d01fb5e507addb059755e0df1fa7b430cf 100644
--- a/app/models/project_services/slack_service/base_message.rb
+++ b/app/models/project_services/chat_message/base_message.rb
@@ -1,6 +1,6 @@
 require 'slack-notifier'
 
-class SlackService
+module ChatMessage
   class BaseMessage
     def initialize(params)
       raise NotImplementedError
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/chat_message/build_message.rb
similarity index 98%
rename from app/models/project_services/slack_service/build_message.rb
rename to app/models/project_services/chat_message/build_message.rb
index 0fca4267bad8eb3f9246c40cf09e24a823526d0a..53e35cb21bf0692e3e83ac1e7520209ea1d67bb3 100644
--- a/app/models/project_services/slack_service/build_message.rb
+++ b/app/models/project_services/chat_message/build_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
   class BuildMessage < BaseMessage
     attr_reader :sha
     attr_reader :ref_type
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/chat_message/issue_message.rb
similarity index 98%
rename from app/models/project_services/slack_service/issue_message.rb
rename to app/models/project_services/chat_message/issue_message.rb
index cd87a79d0c607383f6c92b0c66cda190f095dcd4..14fd64e53321fe644ca98e2842e14e690c4ac6a8 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/chat_message/issue_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
   class IssueMessage < BaseMessage
     attr_reader :user_name
     attr_reader :title
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/chat_message/merge_message.rb
similarity index 98%
rename from app/models/project_services/slack_service/merge_message.rb
rename to app/models/project_services/chat_message/merge_message.rb
index b7615c960686177ef046d6b1ea25e46b6be15c8b..ab5e8b2416775b996bdd3b300bee23765af38fe4 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/chat_message/merge_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
   class MergeMessage < BaseMessage
     attr_reader :user_name
     attr_reader :project_name
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/chat_message/note_message.rb
similarity index 99%
rename from app/models/project_services/slack_service/note_message.rb
rename to app/models/project_services/chat_message/note_message.rb
index 797c5937f097d8084d2bde57da39231448157862..ca1d72070349c6b1a276fd053b114c71d01fdbfb 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/chat_message/note_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
   class NoteMessage < BaseMessage
     attr_reader :message
     attr_reader :user_name
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb
similarity index 95%
rename from app/models/project_services/slack_service/pipeline_message.rb
rename to app/models/project_services/chat_message/pipeline_message.rb
index f8d03c0e2fa3a238d7ec8e226292d4adf2de6415..210027565a8cfe09e04596f95236a180bf439786 100644
--- a/app/models/project_services/slack_service/pipeline_message.rb
+++ b/app/models/project_services/chat_message/pipeline_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
   class PipelineMessage < BaseMessage
     attr_reader :ref_type, :ref, :status, :project_name, :project_url,
                 :user_name, :duration, :pipeline_id
@@ -13,7 +13,7 @@ class SlackService
 
       @project_name = data[:project][:path_with_namespace]
       @project_url = data[:project][:web_url]
-      @user_name = data[:user] && data[:user][:name]
+      @user_name = (data[:user] && data[:user][:name]) || 'API'
     end
 
     def pretext
diff --git a/app/models/project_services/slack_service/push_message.rb b/app/models/project_services/chat_message/push_message.rb
similarity index 99%
rename from app/models/project_services/slack_service/push_message.rb
rename to app/models/project_services/chat_message/push_message.rb
index b26f3e9ddce9f392d6cc06e9446fb03a49fa1de3..2d73b71ec376b11981912d7123a04a41e0dc684d 100644
--- a/app/models/project_services/slack_service/push_message.rb
+++ b/app/models/project_services/chat_message/push_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
   class PushMessage < BaseMessage
     attr_reader :after
     attr_reader :before
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/chat_message/wiki_page_message.rb
similarity index 98%
rename from app/models/project_services/slack_service/wiki_page_message.rb
rename to app/models/project_services/chat_message/wiki_page_message.rb
index 160ca3ac11523fa32fe5c5a36f640e1050401c11..134083e4504d639a67df5c54873118d8f47d9049 100644
--- a/app/models/project_services/slack_service/wiki_page_message.rb
+++ b/app/models/project_services/chat_message/wiki_page_message.rb
@@ -1,4 +1,4 @@
-class SlackService
+module ChatMessage
   class WikiPageMessage < BaseMessage
     attr_reader :user_name
     attr_reader :title
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/chat_notification_service.rb
similarity index 65%
rename from app/models/project_services/slack_service.rb
rename to app/models/project_services/chat_notification_service.rb
index e1b937817f4649d7bfd4adbdd10665da35b023ef..b05569877210677134332d2d35801efcfd12100b 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -1,6 +1,13 @@
-class SlackService < Service
+# Base class for Chat notifications services
+# This class is not meant to be used directly, but only to inherit from.
+class ChatNotificationService < Service
+  include ChatMessage
+
+  default_value_for :category, 'chat'
+
   prop_accessor :webhook, :username, :channel
   boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+
   validates :webhook, presence: true, url: true, if: :activated?
 
   def initialize_properties
@@ -14,35 +21,8 @@ class SlackService < Service
     end
   end
 
-  def title
-    'Slack'
-  end
-
-  def description
-    'A team communication tool for the 21st century'
-  end
-
-  def to_param
-    'slack'
-  end
-
-  def help
-    'This service sends notifications to your Slack channel.<br/>
-    To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel,
-    and enter the Webhook URL below.'
-  end
-
-  def fields
-    default_fields =
-      [
-        { type: 'text', name: 'webhook',   placeholder: 'https://hooks.slack.com/services/...' },
-        { type: 'text', name: 'username', placeholder: 'username' },
-        { type: 'text', name: 'channel', placeholder: "#general" },
-        { type: 'checkbox', name: 'notify_only_broken_builds' },
-        { type: 'checkbox', name: 'notify_only_broken_pipelines' },
-      ]
-
-    default_fields + build_event_channels
+  def can_test?
+    valid?
   end
 
   def supported_events
@@ -67,21 +47,16 @@ class SlackService < Service
 
     message = get_message(object_kind, data)
 
-    if message
-      opt = {}
-
-      event_channel = get_channel_field(object_kind) || channel
+    return false unless message
 
-      opt[:channel] = event_channel if event_channel
-      opt[:username] = username if username
+    opt = {}
 
-      notifier = Slack::Notifier.new(webhook, opt)
-      notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
+    opt[:channel]  = get_channel_field(object_kind).presence || channel || default_channel
+    opt[:username] = username if username
+    notifier = Slack::Notifier.new(webhook, opt)
+    notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
 
-      true
-    else
-      false
-    end
+    true
   end
 
   def event_channel_names
@@ -96,6 +71,10 @@ class SlackService < Service
     fields.reject { |field| field[:name].end_with?('channel') }
   end
 
+  def default_channel
+    raise NotImplementedError
+  end
+
   private
 
   def get_message(object_kind, data)
@@ -124,7 +103,7 @@ class SlackService < Service
 
   def build_event_channels
     supported_events.reduce([]) do |channels, event|
-      channels << { type: 'text', name: event_channel_name(event), placeholder: "#general" }
+      channels << { type: 'text', name: event_channel_name(event), placeholder: default_channel }
     end
   end
 
@@ -166,11 +145,3 @@ class SlackService < Service
     end
   end
 end
-
-require "slack_service/issue_message"
-require "slack_service/push_message"
-require "slack_service/merge_message"
-require "slack_service/note_message"
-require "slack_service/build_message"
-require "slack_service/pipeline_message"
-require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/chat_service.rb b/app/models/project_services/chat_service.rb
index d36beff5fa6e69829fac47db912b808f628e2d5a..574788462deddcc160b75a23b2c8b494640d4682 100644
--- a/app/models/project_services/chat_service.rb
+++ b/app/models/project_services/chat_service.rb
@@ -1,5 +1,5 @@
 # Base class for Chat services
-# This class is not meant to be used directly, but only to inherrit from.
+# This class is not meant to be used directly, but only to inherit from.
 class ChatService < Service
   default_value_for :category, 'chat'
 
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..da6be9dd7b7ee19e7351eadd889f414a0dc4671f
--- /dev/null
+++ b/app/models/project_services/deployment_service.rb
@@ -0,0 +1,15 @@
+# Base class for deployment services
+#
+# These services integrate with a deployment solution like Kubernetes/OpenShift,
+# Mesosphere, etc, to provide additional features to environments.
+class DeploymentService < Service
+  default_value_for :category, 'deployment'
+
+  def supported_events
+    []
+  end
+
+  def predefined_variables
+    []
+  end
+end
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 207bb816ad152b538729bb8fd651193048846708..bce2cdd55165ee3060e1eda1bbeaa930ae0cde22 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -85,8 +85,8 @@ class IssueTrackerService < Service
 
   def enabled_in_gitlab_config
     Gitlab.config.issues_tracker &&
-    Gitlab.config.issues_tracker.values.any? &&
-    issues_tracker
+      Gitlab.config.issues_tracker.values.any? &&
+      issues_tracker
   end
 
   def issues_tracker
diff --git a/app/models/project_services/kubernetes_service.rb b/app/models/project_services/kubernetes_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f5fbf8b353b375cd073baab96bd49aec12ae4906
--- /dev/null
+++ b/app/models/project_services/kubernetes_service.rb
@@ -0,0 +1,128 @@
+class KubernetesService < DeploymentService
+  # Namespace defaults to the project path, but can be overridden in case that
+  # is an invalid or inappropriate name
+  prop_accessor :namespace
+
+  #  Access to kubernetes is directly through the API
+  prop_accessor :api_url
+
+  # Bearer authentication
+  # TODO:  user/password auth, client certificates
+  prop_accessor :token
+
+  # Provide a custom CA bundle for self-signed deployments
+  prop_accessor :ca_pem
+
+  with_options presence: true, if: :activated? do
+    validates :api_url, url: true
+    validates :token
+
+    validates :namespace,
+      format: {
+        with: Gitlab::Regex.kubernetes_namespace_regex,
+        message: Gitlab::Regex.kubernetes_namespace_regex_message,
+      },
+      length: 1..63
+  end
+
+  def initialize_properties
+    if properties.nil?
+      self.properties = {}
+      self.namespace = project.path if project.present?
+    end
+  end
+
+  def title
+    'Kubernetes'
+  end
+
+  def description
+    'Kubernetes / Openshift integration'
+  end
+
+  def help
+    ''
+  end
+
+  def to_param
+    'kubernetes'
+  end
+
+  def fields
+    [
+        { type: 'text',
+          name: 'namespace',
+          title: 'Kubernetes namespace',
+          placeholder: 'Kubernetes namespace',
+        },
+        { type: 'text',
+          name: 'api_url',
+          title: 'API URL',
+          placeholder: 'Kubernetes API URL, like https://kube.example.com/',
+        },
+        { type: 'text',
+          name: 'token',
+          title: 'Service token',
+          placeholder: 'Service token',
+        },
+        { type: 'textarea',
+          name: 'ca_pem',
+          title: 'Custom CA bundle',
+          placeholder: 'Certificate Authority bundle (PEM format)',
+        },
+    ]
+  end
+
+  # Check we can connect to the Kubernetes API
+  def test(*args)
+    kubeclient = build_kubeclient
+    kubeclient.discover
+
+    { success: kubeclient.discovered, result: "Checked API discovery endpoint" }
+  rescue => err
+    { success: false, result: err }
+  end
+
+  def predefined_variables
+    variables = [
+      { key: 'KUBE_URL', value: api_url, public: true },
+      { key: 'KUBE_TOKEN', value: token, public: false },
+      { key: 'KUBE_NAMESPACE', value: namespace, public: true }
+    ]
+    variables << { key: 'KUBE_CA_PEM', value: ca_pem, public: true } if ca_pem.present?
+    variables
+  end
+
+  private
+
+  def build_kubeclient(api_path = '/api', api_version = 'v1')
+    return nil unless api_url && namespace && token
+
+    url = URI.parse(api_url)
+    url.path = url.path[0..-2] if url.path[-1] == "/"
+    url.path += api_path
+
+    ::Kubeclient::Client.new(
+      url,
+      api_version,
+      ssl_options: kubeclient_ssl_options,
+      auth_options: kubeclient_auth_options,
+      http_proxy_uri: ENV['http_proxy']
+    )
+  end
+
+  def kubeclient_ssl_options
+    opts = { verify_ssl: OpenSSL::SSL::VERIFY_PEER }
+
+    if ca_pem.present?
+      opts[:cert_store] = OpenSSL::X509::Store.new
+      opts[:cert_store].add_cert(OpenSSL::X509::Certificate.new(ca_pem))
+    end
+
+    opts
+  end
+
+  def kubeclient_auth_options
+    { bearer_token: token }
+  end
+end
diff --git a/app/models/project_services/mattermost_notification_service.rb b/app/models/project_services/mattermost_notification_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..de18c4b1f00cda5d5fa68873b940c3540e5b060c
--- /dev/null
+++ b/app/models/project_services/mattermost_notification_service.rb
@@ -0,0 +1,41 @@
+class MattermostNotificationService < ChatNotificationService
+  def title
+    'Mattermost notifications'
+  end
+
+  def description
+    'Receive event notifications in Mattermost'
+  end
+
+  def to_param
+    'mattermost_notification'
+  end
+
+  def help
+    'This service sends notifications about projects events to Mattermost channels.<br />
+    To set up this service:
+    <ol>
+      <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#enabling-incoming-webhooks">Enable incoming webhooks</a> in your Mattermost installation. </li>
+      <li><a href="https://docs.mattermost.com/developer/webhooks-incoming.html#creating-integrations-using-incoming-webhooks">Add an incoming webhook</a> in your Mattermost team. The default channel can be overridden for each event. </li>
+      <li>Paste the webhook <strong>URL</strong> into the field bellow. </li>
+      <li>Select events below to enable notifications. The channel and username are optional. </li>
+    </ol>'
+  end
+
+  def fields
+    default_fields + build_event_channels
+  end
+
+  def default_fields
+    [
+      { type: 'text', name: 'webhook', placeholder: 'http://mattermost_host/hooks/...' },
+      { type: 'text', name: 'username', placeholder: 'username' },
+      { type: 'checkbox', name: 'notify_only_broken_builds' },
+      { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+    ]
+  end
+
+  def default_channel
+    "#town-square"
+  end
+end
diff --git a/app/models/project_services/slack_notification_service.rb b/app/models/project_services/slack_notification_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3cbf89efba4e32b0b1f6c92d4780842abbc9ff4b
--- /dev/null
+++ b/app/models/project_services/slack_notification_service.rb
@@ -0,0 +1,40 @@
+class SlackNotificationService < ChatNotificationService
+  def title
+    'Slack notifications'
+  end
+
+  def description
+    'Receive event notifications in Slack'
+  end
+
+  def to_param
+    'slack_notification'
+  end
+
+  def help
+    'This service sends notifications about projects events to Slack channels.<br />
+    To setup this service:
+    <ol>
+      <li><a href="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks">Add an incoming webhook</a> in your Slack team. The default channel can be overridden for each event. </li>
+      <li>Paste the <strong>Webhook URL</strong> into the field below. </li>
+      <li>Select events below to enable notifications. The channel and username are optional. </li>
+    </ol>'
+  end
+
+  def fields
+    default_fields + build_event_channels
+  end
+
+  def default_fields
+    [
+      { type: 'text', name: 'webhook', placeholder: 'https://hooks.slack.com/services/...' },
+      { type: 'text', name: 'username', placeholder: 'username' },
+      { type: 'checkbox', name: 'notify_only_broken_builds' },
+      { type: 'checkbox', name: 'notify_only_broken_pipelines' },
+    ]
+  end
+
+  def default_channel
+    "#general"
+  end
+end
diff --git a/app/models/service.rb b/app/models/service.rb
index 0c36acfc1b72bc259dfac81690089959071fbec8..0bbab078cf6894be36ef1af5755b682a1ae4012b 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -214,12 +214,14 @@ class Service < ActiveRecord::Base
       hipchat
       irker
       jira
+      kubernetes
       mattermost_slash_commands
       pipelines_email
       pivotaltracker
       pushover
       redmine
-      slack
+      mattermost_notification
+      slack_notification
       teamcity
     ]
   end
diff --git a/app/models/user.rb b/app/models/user.rb
index 1bd28203523f460de46f88d6a04d8922b6b773c1..3a17c98eff6d4f5a202fd557ed8131a798797c43 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -178,6 +178,8 @@ class User < ActiveRecord::Base
   scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
   scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
   scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
+  scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
+  scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
 
   def self.with_two_factor
     joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
@@ -205,8 +207,8 @@ class User < ActiveRecord::Base
 
     def sort(method)
       case method.to_s
-      when 'recent_sign_in' then reorder(last_sign_in_at: :desc)
-      when 'oldest_sign_in' then reorder(last_sign_in_at: :asc)
+      when 'recent_sign_in' then order_recent_sign_in
+      when 'oldest_sign_in' then order_oldest_sign_in
       else
         order_by(method)
       end
@@ -390,7 +392,7 @@ class User < ActiveRecord::Base
   def namespace_uniq
     # Return early if username already failed the first uniqueness validation
     return if errors.key?(:username) &&
-      errors[:username].include?('has already been taken')
+        errors[:username].include?('has already been taken')
 
     existing_namespace = Namespace.by_path(username)
     if existing_namespace && existing_namespace != namespace
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 83847466ee23ef3e35ce1b79a31f400961c8efd0..5326061bd077f804317ea0c4f2f94647cfa9739b 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -12,7 +12,7 @@ class NotePolicy < BasePolicy
     end
 
     if @subject.for_merge_request? &&
-       @subject.noteable.author == @user
+        @subject.noteable.author == @user
       can! :resolve_note
     end
   end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index d5aadfce76a8267e4842858c3ffaca0567f157ba..b5db9c12622945da490221b484dff873f38f9cb5 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -3,7 +3,7 @@ class ProjectPolicy < BasePolicy
     team_access!(user)
 
     owner = project.owner == user ||
-            (project.group && project.group.has_owner?(user))
+      (project.group && project.group.has_owner?(user))
 
     owner_access! if user.admin? || owner
     team_member_owner_access! if owner
@@ -13,7 +13,7 @@ class ProjectPolicy < BasePolicy
       public_access!
 
       if project.request_access_enabled &&
-         !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
+          !(owner || user.admin? || project.team.member?(user) || project_group_member?(user))
         can! :request_access
       end
     end
@@ -244,10 +244,10 @@ class ProjectPolicy < BasePolicy
 
   def project_group_member?(user)
     project.group &&
-    (
-      project.group.members.exists?(user_id: user.id) ||
-      project.group.requesters.exists?(user_id: user.id)
-    )
+      (
+        project.group.members.exists?(user_id: user.id) ||
+        project.group.requesters.exists?(user_id: user.id)
+      )
   end
 
   def named_abilities(name)
diff --git a/app/services/access_token_validation_service.rb b/app/services/access_token_validation_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ddaaed90e5befeec1f27f45c155bc8c3ca583a10
--- /dev/null
+++ b/app/services/access_token_validation_service.rb
@@ -0,0 +1,32 @@
+AccessTokenValidationService = Struct.new(:token) do
+  # Results:
+  VALID = :valid
+  EXPIRED = :expired
+  REVOKED = :revoked
+  INSUFFICIENT_SCOPE = :insufficient_scope
+
+  def validate(scopes: [])
+    if token.expired?
+      return EXPIRED
+
+    elsif token.revoked?
+      return REVOKED
+
+    elsif !self.include_any_scope?(scopes)
+      return INSUFFICIENT_SCOPE
+
+    else
+      return VALID
+    end
+  end
+
+  # True if the token's scope contains any of the passed scopes.
+  def include_any_scope?(scopes)
+    if scopes.blank?
+      true
+    else
+      # Check whether the token is allowed access to any of the required scopes.
+      Set.new(scopes).intersection(Set.new(token.scopes)).present?
+    end
+  end
+end
diff --git a/app/services/ci/create_pipeline_builds_service.rb b/app/services/ci/create_pipeline_builds_service.rb
index 005014fa1ded2e94b350f06aad0ec2bf1613106f..b7da3f8e7eb04dbdf540608ded89e17119157c8d 100644
--- a/app/services/ci/create_pipeline_builds_service.rb
+++ b/app/services/ci/create_pipeline_builds_service.rb
@@ -10,18 +10,29 @@ module Ci
       end
     end
 
+    def project
+      pipeline.project
+    end
+
     private
 
     def create_build(build_attributes)
       build_attributes = build_attributes.merge(
         pipeline: pipeline,
-        project: pipeline.project,
+        project: project,
         ref: pipeline.ref,
         tag: pipeline.tag,
         user: current_user,
         trigger_request: trigger_request
       )
-      pipeline.builds.create(build_attributes)
+      build = pipeline.builds.create(build_attributes)
+
+      # Create the environment before the build starts. This sets its slug and
+      # makes it available as an environment variable
+      project.environments.find_or_create_by(name: build.expanded_environment_name) if
+        build.has_environment?
+
+      build
     end
 
     def new_builds
diff --git a/app/services/ci/image_for_build_service.rb b/app/services/ci/image_for_build_service.rb
index 75d847d5bee9043d35ce0ed33a39163c064f12d0..240ddabec36cf73e5ea64dfab68ee26c7a87acbf 100644
--- a/app/services/ci/image_for_build_service.rb
+++ b/app/services/ci/image_for_build_service.rb
@@ -1,13 +1,13 @@
 module Ci
   class ImageForBuildService
     def execute(project, opts)
-      sha = opts[:sha] || ref_sha(project, opts[:ref])
-
+      ref = opts[:ref]
+      sha = opts[:sha] || ref_sha(project, ref)
       pipelines = project.pipelines.where(sha: sha)
-      pipelines = pipelines.where(ref: opts[:ref]) if opts[:ref]
-      image_name = image_for_status(pipelines.status)
 
+      image_name = image_for_status(pipelines.latest_status(ref))
       image_path = Rails.root.join('public/ci', image_name)
+
       OpenStruct.new(path: image_path, name: image_name)
     end
 
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 99ad12b1003c3a068099055666d16e63110e9046..fff2273f402df665a02b3649bfb2fe9c46774e67 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -5,7 +5,7 @@ module Groups
       new_visibility = params[:visibility_level]
       if new_visibility && new_visibility.to_i != group.visibility_level
         unless can?(current_user, :change_visibility_level, group) &&
-          Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+            Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
 
           deny_visibility_level(group, new_visibility)
           return group
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index b5f63cc5a1a0592d2e68dd988a94c86bf8e0af81..ab3d2a9a0cd5ed7ed557a99951d3f382a3481327 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -184,7 +184,8 @@ class IssuableBaseService < BaseService
     old_labels = issuable.labels.to_a
     old_mentioned_users = issuable.mentioned_users.to_a
 
-    params[:label_ids] = process_label_ids(params, existing_label_ids: issuable.label_ids)
+    label_ids = process_label_ids(params, existing_label_ids: issuable.label_ids)
+    params[:label_ids] = label_ids if labels_changing?(issuable.label_ids, label_ids)
 
     if params.present? && update_issuable(issuable, params)
       # We do not touch as it will affect a update on updated_at field
@@ -201,6 +202,10 @@ class IssuableBaseService < BaseService
     issuable
   end
 
+  def labels_changing?(old_label_ids, new_label_ids)
+    old_label_ids.sort != new_label_ids.sort
+  end
+
   def change_state(issuable)
     case params.delete(:state_event)
     when 'reopen'
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index a2111b3806ba7fbc710315feb762dbe68e389d76..78cbf94ec69c03962ed545cf5d5268f689fe1eb1 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -10,7 +10,7 @@ module Issues
       end
 
       if issue.previous_changes.include?('title') ||
-         issue.previous_changes.include?('description')
+          issue.previous_changes.include?('description')
         todo_service.update_issue(issue, current_user)
       end
 
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index bebfca7537bf25caa22e1d7e443c5af11a5ed2d0..6a7393a9921728a1dcc04e72a1c9c9bf5da915ee 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -42,7 +42,7 @@ module MergeRequests
       end
 
       if merge_request.source_project == merge_request.target_project &&
-         merge_request.target_branch == merge_request.source_branch
+          merge_request.target_branch == merge_request.source_branch
 
         messages << 'You must select different branches'
       end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index fda0da19d87af273c0c394a0fc95c23b68a7b147..ad16ef8c70ffd5d72f002ef1454675623acc7819 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -25,7 +25,7 @@ module MergeRequests
       end
 
       if merge_request.previous_changes.include?('title') ||
-         merge_request.previous_changes.include?('description')
+          merge_request.previous_changes.include?('description')
         todo_service.update_merge_request(merge_request, current_user)
       end
 
diff --git a/app/services/oauth2/access_token_validation_service.rb b/app/services/oauth2/access_token_validation_service.rb
deleted file mode 100644
index 264fdccde8fdf2e286fabf274ccdd8e339daa9cb..0000000000000000000000000000000000000000
--- a/app/services/oauth2/access_token_validation_service.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-module Oauth2::AccessTokenValidationService
-  # Results:
-  VALID = :valid
-  EXPIRED = :expired
-  REVOKED = :revoked
-  INSUFFICIENT_SCOPE = :insufficient_scope
-
-  class << self
-    def validate(token, scopes: [])
-      if token.expired?
-        return EXPIRED
-
-      elsif token.revoked?
-        return REVOKED
-
-      elsif !self.sufficient_scope?(token, scopes)
-        return INSUFFICIENT_SCOPE
-
-      else
-        return VALID
-      end
-    end
-
-    protected
-
-    # True if the token's scope is a superset of required scopes,
-    # or the required scopes is empty.
-    def sufficient_scope?(token, scopes)
-      if scopes.blank?
-        # if no any scopes required, the scopes of token is sufficient.
-        return true
-      else
-        # If there are scopes required, then check whether
-        # the set of authorized scopes is a superset of the set of required scopes
-        required_scopes = Set.new(scopes)
-        authorized_scopes = Set.new(token.scopes)
-
-        return authorized_scopes >= required_scopes
-      end
-    end
-  end
-end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 921ca6748d3c6cd9dbb10298bbeed6c3e6ebf3d1..8a6af8d8adafd88d69cc4ec8b5f5ef8dc83088ad 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -6,7 +6,7 @@ module Projects
 
       if new_visibility && new_visibility.to_i != project.visibility_level
         unless can?(current_user, :change_visibility_level, project) &&
-          Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+            Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
 
           deny_visibility_level(project, new_visibility)
           return project
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index b6c52ddac7a6a5a16a9e7d327e1ffbcae8a1d35b..86f317dcd18f51d691c78e01e48e0614234c91fe 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -1,4 +1,4 @@
-class ArtifactUploader < CarrierWave::Uploader::Base
+class ArtifactUploader < GitlabUploader
   storage :file
 
   attr_accessor :build, :field
@@ -38,12 +38,4 @@ class ArtifactUploader < CarrierWave::Uploader::Base
   def exists?
     file.try(:exists?)
   end
-
-  def move_to_cache
-    true
-  end
-
-  def move_to_store
-    true
-  end
 end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index fb3b5dfecd06a1d69613ea95a536d5c775dad9a7..cfcb877cc3e5b83df013184465d8e3959f0ca5f3 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -1,4 +1,4 @@
-class AttachmentUploader < CarrierWave::Uploader::Base
+class AttachmentUploader < GitlabUploader
   include UploaderHelper
 
   storage :file
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index 38683fdf6d7cb47a274e48a205770df7dd481dc1..a1ecb7bc00b799d2b7ae8ca6119790a0c38f083a 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -1,4 +1,4 @@
-class AvatarUploader < CarrierWave::Uploader::Base
+class AvatarUploader < GitlabUploader
   include UploaderHelper
 
   storage :file
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 3ac6030c21c747a2b76b64bf292876bdfce1e820..47bef7cd1e48675b8a8cb7e7114182b569fbefc7 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -1,4 +1,4 @@
-class FileUploader < CarrierWave::Uploader::Base
+class FileUploader < GitlabUploader
   include UploaderHelper
   MARKDOWN_PATTERN = %r{\!?\[.*?\]\(/uploads/(?<secret>[0-9a-f]{32})/(?<file>.*?)\)}
 
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
new file mode 100644
index 0000000000000000000000000000000000000000..02d7c601d6c08667b58d453738d477c194bf8a9f
--- /dev/null
+++ b/app/uploaders/gitlab_uploader.rb
@@ -0,0 +1,11 @@
+class GitlabUploader < CarrierWave::Uploader::Base
+  # Reduce disk IO
+  def move_to_cache
+    true
+  end
+
+  # Reduce disk IO
+  def move_to_store
+    true
+  end
+end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
index 4f356dd663ee303920027b11dce1b4707259918a..faab539b8e0bc5496018177cd7a949077722c2f5 100644
--- a/app/uploaders/lfs_object_uploader.rb
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -1,4 +1,4 @@
-class LfsObjectUploader < CarrierWave::Uploader::Base
+class LfsObjectUploader < GitlabUploader
   storage :file
 
   def store_dir
@@ -9,14 +9,6 @@ class LfsObjectUploader < CarrierWave::Uploader::Base
     "#{Gitlab.config.lfs.storage_path}/tmp/cache"
   end
 
-  def move_to_cache
-    true
-  end
-
-  def move_to_store
-    true
-  end
-
   def exists?
     file.try(:exists?)
   end
diff --git a/app/views/admin/applications/_form.html.haml b/app/views/admin/applications/_form.html.haml
index 4aacbb8cd7791c01485068310af6aa145e8c8e28..c689b26d6e6f02409c9c2db3b78af54bd46a2b56 100644
--- a/app/views/admin/applications/_form.html.haml
+++ b/app/views/admin/applications/_form.html.haml
@@ -18,6 +18,12 @@
           Use
           %code= Doorkeeper.configuration.native_redirect_uri
           for local tests
+
+  .form-group
+    = f.label :scopes, class: 'col-sm-2 control-label'
+    .col-sm-10
+      = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
   .form-actions
     = f.submit 'Submit', class: "btn btn-save wide"
     = link_to "Cancel", admin_applications_path, class: "btn btn-default"
diff --git a/app/views/admin/applications/show.html.haml b/app/views/admin/applications/show.html.haml
index 3eb9d61972b314a3fea5e12e7f460b69fbadf933..14683cc66e923d3a065611662097d300b01ffc73 100644
--- a/app/views/admin/applications/show.html.haml
+++ b/app/views/admin/applications/show.html.haml
@@ -2,8 +2,7 @@
 %h3.page-title
   Application: #{@application.name}
 
-
-.table-holder
+.table-holder.oauth-application-show
   %table.table
     %tr
       %td
@@ -23,6 +22,9 @@
         - @application.redirect_uri.split.each do |uri|
           %div
             %span.monospace= uri
+
+    = render "shared/tokens/scopes_list", token: @application
+
 .form-actions
   = link_to 'Edit', edit_admin_application_path(@application), class: 'btn btn-primary wide pull-left'
   = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index ec40391a3e3a83d2afce1940fdbe65e0672bd840..b5f96363230b5729ffcbd40b04321115491a9523 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -8,7 +8,7 @@
             %span
               Overview
         = nav_link(controller: [:admin, :projects]) do
-          = link_to admin_namespaces_projects_path, title: 'Projects' do
+          = link_to admin_projects_path, title: 'Projects' do
             %span
               Projects
         = nav_link(controller: :users) do
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e51f4ac1d934ac7647db796a92d861298f1eb0ab..5238623e936efdc64db0f7fdc89a11ae028035a4 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -116,7 +116,7 @@
         .light-well.well-centered
           %h4 Projects
           .data
-            = link_to admin_namespaces_projects_path do
+            = link_to admin_projects_path do
               %h1= number_with_delimiter(Project.cached_count)
             %hr
             = link_to('New Project', new_project_path, class: "btn btn-new")
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 664bb417c6a70ea4e76829fc7bc106d2b7193171..cf28f92853e145f5ade7982d2122fffa6e7122b2 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -2,7 +2,7 @@
 
 %li.group-row{ class: css_class }
   .controls
-    = link_to 'Edit', edit_admin_group_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
+    = link_to 'Edit', admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn'
     = link_to 'Delete', [:admin, group], data: { confirm: "Are you sure you want to remove #{group.name}?" }, method: :delete, class: 'btn btn-remove'
   .stats
     %span
@@ -20,7 +20,7 @@
     = image_tag group_icon(group), class: "avatar s40 hidden-xs"
   .title
     = link_to [:admin, group], class: 'group-name' do
-      = group.name
+      = group.full_name
 
   - if group.description.present?
     .description
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 40871e32913bc5b6aae16147e98339aa6f8273e4..7b0175af2140e0a6e87f156cd02be58e8840aa3d 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -1,8 +1,8 @@
 - page_title @group.name, "Groups"
 %h3.page-title
-  Group: #{@group.name}
+  Group: #{@group.full_name}
 
-  = link_to edit_admin_group_path(@group), class: "btn pull-right" do
+  = link_to admin_group_edit_path(@group), class: "btn pull-right" do
     %i.fa.fa-pencil-square-o
     Edit
 %hr
@@ -88,7 +88,7 @@
             Read more about project permissions
             %strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
 
-          = form_tag members_update_admin_group_path(@group), id: "new_project_member", class: "bulk_import", method: :put  do
+          = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put  do
             %div
               = users_select_tag(:user_ids, multiple: true, email_user: true, scope: :all)
             %div.prepend-top-10
diff --git a/app/views/admin/logs/show.html.haml b/app/views/admin/logs/show.html.haml
index 824edd171f3672d3192f5a6b0eb67ffe625b9ef9..0a954c20fcdff8ddacf116776d0d398ab81e5aed 100644
--- a/app/views/admin/logs/show.html.haml
+++ b/app/views/admin/logs/show.html.haml
@@ -8,7 +8,7 @@
 %div{ class: container_class }
   %ul.nav-links.log-tabs
     - loggers.each do |klass|
-      %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }
+      %li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }>
         = link_to klass::file_name, "##{klass::file_name_noext}",
             'data-toggle' => 'tab'
   .row-content-block
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index b37b8d4fee78728d820a7fb0cfc144cbb2016556..8bc7dc7dd51e6805f910758f9574e16c5e6fe525 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -7,7 +7,7 @@
 %div{ class: container_class }
   .top-area
     .prepend-top-default
-      = form_tag admin_namespaces_projects_path, method: :get do |f|
+      = form_tag admin_projects_path, method: :get do |f|
         .search-holder
           .search-field-holder
             = search_field_tag :name, params[:name], class: "form-control search-text-input js-search-input", id: "dashboard_search", autofocus: true, spellcheck: false, placeholder: 'Search by name'
@@ -41,19 +41,19 @@
           = button_tag "Search", class: "btn btn-primary btn-search"
 
     %ul.nav-links
-      - opts = params[:visibility_level].present? ? {} : { page: admin_namespaces_projects_path }
+      - opts = params[:visibility_level].present? ? {} : { page: admin_projects_path }
       = nav_link(opts) do
-        = link_to admin_namespaces_projects_path do
+        = link_to admin_projects_path do
           All
 
       = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do
-        = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
+        = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
           Private
       = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do
-        = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
+        = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
           Internal
       = nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do
-        = link_to admin_namespaces_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
+        = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
           Public
 
     .nav-controls
diff --git a/app/views/ci/status/_graph_badge.html.haml b/app/views/ci/status/_graph_badge.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..c7d04ab61e9c934725a148d97324652733678840
--- /dev/null
+++ b/app/views/ci/status/_graph_badge.html.haml
@@ -0,0 +1,19 @@
+-# Renders the graph node with both the status icon, status name and action icon
+
+- subject = local_assigns.fetch(:subject)
+- status = subject.detailed_status(current_user)
+- klass = "ci-status-icon ci-status-icon-#{status}"
+
+- if status.has_details?
+  = link_to status.details_path, data: { toggle: 'tooltip', title: "#{subject.name} - #{status.label}" } do
+    %span{ class: klass }= custom_icon(status.icon)
+    .ci-status-text= subject.name
+- else
+  %span{ class: klass }= custom_icon(status.icon)
+  .ci-status-text= subject.name
+
+- if status.has_action?
+  = link_to status.action_path, method: status.action_method,
+    title: status.action_title, class: 'ci-action-icon-container' do
+    %i.ci-action-icon-wrapper
+      = icon(status.action_icon, class: status.action_class)
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index b78e70ebc1e936554adaf03c090b3ddb0fd65e85..02b94beee928a0e2c031f51cd6daff2597aa918d 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -1,7 +1,7 @@
 %ul.nav-links
-  %li{ class: ("active" unless params[:filter]) }
+  %li{ class: ("active" unless params[:filter]) }>
     = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
       Your Projects
-  %li{ class: ("active" if params[:filter] == 'starred') }
+  %li{ class: ("active" if params[:filter] == 'starred') }>
     = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
       Starred Projects
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 62f52086be41ea60027a50633cdd6be005f4b470..e13f404fee2b3479eb3d525455d85a105ec35778 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -5,14 +5,14 @@
   .top-area
     %ul.nav-links
       - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
-      %li{class: "todos-pending #{todo_pending_active}"}
+      %li{class: "todos-pending #{todo_pending_active}"}>
         = link_to todos_filter_path(state: 'pending') do
           %span
             To do
           %span.badge
             = number_with_delimiter(todos_pending_count)
       - todo_done_active = ('active' if params[:state] == 'done')
-      %li{class: "todos-done #{todo_done_active}"}
+      %li{class: "todos-done #{todo_done_active}"}>
         = link_to todos_filter_path(state: 'done') do
           %span
             Done
@@ -32,7 +32,7 @@
           - if params[:project_id].present?
             = hidden_field_tag(:project_id, params[:project_id])
           = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
-            placeholder: 'Search projects', data: { data: todo_projects_options } })
+            placeholder: 'Search projects', data: { data: todo_projects_options, default_label: 'Project' } })
         .filter-item.inline
           - if params[:author_id].present?
             = hidden_field_tag(:author_id, params[:author_id])
@@ -42,12 +42,12 @@
           - if params[:type].present?
             = hidden_field_tag(:type, params[:type])
           = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
-            data: { data: todo_types_options } })
+            data: { data: todo_types_options, default_label: 'Type' } })
         .filter-item.inline.actions-filter
           - if params[:action_id].present?
             = hidden_field_tag(:action_id, params[:action_id])
           = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
-            data: { data: todo_actions_options }})
+            data: { data: todo_actions_options, default_label: 'Action' } })
         .pull-right
           .dropdown.inline.prepend-left-10
             %button.dropdown-toggle{type: 'button', 'data-toggle' => 'dropdown'}
diff --git a/app/views/doorkeeper/applications/_form.html.haml b/app/views/doorkeeper/applications/_form.html.haml
index 5c98265727a13e899f315e14d7705efd33e86b7e..b3313c7c985683c58c723303058ca3e4db3bba53 100644
--- a/app/views/doorkeeper/applications/_form.html.haml
+++ b/app/views/doorkeeper/applications/_form.html.haml
@@ -17,5 +17,9 @@
         %code= Doorkeeper.configuration.native_redirect_uri
         for local tests
 
+  .form-group
+    = f.label :scopes, class: 'label-light'
+    = render 'shared/tokens/scopes_form', prefix: 'doorkeeper_application', token: application, scopes: @scopes
+
   .prepend-top-default
     = f.submit 'Save application', class: "btn btn-create"
diff --git a/app/views/doorkeeper/applications/show.html.haml b/app/views/doorkeeper/applications/show.html.haml
index 47442b78d48e4972b4f87e59070e83502ac4a7ea..559de63d96dd3c153fe61aa0becf7de33d1411ff 100644
--- a/app/views/doorkeeper/applications/show.html.haml
+++ b/app/views/doorkeeper/applications/show.html.haml
@@ -2,7 +2,7 @@
 %h3.page-title
   Application: #{@application.name}
 
-.table-holder
+.table-holder.oauth-application-show
   %table.table
     %tr
       %td
@@ -22,6 +22,9 @@
         - @application.redirect_uri.split.each do |uri|
           %div
             %span.monospace= uri
+
+    = render "shared/tokens/scopes_list", token: @application
+
 .form-actions
   = link_to 'Edit', edit_oauth_application_path(@application), class: 'btn btn-primary wide pull-left'
   = render 'delete_form', application: @application, submit_btn_css: 'btn btn-danger prepend-left-10'
diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml
index d8a57560788c95cb3809376f726b85b4bf0509b1..a3b0709e26108f7a64d84db733bc4c396be464db 100644
--- a/app/views/explore/_head.html.haml
+++ b/app/views/explore/_head.html.haml
@@ -1,5 +1,5 @@
-.explore-title
-  %h3
+.explore-title.text-center
+  %h2
     Explore GitLab
   %p.lead
     Discover projects, groups and snippets. Share your projects with others
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index ebf9aca7700849b90e958f0a851fa37c9f6fcafc..bc5d3c797ac3086c5e76791b64ea79c21ccf1cac 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -21,6 +21,7 @@
         = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
         %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
           = icon("search")
+        = render 'shared/members/sort_dropdown'
   .panel.panel-default
     .panel-heading
       Users with access to
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index f8b4b107513991c0005964c2f416ab545332b49f..ac09b71ae892306197e2094b04686b5d9a78f765 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -1,5 +1,6 @@
-- page_title "Bitbucket import"
-- header_title "Projects", root_path
+- page_title 'Bitbucket import'
+- header_title 'Projects', root_path
+
 %h3.page-title
   %i.fa.fa-bitbucket
   Import projects from Bitbucket
@@ -10,13 +11,13 @@
   %hr
   %p
   - if @incompatible_repos.any?
-    = button_tag class: "btn btn-import btn-success js-import-all" do
+    = button_tag class: 'btn btn-import btn-success js-import-all' do
       Import all compatible projects
-      = icon("spinner spin", class: "loading-icon")
+      = icon('spinner spin', class: 'loading-icon')
   - else
-    = button_tag class: "btn btn-success js-import-all" do
+    = button_tag class: 'btn btn-import btn-success js-import-all' do
       Import all projects
-      = icon("spinner spin", class: "loading-icon")
+      = icon('spinner spin', class: 'loading-icon')
 
 .table-responsive
   %table.table.import-jobs
@@ -32,7 +33,7 @@
       - @already_added_projects.each do |project|
         %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
           %td
-            = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: "_blank"
+            = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
           %td
             = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
           %td.job-status
@@ -47,31 +48,41 @@
               = project.human_import_status_name
 
       - @repos.each do |repo|
-        %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+        %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
           %td
-            = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
           %td.import-target
-            = import_project_target(repo['owner'], repo['slug'])
+            %fieldset.row
+            .input-group
+              .project-path.input-group-btn
+                - if current_user.can_select_namespace?
+                  - selected = params[:namespace_id] || :current_user
+                  - opts = current_user.can_create_group? ? { extra_group: Group.new(name: repo.owner, path: repo.owner) } : {}
+                  = select_tag :namespace_id, namespaces_options(selected, opts.merge({ display_path: true })), { class: 'select2 js-select-namespace', tabindex: 1 }
+                - else
+                  = text_field_tag :path, current_user.namespace_path, class: "input-large form-control", tabindex: 1, disabled: true
+              %span.input-group-addon /
+              = text_field_tag :path, repo.name, class: "input-mini form-control", tabindex: 2, autofocus: true, required: true
           %td.import-actions.job-status
-            = button_tag class: "btn btn-import js-add-to-import" do
+            = button_tag class: 'btn btn-import js-add-to-import' do
               Import
-              = icon("spinner spin", class: "loading-icon")
+              = icon('spinner spin', class: 'loading-icon')
       - @incompatible_repos.each do |repo|
-        %tr{id: "repo_#{repo["owner"]}___#{repo["slug"]}"}
+        %tr{id: "repo_#{repo.owner}___#{repo.slug}"}
           %td
-            = link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
+            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
           %td.import-target
           %td.import-actions-job-status
-            = label_tag "Incompatible Project", nil, class: "label label-danger"
+            = label_tag 'Incompatible Project', nil, class: 'label label-danger'
 
 - if @incompatible_repos.any?
   %p
     One or more of your Bitbucket projects cannot be imported into GitLab
     directly because they use Subversion or Mercurial for version control,
     rather than Git. Please convert
-    = link_to "them to Git,", "https://www.atlassian.com/git/tutorials/migrating-overview"
+    = link_to 'them to Git,', 'https://www.atlassian.com/git/tutorials/migrating-overview'
     and go through the
-    = link_to "import flow", status_import_bitbucket_path, "data-no-turbolink" => "true"
+    = link_to 'import flow', status_import_bitbucket_path, 'data-no-turbolink' => 'true'
     again.
 
 .js-importer-status{ data: { jobs_import_path: "#{jobs_import_bitbucket_path}", import_path: "#{import_bitbucket_path}" } }
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index e138ebab0188767b60b2313c3883ea30ee26a4f2..3daa1e90a8c335fd41db5b9042af4a14d6399446 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -3,6 +3,14 @@
 
 - if project
   :javascript
-    GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
-    GitLab.GfmAutoComplete.cachedData = undefined;
-    GitLab.GfmAutoComplete.setup();
+    gl.GfmAutoComplete.dataSources = {
+      emojis: "#{emojis_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      members: "#{members_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}",
+      issues: "#{issues_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      mergeRequests: "#{merge_requests_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      labels: "#{labels_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      milestones: "#{milestones_namespace_project_autocomplete_sources_path(project.namespace, project)}",
+      commands: "#{commands_namespace_project_autocomplete_sources_path(project.namespace, project, type: noteable_type, type_id: params[:id])}"
+    };
+
+    gl.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index a9a0b149049661e5a981759a593177111330144f..54d02ee8e4b961a799122f26dc6063e238b38f5d 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -22,9 +22,10 @@
         = render "layouts/nav/#{nav}"
   .content-wrapper{ class: "#{layout_nav_class}" }
     = yield :sub_nav
-    = render "layouts/broadcast"
-    = render "layouts/flash"
-    = yield :flash_message
+    .alert-wrapper
+      = render "layouts/broadcast"
+      = render "layouts/flash"
+      = yield :flash_message
     %div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
       .content{ id: "content-body" }
         = yield
diff --git a/app/views/profiles/personal_access_tokens/_form.html.haml b/app/views/profiles/personal_access_tokens/_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..3f6efa339537f74bb466e1c343b721aa8c86a345
--- /dev/null
+++ b/app/views/profiles/personal_access_tokens/_form.html.haml
@@ -0,0 +1,21 @@
+- personal_access_token = local_assigns.fetch(:personal_access_token)
+- scopes = local_assigns.fetch(:scopes)
+
+= form_for [:profile, personal_access_token], method: :post, html: { class: 'js-requires-input' } do |f|
+
+  = form_errors(personal_access_token)
+
+  .form-group
+    = f.label :name, class: 'label-light'
+    = f.text_field :name, class: "form-control", required: true
+
+  .form-group
+    = f.label :expires_at, class: 'label-light'
+    = f.text_field :expires_at, class: "datepicker form-control"
+
+  .form-group
+    = f.label :scopes, class: 'label-light'
+    = render 'shared/tokens/scopes_form', prefix: 'personal_access_token', token: personal_access_token, scopes: scopes
+
+  .prepend-top-default
+    = f.submit 'Create Personal Access Token', class: "btn btn-create"
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index 05a2ea67aa2189c8327fe45aa409c21f70ced497..bb4effeeeb14280b9114be5928726c4799cdddf8 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -28,21 +28,8 @@
       Add a Personal Access Token
     %p.profile-settings-content
       Pick a name for the application, and we'll give you a unique token.
-    = form_for [:profile, @personal_access_token],
-                method: :post, html: { class: 'js-requires-input' } do |f|
 
-      = form_errors(@personal_access_token)
-
-      .form-group
-        = f.label :name, class: 'label-light'
-        = f.text_field :name, class: "form-control", required: true
-
-      .form-group
-        = f.label :expires_at, class: 'label-light'
-        = f.text_field :expires_at, class: "datepicker form-control", required: false
-
-      .prepend-top-default
-        = f.submit 'Create Personal Access Token', class: "btn btn-create"
+    = render "form", personal_access_token: @personal_access_token, scopes: @scopes
 
     %hr
 
@@ -56,6 +43,7 @@
               %th Name
               %th Created
               %th Expires
+              %th Scopes
               %th
           %tbody
             - @active_personal_access_tokens.each do |token|
@@ -67,6 +55,7 @@
                     = token.expires_at.to_date.to_s(:medium)
                   - else
                     %span.personal-access-tokens-never-expires-label Never
+                %td= token.scopes.present? ? token.scopes.join(", ") : "<no scopes selected>"
                 %td= link_to "Revoke", revoke_profile_personal_access_token_path(token), method: :put, class: "btn btn-danger pull-right", data: { confirm: "Are you sure you want to revoke this token? This action cannot be undone." }
 
     - else
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index 03ac739ade51517de77757c31335e0f6445d2918..558a1d56151421e76806c9cc5bc866c8bcc6bfba 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -30,7 +30,7 @@
               To add the entry manually, provide the following details to the application on your phone.
             %p.prepend-top-0.append-bottom-0
               Account:
-              = current_user.email
+              = @account_string
             %p.prepend-top-0.append-bottom-0
               Key:
               = current_user.otp_secret.scan(/.{4}/).join(' ')
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 4a6aa92e3f3db04685091e0882c416a59e2a3ac2..1d058daa0943301b6766f53a45f3b90d4a5c994f 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -21,6 +21,8 @@
         = dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'btn js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
       .gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
         = dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'btn js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+      .dockerfile-selector.js-dockerfile-selector-wrap.hidden
+        = dropdown_tag("Choose a Dockerfile template", options: { toggle_class: 'btn js-dockerfile-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: dockerfile_names } } )
       = button_tag class: 'soft-wrap-toggle btn', type: 'button' do
         %span.no-wrap
           = custom_icon('icon_no_wrap')
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
deleted file mode 100644
index ad1a7360a8b3e827318de965eeee0bb337a81431..0000000000000000000000000000000000000000
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- is_playable = subject.playable? && can?(current_user, :update_build, @project)
-- if is_playable
-  = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.js-pipeline-graph', placement: 'bottom' } do
-    = ci_icon_for_status('play')
-    .ci-status-text= subject.name
-- elsif can?(current_user, :read_build, @project)
-  = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } do
-    %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
-      = ci_icon_for_status(subject.status)
-    .ci-status-text= subject.name
-- else
-  %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
-    = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
deleted file mode 100644
index 1bba04431542e1128b7514e1a9dc970534c32193..0000000000000000000000000000000000000000
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ /dev/null
@@ -1,10 +0,0 @@
-%a{ data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.js-pipeline-graph', placement: 'bottom' } }
-  - if subject.target_url
-    = link_to subject.target_url do
-      %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
-        = ci_icon_for_status(subject.status)
-      %span.ci-status-text= subject.name
-  - else
-    %span{class: "ci-status-icon ci-status-icon-#{subject.status}"}
-      = ci_icon_for_status(subject.status)
-    %span.ci-status-text= subject.name
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index bd629b5c5197e1fd7e4c404fe3ce128f166c5560..981bf640a6b97b1b4aa4cf526b384e0a4caf092a 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width"
 - page_title           "#{@issue.title} (#{@issue.to_reference})", "Issues"
 - page_description     @issue.description
 - page_card_attributes @issue.card_attributes
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index fa189ae62d824073952a3d88b704994e99ac14ff..b3c43286a5056372816dcd240478babd8a03c3e4 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,4 @@
-%li{ class: mr_css_classes(merge_request) }
+%li{ id: dom_id(merge_request), class: mr_css_classes(merge_request), data: { labels: merge_request.label_ids, id: merge_request.id } }
   - if @bulk_edit
     .issue-check
       = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
@@ -39,7 +39,7 @@
           = icon('thumbs-down')
           = downvotes
 
-      - note_count = merge_request.mr_and_commit_notes.user.count
+      - note_count = merge_request.related_notes.user.count
       %li
         = link_to merge_request_path(merge_request, anchor: 'notes'), class: ('no-comments' if note_count.zero?) do
           = icon('comments')
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 896f10104fa6f7dc3749fccaa5ed58d1da4d97c4..7725558518f320146a76f32080bd46f6c711aee1 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,3 +1,4 @@
+- @content_class = "limit-container-width"
 - page_title           "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
 - page_description     @merge_request.description
 - page_card_attributes @merge_request.card_attributes
@@ -41,7 +42,7 @@
     = render "projects/merge_requests/widget/show.html.haml"
 
     - if @merge_request.source_branch_exists? && @merge_request.mergeable? && @merge_request.can_be_merged_by?(current_user)
-      .light.prepend-top-default.append-bottom-default
+      .merge-manually.light.prepend-top-default.append-bottom-default
         You can also accept this merge request manually using the
         = succeed '.' do
           = link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
@@ -53,7 +54,7 @@
             %li.notes-tab
               = link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#notes', action: 'notes', toggle: 'tab' } do
                 Discussion
-                %span.badge= @merge_request.mr_and_commit_notes.user.count
+                %span.badge= @merge_request.related_notes.user.count
             - if @merge_request.source_project
               %li.commits-tab
                 = link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: { target: 'div#commits', action: 'commits', toggle: 'tab' } do
diff --git a/app/views/projects/merge_requests/show/_commits.html.haml b/app/views/projects/merge_requests/show/_commits.html.haml
index a0e12fb3f38340118a7443fd6b17a9af1c8a6c27..baa1ade5eee4726fc886fc92be10a51595e8cddf 100644
--- a/app/views/projects/merge_requests/show/_commits.html.haml
+++ b/app/views/projects/merge_requests/show/_commits.html.haml
@@ -1,6 +1,2 @@
-.content-block.oneline-block
-  = icon("sort-amount-desc")
-  Most recent commits displayed first
-
 %ol#commits-list.list-unstyled
   = render "projects/commits/commits", project: @merge_request.source_project, ref: @merge_request.source_branch
diff --git a/app/views/projects/merge_requests/widget/_open.html.haml b/app/views/projects/merge_requests/widget/_open.html.haml
index eee711dc5afc73041ecdc528eaf2629942c0f91c..f4aa1609a1b9fa714cbdae2ae5595cd72fffb555 100644
--- a/app/views/projects/merge_requests/widget/_open.html.haml
+++ b/app/views/projects/merge_requests/widget/_open.html.haml
@@ -30,11 +30,18 @@
     - elsif @merge_request.can_be_merged? || resolved_conflicts
       = render 'projects/merge_requests/widget/open/accept'
 
-  - if mr_closes_issues.present?
+  - if mr_closes_issues.present? || mr_issues_mentioned_but_not_closing.present?
     .mr-widget-footer
       %span
-        %i.fa.fa-check
-        Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
-        = succeed '.' do
-          != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
-        = mr_assign_issues_link
+        = icon('check')
+        - if mr_closes_issues.present?
+          Accepting this merge request will close #{"issue".pluralize(mr_closes_issues.size)}
+          = succeed '.' do
+            != markdown issues_sentence(mr_closes_issues), pipeline: :gfm, author: @merge_request.author
+          = mr_assign_issues_link
+        - if mr_issues_mentioned_but_not_closing.present?
+          #{"Issue".pluralize(mr_issues_mentioned_but_not_closing.size)}
+          != markdown issues_sentence(mr_issues_mentioned_but_not_closing), pipeline: :gfm, author: @merge_request.author
+          #{mr_issues_mentioned_but_not_closing.size > 1 ? 'are' : 'is'} mentioned but will not closed.
+         
+
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index 435fe835fae6e6358e627aa96561c04c04e6138c..d6f7f23533cefb020ad2a45ea67b3e8f13397279 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -41,6 +41,8 @@
           Modify commit message
     .js-toggle-content.hide.prepend-top-default
       = render 'shared/commit_message_container', params: params,
+          message_with_description: @merge_request.merge_commit_message(include_description: true),
+          message_without_description: @merge_request.merge_commit_message,
           text: @merge_request.merge_commit_message,
           rows: 14, hint: true
 
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index e1e787dbde434306b6bc78ee63d2ad1fe8a426cf..030cd8ef78f6cac3cf04ea50fab1ee961486b50f 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -5,23 +5,23 @@
 %div{ class: container_class }
   .top-area
     %ul.nav-links
-      %li{class: ('active' if @scope.nil?)}
+      %li{class: ('active' if @scope.nil?)}>
         = link_to project_pipelines_path(@project) do
           All
           %span.badge.js-totalbuilds-count
             = number_with_delimiter(@pipelines_count)
 
-      %li{class: ('active' if @scope == 'running')}
+      %li{class: ('active' if @scope == 'running')}>
         = link_to project_pipelines_path(@project, scope: :running) do
           Running
           %span.badge.js-running-count
             = number_with_delimiter(@running_or_pending_count)
 
-      %li{class: ('active' if @scope == 'branches')}
+      %li{class: ('active' if @scope == 'branches')}>
         = link_to project_pipelines_path(@project, scope: :branches) do
           Branches
 
-      %li{class: ('active' if @scope == 'tags')}
+      %li{class: ('active' if @scope == 'tags')}>
         = link_to project_pipelines_path(@project, scope: :tags) do
           Tags
 
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index bdeb704b6daa6a7c01393f5aab0f5778a168c185..4f1cec20f8579d1118d12119a590ddf69dfb310f 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -21,6 +21,7 @@
         = search_field_tag :search, params[:search], { placeholder: 'Find existing members by name', class: 'form-control', spellcheck: false }
         %button.member-search-btn{ type: "submit", "aria-label" => "Submit search" }
           = icon("search")
+        = render 'shared/members/sort_dropdown'
   - if @group_links.any?
     = render 'groups', group_links: @group_links
 
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index a676c0290a090c8ba510c2bc0c3a38162a9510f4..01a77a952d183bd32239acb96a8a2eca5935ba71 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -1,5 +1,4 @@
-- pretty_path_with_namespace = "#{@project ? @project.namespace.name : 'namespace'} / #{@project ? @project.name : 'name'}"
-- run_actions_text = "Perform common operations on this project: #{pretty_path_with_namespace}"
+- run_actions_text = "Perform common operations on this project: #{@project.name_with_namespace}"
 
 .well
   This service allows GitLab users to perform common operations on this
@@ -27,7 +26,7 @@
     .form-group
       = label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
       .col-sm-10.col-xs-12.input-group
-        = text_field_tag :display_name, "GitLab / #{pretty_path_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+        = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
         .input-group-btn
           = clipboard_button(clipboard_target: '#display_name')
 
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 9503dbded13ae43766c19c3f64b52e5811832aef..79d87b7db127d89e8c291e2241c03cb9e6f930dd 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -8,10 +8,11 @@
       = blob_icon 0, @snippet.file_name
       = @snippet.file_name
       .file-actions
-        = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+        = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
         = link_to 'Raw', raw_namespace_project_snippet_path(@project.namespace, @project, @snippet), class: "btn btn-sm", target: "_blank"
     = render 'shared/snippets/blob'
 
-  = render 'award_emoji/awards_block', awardable: @snippet, inline: true
+  .row-content-block.top-block.content-component-block
+    = render 'award_emoji/awards_block', awardable: @snippet, inline: true
 
   %div#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/stage/_graph.html.haml b/app/views/projects/stage/_graph.html.haml
index bf8c75b6e5c6e240e2846406224667eeadf91863..b70b574e687052793615d1a4603e078cf552a2ba 100644
--- a/app/views/projects/stage/_graph.html.haml
+++ b/app/views/projects/stage/_graph.html.haml
@@ -10,11 +10,10 @@
       - status_groups.each do |group_name, grouped_statuses|
         - if grouped_statuses.one?
           - status = grouped_statuses.first
-          - is_playable = status.playable? && can?(current_user, :update_build, @project)
-          %li.build{ class: ("playable" if is_playable) }
+          %li.build
             .curve
             .build-content
-              = render "projects/#{status.to_partial_path}_pipeline", subject: status
+              = render 'ci/status/graph_badge', subject: status
         - else
           %li.build
             .curve
diff --git a/app/views/projects/stage/_in_stage_group.html.haml b/app/views/projects/stage/_in_stage_group.html.haml
index 2b26ad9d6fa2f8287c777f676257bc6f21dfb7e2..b03837d12119ff95ce7ab8aad3d3cf00b65f07c3 100644
--- a/app/views/projects/stage/_in_stage_group.html.haml
+++ b/app/views/projects/stage/_in_stage_group.html.haml
@@ -4,10 +4,10 @@
     = ci_icon_for_status(group_status)
   %span.ci-status-text
     = name
-  %span.badge= subject.size
+  %span.dropdown-counter-badge= subject.size
 .dropdown-menu.grouped-pipeline-dropdown
   .arrow
   %ul
     - subject.each do |status|
-      %li
-        = render "projects/#{status.to_partial_path}_pipeline", subject: status
+      %li.dropdown-build
+        = render 'ci/status/graph_badge', subject: status
diff --git a/app/views/repository_check_mailer/notify.html.haml b/app/views/repository_check_mailer/notify.html.haml
index a585147ddd18fd36194b0f52e0f612e460258ee5..94e5a5d9709997465d203cd82553f8c44e60d4ad 100644
--- a/app/views/repository_check_mailer/notify.html.haml
+++ b/app/views/repository_check_mailer/notify.html.haml
@@ -2,7 +2,7 @@
   #{@message}.
 
 %p
-  = link_to "See the affected projects in the GitLab admin panel", admin_namespaces_projects_url(last_repository_check_failed: 1)
+  = link_to "See the affected projects in the GitLab admin panel", admin_projects_url(last_repository_check_failed: 1)
 
 %p
   You are receiving this message because you are a GitLab administrator for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/repository_check_mailer/notify.text.haml b/app/views/repository_check_mailer/notify.text.haml
index 93db151329efecc297ef3be4c8b1700b3ecccbda..0902c50d0527e96944fa2c45da1d3b7965d2bf2e 100644
--- a/app/views/repository_check_mailer/notify.text.haml
+++ b/app/views/repository_check_mailer/notify.text.haml
@@ -1,6 +1,6 @@
 #{@message}.
 \
-View details: #{admin_namespaces_projects_url(last_repository_check_failed: 1)}
+View details: #{admin_projects_url(last_repository_check_failed: 1)}
 
 You are receiving this message because you are a GitLab administrator
 for #{Gitlab.config.gitlab.url}.
diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml
index 0a38327baa2eb6ca47ba78f655840e18dc6147a9..c196bc06b174e25938382a9c20a4736e0d119ae0 100644
--- a/app/views/shared/_commit_message_container.html.haml
+++ b/app/views/shared/_commit_message_container.html.haml
@@ -1,5 +1,6 @@
 .form-group.commit_message-group
   - nonce = SecureRandom.hex
+  - descriptions = local_assigns.slice(:message_with_description, :message_without_description)
   = label_tag "commit_message-#{nonce}", class: 'control-label' do
     Commit message
   .col-sm-10
@@ -8,9 +9,17 @@
       = text_area_tag 'commit_message',
           (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]),
           class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder],
+          data: descriptions,
           required: true, rows: (local_assigns[:rows] || 3),
           id: "commit_message-#{nonce}"
     - if local_assigns[:hint]
       %p.hint
         Try to keep the first line under 52 characters
         and the others under 72.
+    - if descriptions.present?
+      %p.hint.js-with-description-hint
+        = link_to "#", class: "js-with-description-link" do
+          Include description in commit message
+      %p.hint.js-without-description-hint.hide
+        = link_to "#", class: "js-without-description-link" do
+          Don't include description in commit message
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index 73d288e22366f32229b4761c5e2032691b4d81d1..186ed4a7c8b0147f520756e226ba25febd28e7b2 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -2,17 +2,17 @@
   - counts = milestone_counts(@project.milestones)
 
 %ul.nav-links
-  %li{class: milestone_class_for_state(params[:state], 'opened', true)}
+  %li{class: milestone_class_for_state(params[:state], 'opened', true)}>
     = link_to milestones_filter_path(state: 'opened') do
       Open
       - if @project
         %span.badge #{counts[:opened]}
-  %li{class: milestone_class_for_state(params[:state], 'closed')}
+  %li{class: milestone_class_for_state(params[:state], 'closed')}>
     = link_to milestones_filter_path(state: 'closed') do
       Closed
       - if @project
         %span.badge #{counts[:closed]}
-  %li{class: milestone_class_for_state(params[:state], 'all')}
+  %li{class: milestone_class_for_state(params[:state], 'all')}>
     = link_to milestones_filter_path(state: 'all') do
       All
       - if @project
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 60353aee7f15750eaf4611a954771dc9fda6a6dd..b6047ece5927b66b5ae113a44f4a6b542fa8381d 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,23 +1,23 @@
 %ul.nav-links
-  %li{ class: ('active' if scope.nil?) }
+  %li{ class: ('active' if scope.nil?) }>
     = link_to build_path_proc.call(nil) do
       All
       %span.badge.js-totalbuilds-count
         = number_with_delimiter(all_builds.count(:id))
 
-  %li{ class: ('active' if scope == 'pending') }
+  %li{ class: ('active' if scope == 'pending') }>
     = link_to build_path_proc.call('pending') do
       Pending
       %span.badge
         = number_with_delimiter(all_builds.pending.count(:id))
 
-  %li{ class: ('active' if scope == 'running') }
+  %li{ class: ('active' if scope == 'running') }>
     = link_to build_path_proc.call('running') do
       Running
       %span.badge
         = number_with_delimiter(all_builds.running.count(:id))
 
-  %li{ class: ('active' if scope == 'finished') }
+  %li{ class: ('active' if scope == 'finished') }>
     = link_to build_path_proc.call('finished') do
       Finished
       %span.badge
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 19221e3391fcb16bf3f70ce06d85c8acc70a43b8..8164f61797c0d0a451e4fecb7cb80af18c036fb8 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -28,7 +28,7 @@
     = image_tag group_icon(group), class: "avatar s40 hidden-xs"
   .title
     = link_to group, class: 'group-name' do
-      = group.name
+      = group.full_name
 
     - if group_member
       as
diff --git a/app/views/shared/icons/_icon_status_canceled.svg b/app/views/shared/icons/_icon_status_canceled.svg
old mode 100644
new mode 100755
index 41a210a8ed97241965caf6fa471b0d170923eb51..bd5d04e1cd7c4f378716cc5524d83c361917d995
--- a/app/views/shared/icons/_icon_status_canceled.svg
+++ b/app/views/shared/icons/_icon_status_canceled.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><rect width="8" height="2" x="3" y="6" transform="rotate(45 7 7)" rx=".5"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M5.2 3.8l4.9 4.9c.2.2.2.5 0 .7l-.7.7c-.2.2-.5.2-.7 0L3.8 5.2c-.2-.2-.2-.5 0-.7l.7-.7c.2-.2.5-.2.7 0"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
old mode 100644
new mode 100755
index 1f5c3b51b0386aaf4fecf79893bf38714b37169b..326ad04e017005045e1f9ea5cb8cc4d3397538cc
--- a/app/views/shared/icons/_icon_status_created.svg
+++ b/app/views/shared/icons/_icon_status_created.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><circle cx="7" cy="7" r="3.25"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_failed.svg b/app/views/shared/icons/_icon_status_failed.svg
old mode 100644
new mode 100755
index af267b8938a3132658a538ad69bc002861a9109f..64da5aa31fc612d52c4fba5858c04ec150588ecd
--- a/app/views/shared/icons/_icon_status_failed.svg
+++ b/app/views/shared/icons/_icon_status_failed.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.72916667,6.27083333 L7.72916667,4.28939247 C7.72916667,4.12531853 7.59703895,4 7.43405116,4 L6.56594884,4 C6.40541585,4 6.27083333,4.12956542 6.27083333,4.28939247 L6.27083333,6.27083333 L4.28939247,6.27083333 C4.12531853,6.27083333 4,6.40296105 4,6.56594884 L4,7.43405116 C4,7.59458415 4.12956542,7.72916667 4.28939247,7.72916667 L6.27083333,7.72916667 L6.27083333,9.71060753 C6.27083333,9.87468147 6.40296105,10 6.56594884,10 L7.43405116,10 C7.59458415,10 7.72916667,9.87043458 7.72916667,9.71060753 L7.72916667,7.72916667 L9.71060753,7.72916667 C9.87468147,7.72916667 10,7.59703895 10,7.43405116 L10,6.56594884 C10,6.40541585 9.87043458,6.27083333 9.71060753,6.27083333 L7.72916667,6.27083333 Z" transform="rotate(-45 7 7)"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 5.969L5.599 4.568a.29.29 0 0 0-.413.004l-.614.614a.294.294 0 0 0-.004.413L5.968 7l-1.4 1.401a.29.29 0 0 0 .004.413l.614.614c.113.114.3.117.413.004L7 8.032l1.401 1.4a.29.29 0 0 0 .413-.004l.614-.614a.294.294 0 0 0 .004-.413L8.032 7l1.4-1.401a.29.29 0 0 0-.004-.413l-.614-.614a.294.294 0 0 0-.413-.004L7 5.968z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_pending.svg b/app/views/shared/icons/_icon_status_pending.svg
old mode 100644
new mode 100755
index 516231d1b4487dc60b0fcefe9ec98a2470b9b013..02d5da407e3068867bd8f64326e5da964f99e36f
--- a/app/views/shared/icons/_icon_status_pending.svg
+++ b/app/views/shared/icons/_icon_status_pending.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M4.69999981,5.30065012 C4.69999981,5.13460564 4.83842754,5 5.00354719,5 L5.89645243,5 C6.06409702,5 6.19999981,5.13308716 6.19999981,5.30065012 L6.19999981,8.69934988 C6.19999981,8.86539436 6.06157207,9 5.89645243,9 L5.00354719,9 C4.8359026,9 4.69999981,8.86691284 4.69999981,8.69934988 L4.69999981,5.30065012 Z M7.69999981,5.30065012 C7.69999981,5.13460564 7.83842754,5 8.00354719,5 L8.89645243,5 C9.06409702,5 9.19999981,5.13308716 9.19999981,5.30065012 L9.19999981,8.69934988 C9.19999981,8.86539436 9.06157207,9 8.89645243,9 L8.00354719,9 C7.8359026,9 7.69999981,8.86691284 7.69999981,8.69934988 L7.69999981,5.30065012 Z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M4.7 5.3c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H5c-.2 0-.3-.1-.3-.3V5.3m3 0c0-.2.1-.3.3-.3h.9c.2 0 .3.1.3.3v3.4c0 .2-.1.3-.3.3H8c-.2 0-.3-.1-.3-.3V5.3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_running.svg b/app/views/shared/icons/_icon_status_running.svg
old mode 100644
new mode 100755
index d2618bce200bc0dd8a236e1e708e8b440bc1bf4f..532f4fee33ce8a6a2faa802472fe8044c6d2a544
--- a/app/views/shared/icons/_icon_status_running.svg
+++ b/app/views/shared/icons/_icon_status_running.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7,3 C9.209139,3 11,4.790861 11,7 C11,9.209139 9.209139,11 7,11 C5.65802855,11 4.47040669,10.3391508 3.74481446,9.32513253 L7,7 L7,3 L7,3 Z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7 3c2.2 0 4 1.8 4 4s-1.8 4-4 4c-1.3 0-2.5-.7-3.3-1.7L7 7V3"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_skipped.svg b/app/views/shared/icons/_icon_status_skipped.svg
old mode 100644
new mode 100755
index 701f33bcbea00db7942c99c720173605dbf00e01..1998dfef9ea0ae10aefe7a672e7a4ab553f538fa
--- a/app/views/shared/icons/_icon_status_skipped.svg
+++ b/app/views/shared/icons/_icon_status_skipped.svg
@@ -1 +1 @@
-<svg width="14" height="14" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M7.69 7.7l-.905.905a.7.7 0 0 0 .99.99l1.85-1.85c.411-.412.411-1.078 0-1.49l-1.85-1.85a.7.7 0 0 0-.99.99l.905.905H4.48a.7.7 0 0 0 0 1.4h3.21z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_success.svg b/app/views/shared/icons/_icon_status_success.svg
old mode 100644
new mode 100755
index b7c21ba6971a447d10ef1d9492f08d007b668fa1..eed5006bebeae4539119736e0e0218d25f233a94
--- a/app/views/shared/icons/_icon_status_success.svg
+++ b/app/views/shared/icons/_icon_status_success.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M7.29166667,7.875 L5.54840803,7.875 C5.38293028,7.875 5.25,8.00712771 5.25,8.17011551 L5.25,9.03821782 C5.25,9.19875081 5.38360183,9.33333333 5.54840803,9.33333333 L8.24853534,9.33333333 C8.52035522,9.33333333 8.75,9.11228506 8.75,8.83960819 L8.75,8.46475969 L8.75,4.07392947 C8.75,3.92144267 8.61787229,3.79166667 8.45488449,3.79166667 L7.58678218,3.79166667 C7.42624919,3.79166667 7.29166667,3.91804003 7.29166667,4.07392947 L7.29166667,7.875 Z" transform="rotate(45 7 6.563)"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6.278 7.697L5.045 6.464a.296.296 0 0 0-.42-.002l-.613.614a.298.298 0 0 0 .002.42l1.91 1.909a.5.5 0 0 0 .703.005l.265-.265L9.997 6.04a.291.291 0 0 0-.009-.408l-.614-.614a.29.29 0 0 0-.408-.009L6.278 7.697z"/></g></svg>
diff --git a/app/views/shared/icons/_icon_status_warning.svg b/app/views/shared/icons/_icon_status_warning.svg
old mode 100644
new mode 100755
index 9191e0050a644f0cc96223b78d4e15c18a6a4055..cb785635b7edd19ec7da1203a64a9081be5afa97
--- a/app/views/shared/icons/_icon_status_warning.svg
+++ b/app/views/shared/icons/_icon_status_warning.svg
@@ -1 +1 @@
-<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"><g fill-rule="evenodd"><path d="M12.5,7 C12.5,3.96243388 10.0375661,1.5 7,1.5 C3.96243388,1.5 1.5,3.96243388 1.5,7 C1.5,10.0375661 3.96243388,12.5 7,12.5 C10.0375661,12.5 12.5,10.0375661 12.5,7 Z M0,7 C0,3.13400675 3.13400675,0 7,0 C10.8659932,0 14,3.13400675 14,7 C14,10.8659932 10.8659932,14 7,14 C3.13400675,14 0,10.8659932 0,7 Z"/><path d="M6,3.49769878 C6,3.22282734 6.21403503,3 6.50468445,3 L7.49531555,3 C7.77404508,3 8,3.21484375 8,3.49769878 L8,7.50230122 C8,7.77717266 7.78596497,8 7.49531555,8 L6.50468445,8 C6.22595492,8 6,7.78515625 6,7.50230122 L6,3.49769878 Z M6,9.50468445 C6,9.22595492 6.21403503,9 6.50468445,9 L7.49531555,9 C7.77404508,9 8,9.21403503 8,9.50468445 L8,10.4953156 C8,10.7740451 7.78596497,11 7.49531555,11 L6.50468445,11 C6.22595492,11 6,10.785965 6,10.4953156 L6,9.50468445 Z"/></g></svg>
+<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><g fill-rule="evenodd"><path d="M0 7a7 7 0 1 1 14 0A7 7 0 0 1 0 7z"/><path d="M13 7A6 6 0 1 0 1 7a6 6 0 0 0 12 0z" fill="#FFF"/><path d="M6 3.5c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v4c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-4m0 6c0-.3.2-.5.5-.5h1c.3 0 .5.2.5.5v1c0 .3-.2.5-.5.5h-1c-.3 0-.5-.2-.5-.5v-1"/></g></svg>
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 0af92b595848f7606900243c25ed64bc7cd78d47..d938edf4dbdd8a5d5d73930f53cffe215417e7bb 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -3,23 +3,23 @@
 - issuables = @issues || @merge_requests
 
 %ul.nav-links.issues-state-filters
-  %li{class: ("active" if params[:state] == 'opened')}
+  %li{class: ("active" if params[:state] == 'opened')}>
     = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do
       #{issuables_state_counter_text(type, :opened)}
 
   - if type == :merge_requests
-    %li{class: ("active" if params[:state] == 'merged')}
+    %li{class: ("active" if params[:state] == 'merged')}>
       = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do
         #{issuables_state_counter_text(type, :merged)}
 
-    %li{class: ("active" if params[:state] == 'closed')}
+    %li{class: ("active" if params[:state] == 'closed')}>
       = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do
         #{issuables_state_counter_text(type, :closed)}
   - else
-    %li{class: ("active" if params[:state] == 'closed')}
+    %li{class: ("active" if params[:state] == 'closed')}>
       = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do
         #{issuables_state_counter_text(type, :closed)}
 
-  %li{class: ("active" if params[:state] == 'all')}
+  %li{class: ("active" if params[:state] == 'all')}>
     = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do
       #{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 958f8413e1d7fcc4e633cd8f39054c9122e334cb..9fe1be5a597d25bb5468ed805e89d9352919ace9 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -167,4 +167,4 @@
       new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
       gl.Subscription.bindAll('.subscription');
       new gl.DueDateSelectors();
-      sidebar = new Sidebar();
+      window.sidebar = new Sidebar();
diff --git a/app/views/shared/members/_sort_dropdown.html.haml b/app/views/shared/members/_sort_dropdown.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..bad0891f9f21102a218c36566d61f99f22736bf3
--- /dev/null
+++ b/app/views/shared/members/_sort_dropdown.html.haml
@@ -0,0 +1,9 @@
+.dropdown.inline.member-sort-dropdown
+  = dropdown_toggle(member_sort_options_hash[@sort], { toggle: 'dropdown' })
+  %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+    %li.dropdown-header
+      Sort by
+    - member_sort_options_hash.each do |value, title|
+      %li
+        = link_to filter_group_project_member_path(sort: value), class: ("is-active" if @sort == value) do
+          = title
diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5074afb63a1245bdd4a18fc48a80cf0542f09a3b
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_form.html.haml
@@ -0,0 +1,9 @@
+- scopes = local_assigns.fetch(:scopes)
+- prefix = local_assigns.fetch(:prefix)
+- token = local_assigns.fetch(:token)
+
+- scopes.each do |scope|
+  %fieldset
+    = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}"
+    = label_tag "#{prefix}_scopes_#{scope}", scope
+    %span= "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/shared/tokens/_scopes_list.html.haml b/app/views/shared/tokens/_scopes_list.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f99e905e95c7b261ca7adbcb4b7a93cf6b016cca
--- /dev/null
+++ b/app/views/shared/tokens/_scopes_list.html.haml
@@ -0,0 +1,13 @@
+- token = local_assigns.fetch(:token)
+
+- return unless token.scopes.present?
+
+%tr
+  %td
+    Scopes
+  %td
+    %ul.scopes-list.append-bottom-0
+      - token.scopes.each do |scope|
+        %li
+          %span.scope-name= scope
+          = "(#{t(scope, scope: [:doorkeeper, :scopes])})"
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 27d7a6c5bb67a202c55fa1f49304edaa6ef211da..837a1a0cc8c9d0c2a7605a76703bd68095dc6cfe 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -7,9 +7,9 @@
     = blob_icon 0, @snippet.file_name
     = @snippet.file_name
     .file-actions
-      = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']")
+      = clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{@snippet.id}']", class: "btn btn-sm")
       = link_to 'Raw', raw_snippet_path(@snippet), class: "btn btn-sm", target: "_blank"
       = link_to 'Download', download_snippet_path(@snippet), class: "btn btn-sm"
   = render 'shared/snippets/blob'
 
-= render 'award_emoji/awards_block', awardable: @snippet, inline: true
\ No newline at end of file
+= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/changelogs/unreleased/18435-autocomplete-is-not-performant.yml b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
new file mode 100644
index 0000000000000000000000000000000000000000..019c55e27dc310666b6d5b6ae91f4d8036a4e4bf
--- /dev/null
+++ b/changelogs/unreleased/18435-autocomplete-is-not-performant.yml
@@ -0,0 +1,4 @@
+---
+title: Made comment autocomplete more performant and removed some loading bugs
+merge_request: 6856
+author: 
diff --git a/changelogs/unreleased/20492-access-token-scopes.yml b/changelogs/unreleased/20492-access-token-scopes.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a9424ded6623c549a570f652d2490b2cce7a2826
--- /dev/null
+++ b/changelogs/unreleased/20492-access-token-scopes.yml
@@ -0,0 +1,4 @@
+---
+title: Add scopes for personal access tokens and OAuth tokens
+merge_request: 5951
+author:
diff --git a/changelogs/unreleased/22604-manual-actions.yml b/changelogs/unreleased/22604-manual-actions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7335e59729261483c5a9467e824a2b0cfd5f027a
--- /dev/null
+++ b/changelogs/unreleased/22604-manual-actions.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve "Manual actions on pipeline graph"
+merge_request: 7931
+author:
diff --git a/changelogs/unreleased/22849-ci-build-ref-slug.yml b/changelogs/unreleased/22849-ci-build-ref-slug.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b159ecca6d8a451ff47b98b0e78c01d44f613e23
--- /dev/null
+++ b/changelogs/unreleased/22849-ci-build-ref-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce $CI_BUILD_REF_SLUG
+merge_request: 8072
+author: 
diff --git a/changelogs/unreleased/22864-add-environment-slug.yml b/changelogs/unreleased/22864-add-environment-slug.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f90f79337d5c73137947d76701617d3fcfdaa7d5
--- /dev/null
+++ b/changelogs/unreleased/22864-add-environment-slug.yml
@@ -0,0 +1,4 @@
+---
+title: Add a slug to environments
+merge_request: 7983
+author:
diff --git a/changelogs/unreleased/22864-kubernetes-service.yml b/changelogs/unreleased/22864-kubernetes-service.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ea1323cbeb02966c918a2f5d53bef96485397fbb
--- /dev/null
+++ b/changelogs/unreleased/22864-kubernetes-service.yml
@@ -0,0 +1,4 @@
+---
+title: Introduce deployment services, starting with a KubernetesService
+merge_request: 7994
+author: 
diff --git a/changelogs/unreleased/23573-sort-functionality-for-project-member.yml b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
new file mode 100644
index 0000000000000000000000000000000000000000..73de0a6351b5725ed54289ba5bfafae14d40a20c
--- /dev/null
+++ b/changelogs/unreleased/23573-sort-functionality-for-project-member.yml
@@ -0,0 +1,4 @@
+---
+title: Add sorting functionality for group/project members
+merge_request: 7032
+author:
diff --git a/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b9d84c0ce31df4bf551ca87c12ec6340bb3247fa
--- /dev/null
+++ b/changelogs/unreleased/24803-change-cursor-for-ca-stages.yml
@@ -0,0 +1,5 @@
+---
+title: Changed cursor icon to pointer when mousing over stages on the Cycle Analytics
+  pages
+merge_request: 
+author: Ryan Harris
diff --git a/changelogs/unreleased/24824-dropdown-items-focus.yml b/changelogs/unreleased/24824-dropdown-items-focus.yml
new file mode 100644
index 0000000000000000000000000000000000000000..66970c2a9a58f26d0e4a031fc596b854749770bd
--- /dev/null
+++ b/changelogs/unreleased/24824-dropdown-items-focus.yml
@@ -0,0 +1,4 @@
+---
+title: Add focus state to dropdown items
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cc7c260482483e5082affdf037a3e666d3491e9a
--- /dev/null
+++ b/changelogs/unreleased/24877-bulk-edit-only-keeps-common-labels-when-searching.yml
@@ -0,0 +1,4 @@
+---
+title: Improve bulk assignment for issuables
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/24927-custom-event-polyfill-test.yml b/changelogs/unreleased/24927-custom-event-polyfill-test.yml
new file mode 100644
index 0000000000000000000000000000000000000000..879c28a951e7673003d7383b408ea47124470641
--- /dev/null
+++ b/changelogs/unreleased/24927-custom-event-polyfill-test.yml
@@ -0,0 +1,4 @@
+---
+title: Adds tests for custom event polyfill
+merge_request: 7996
+author:
diff --git a/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
new file mode 100644
index 0000000000000000000000000000000000000000..531b0f83099c672ee4fb2cb5a99ce7829386accf
--- /dev/null
+++ b/changelogs/unreleased/25144-gitlab-ce-mattermost-slash-command-for-issue-create-needs-better-documentation.yml
@@ -0,0 +1,4 @@
+---
+title: Improve help message for issue create slash command
+merge_request: 7850
+author: 
diff --git a/changelogs/unreleased/25207-text-overflow-env-table.yml b/changelogs/unreleased/25207-text-overflow-env-table.yml
new file mode 100644
index 0000000000000000000000000000000000000000..69348281a5036810f0951dcdfaa62381dd0e479d
--- /dev/null
+++ b/changelogs/unreleased/25207-text-overflow-env-table.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent enviroment table to overflow when name has underscores
+merge_request: 8142
+author:
diff --git a/changelogs/unreleased/25301-git-2-11-force-push-bug.yml b/changelogs/unreleased/25301-git-2-11-force-push-bug.yml
new file mode 100644
index 0000000000000000000000000000000000000000..afe57729c483a90e0f8f2b268bbfc258314047b1
--- /dev/null
+++ b/changelogs/unreleased/25301-git-2-11-force-push-bug.yml
@@ -0,0 +1,4 @@
+---
+title: Accept environment variables from the `pre-receive` script
+merge_request: 7967
+author: 
diff --git a/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b12eab26b67b18e00a8ebd222b142b72b9f8b917
--- /dev/null
+++ b/changelogs/unreleased/25339-2-webhooks-fired-for-issue-closed-and-reopened.yml
@@ -0,0 +1,4 @@
+---
+title: Ensure issuable state changes only fire webhooks once
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c6a92547c5cb970afe9e1c14509bc07c48555866
--- /dev/null
+++ b/changelogs/unreleased/25534-adding-a-way-to-go-back-on-error-pages.yml
@@ -0,0 +1,4 @@
+---
+title: Added go back anchor on error pages.
+merge_request: 8087
+author:
diff --git a/changelogs/unreleased/25617-todos-filter-placeholder.yml b/changelogs/unreleased/25617-todos-filter-placeholder.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5d0adb04ef3c1b4b721200f9a4d6e79cec1b46e4
--- /dev/null
+++ b/changelogs/unreleased/25617-todos-filter-placeholder.yml
@@ -0,0 +1,4 @@
+---
+title: 25617 Fix placeholder color of todo filters
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0a81124de0deb6064ad7f658b491eed50eadb881
--- /dev/null
+++ b/changelogs/unreleased/25743-clean-up-css-for-project-alerts-and-flash-notifications.yml
@@ -0,0 +1,4 @@
+---
+title: fix colors and margins for adjacent alert banners
+merge_request: 8151
+author: 
diff --git a/changelogs/unreleased/8003-katex-math.yml b/changelogs/unreleased/8003-katex-math.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a40dcde13930c404d1557450479fbdc69912a15c
--- /dev/null
+++ b/changelogs/unreleased/8003-katex-math.yml
@@ -0,0 +1,4 @@
+---
+title: Added support for math rendering, using KaTeX, in Markdown and asciidoc
+merge_request: 8003
+author: Munken
diff --git a/changelogs/unreleased/abuse_report-fixture.yml b/changelogs/unreleased/abuse_report-fixture.yml
new file mode 100644
index 0000000000000000000000000000000000000000..47478a2048b8d991965270a1d3a8e13e5c04d1f7
--- /dev/null
+++ b/changelogs/unreleased/abuse_report-fixture.yml
@@ -0,0 +1,4 @@
+---
+title: Replace static fixture for abuse_reports_spec
+merge_request: 7644
+author: winniehell
diff --git a/changelogs/unreleased/add_info_to_qr.yml b/changelogs/unreleased/add_info_to_qr.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a4b0354a9c95d3df05e2396d4128bfab4fc9db09
--- /dev/null
+++ b/changelogs/unreleased/add_info_to_qr.yml
@@ -0,0 +1,4 @@
+---
+title: Add GitLab host to 2FA QR code and manual info
+merge_request: 6941
+author: 
diff --git a/changelogs/unreleased/bitbucket-oauth2.yml b/changelogs/unreleased/bitbucket-oauth2.yml
new file mode 100644
index 0000000000000000000000000000000000000000..97d82518b7b96f0d195e59e6b00a2ee273aa8c1a
--- /dev/null
+++ b/changelogs/unreleased/bitbucket-oauth2.yml
@@ -0,0 +1,4 @@
+---
+title: Refactor Bitbucket importer to use BitBucket API Version 2
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/change_development_build_fixtures.yml b/changelogs/unreleased/change_development_build_fixtures.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b5dc3792745a63f11b1c4929779167bcd99a2704
--- /dev/null
+++ b/changelogs/unreleased/change_development_build_fixtures.yml
@@ -0,0 +1,4 @@
+---
+title: Ci::Builds have same ref as Ci::Pipeline in dev fixtures
+merge_request: 
+author: twonegatives 
diff --git a/changelogs/unreleased/dockerfile-templates.yml b/changelogs/unreleased/dockerfile-templates.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e4db46cdf9ac8b991ec0780e4aa6620c0f53855b
--- /dev/null
+++ b/changelogs/unreleased/dockerfile-templates.yml
@@ -0,0 +1,4 @@
+---
+title: Add support for Dockerfile templates
+merge_request: 7247
+author:
diff --git a/changelogs/unreleased/expose-deployment-variables.yml b/changelogs/unreleased/expose-deployment-variables.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7663d5b6ae5ae801a099be2dca0ccca7a021b497
--- /dev/null
+++ b/changelogs/unreleased/expose-deployment-variables.yml
@@ -0,0 +1,4 @@
+---
+title: Pass variables from deployment project services to CI runner
+merge_request: 8107
+author:
diff --git a/changelogs/unreleased/file-template-dropwdown-proper-position.yml b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
new file mode 100644
index 0000000000000000000000000000000000000000..cf2a622b7e6b010a9d67ba3141ee054953f754a1
--- /dev/null
+++ b/changelogs/unreleased/file-template-dropwdown-proper-position.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed file template dropdown for the "New File" editor for smaller/zoomed screens
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..aa5ad5cd8d6a4adb084ed697db5d0092d28bfb43
--- /dev/null
+++ b/changelogs/unreleased/fix-slack-pipeline-message-by-api.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Slack pipeline message from pipelines made by API
+merge_request: 8059
+author:
diff --git a/changelogs/unreleased/gem-update-grape.yml b/changelogs/unreleased/gem-update-grape.yml
new file mode 100644
index 0000000000000000000000000000000000000000..46b6702d9fd36e77be9e00785ac4ee2d80564857
--- /dev/null
+++ b/changelogs/unreleased/gem-update-grape.yml
@@ -0,0 +1,4 @@
+---
+title: 'Gem update: Update grape to 0.18.0'
+merge_request:
+author: Robert Schilling
diff --git a/changelogs/unreleased/gitlab-workhorse-multipart.yml b/changelogs/unreleased/gitlab-workhorse-multipart.yml
new file mode 100644
index 0000000000000000000000000000000000000000..23c2139cf9311f3634d3f2d82386dc0ffb09bd10
--- /dev/null
+++ b/changelogs/unreleased/gitlab-workhorse-multipart.yml
@@ -0,0 +1,4 @@
+---
+title: Replace Rack::Multipart with GitLab-Workhorse based solution
+merge_request: 5867
+author: 
diff --git a/changelogs/unreleased/issue_22269.yml b/changelogs/unreleased/issue_22269.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6b7164aff77d614e96d2375a437637bf8d97116e
--- /dev/null
+++ b/changelogs/unreleased/issue_22269.yml
@@ -0,0 +1,4 @@
+---
+title: Create mattermost service
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
new file mode 100644
index 0000000000000000000000000000000000000000..db1389e202407c0826583fa7e4280e5833938951
--- /dev/null
+++ b/changelogs/unreleased/jej-24637-move-issue-visible_to_user-to-finder.yml
@@ -0,0 +1,4 @@
+---
+title: Issue#visible_to_user moved to IssuesFinder to prevent accidental use
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/jej-note-search-uses-finder.yml b/changelogs/unreleased/jej-note-search-uses-finder.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1768bdfd48733718d83591717945951683f57c48
--- /dev/null
+++ b/changelogs/unreleased/jej-note-search-uses-finder.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing Note access checks by moving Note#search to updated NoteFinder
+merge_request: 
+author: 
diff --git a/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
new file mode 100644
index 0000000000000000000000000000000000000000..11250643a23367480fee19a7815915b1aa6be17d
--- /dev/null
+++ b/changelogs/unreleased/move-admin-active-tab-spinach-tests-to-rspec.yml
@@ -0,0 +1,4 @@
+---
+title: Move admin active tab spinach tests to rspec
+merge_request: 8037
+author: Semyon Pupkov
diff --git a/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
new file mode 100644
index 0000000000000000000000000000000000000000..754af641addd5bfc5b0f7f49c39f7c92ebd987f2
--- /dev/null
+++ b/changelogs/unreleased/remove-unnecessary-message-mr-commits-tab.yml
@@ -0,0 +1,4 @@
+---
+title: Remove unnecessary commits order message
+merge_request: 8004
+author:
diff --git a/changelogs/unreleased/rounded-labels-fixes.yml b/changelogs/unreleased/rounded-labels-fixes.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e0fbc6e3b5a02a0fae9ee9271f08826ce91203fa
--- /dev/null
+++ b/changelogs/unreleased/rounded-labels-fixes.yml
@@ -0,0 +1,4 @@
+---
+title: Additional rounded label fixes
+merge_request:
+author:
diff --git a/changelogs/unreleased/seed-runner-token.yml b/changelogs/unreleased/seed-runner-token.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e8153be043ac3e3078196154b06f2367953049e6
--- /dev/null
+++ b/changelogs/unreleased/seed-runner-token.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for setting the GitLab Runners Registration Token during initial
+  database seeding
+merge_request: 6642
+author: 
diff --git a/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
new file mode 100644
index 0000000000000000000000000000000000000000..bbd7a21749356bd4597da3abf0e2b7be33fbc2d2
--- /dev/null
+++ b/changelogs/unreleased/show-commit-status-from-latest-pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Show commit status from latest pipeline
+merge_request: 7333
+author:
diff --git a/changelogs/unreleased/username-exists-root.yml b/changelogs/unreleased/username-exists-root.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1ffb3eb435c09b246e2638877993691ed14f83f2
--- /dev/null
+++ b/changelogs/unreleased/username-exists-root.yml
@@ -0,0 +1,4 @@
+---
+title: Username exists check respects relative root path
+merge_request: 
+author: 
diff --git a/config/application.rb b/config/application.rb
index 0aa2873f94a4bed120f9c8a7b5c8c198c7d847a5..782a7a3689558b228640dfda4e9146955ce63aef 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -45,7 +45,7 @@ module Gitlab
     #
     # Parameters filtered:
     # - Password (:password, :password_confirmation)
-    # - Private tokens (:private_token, :authentication_token)
+    # - Private tokens
     # - Two-factor tokens (:otp_attempt)
     # - Repo/Project Import URLs (:import_url)
     # - Build variables (:variables)
@@ -60,11 +60,13 @@ module Gitlab
       encrypted_key
       hook
       import_url
+      incoming_email_token
       key
       otp_attempt
       password
       password_confirmation
       private_token
+      runners_token
       secret_token
       sentry_dsn
       variables
@@ -85,6 +87,8 @@ module Gitlab
     config.assets.precompile << "print.css"
     config.assets.precompile << "notify.css"
     config.assets.precompile << "mailers/*.css"
+    config.assets.precompile << "katex.css"
+    config.assets.precompile << "katex.js"
     config.assets.precompile << "graphs/graphs_bundle.js"
     config.assets.precompile << "users/users_bundle.js"
     config.assets.precompile << "network/network_bundle.js"
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 327e4a7937c01fc542d95645ad02cf5b9084d5d1..b8b41a0d86c4a00d9ddafd5c1910557dea64be38 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -153,6 +153,12 @@ production: &base
     # The location where LFS objects are stored (default: shared/lfs-objects).
     # storage_path: shared/lfs-objects
 
+  ## Mattermost
+  ## For enabling Add to Mattermost button
+  mattermost:
+    enabled: false
+    host: 'https://mattermost.example.com'
+
   ## Gravatar
   ## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
   gravatar:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 0ee1b1ec6344eca9855c26b4fbf811deab222bcb..ddea325c6ca9c17b18cdf74ede14edf2e204e552 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -261,6 +261,13 @@ Settings['lfs'] ||= Settingslogic.new({})
 Settings.lfs['enabled']      = true if Settings.lfs['enabled'].nil?
 Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
 
+#
+# Mattermost
+#
+Settings['mattermost'] ||= Settingslogic.new({})
+Settings.mattermost['enabled'] = false if Settings.mattermost['enabled'].nil?
+Settings.mattermost['host'] = nil unless Settings.mattermost.enabled
+
 #
 # Gravatar
 #
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index fc4b0a72addf4190b96ce3e38a2dfc143dcff239..88cd0f5f6524dffc586a6e04ac5ad2907e7f6463 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -52,8 +52,8 @@ Doorkeeper.configure do
   # Define access token scopes for your provider
   # For more information go to
   # https://github.com/doorkeeper-gem/doorkeeper/wiki/Using-Scopes
-  default_scopes  :api
-  # optional_scopes :write, :update
+  default_scopes(*Gitlab::Auth::DEFAULT_SCOPES)
+  optional_scopes(*Gitlab::Auth::OPTIONAL_SCOPES)
 
   # Change the way client credentials are retrieved from the request object.
   # By default it retrieves first from the `HTTP_AUTHORIZATION` header, then
diff --git a/config/initializers/math_lexer.rb b/config/initializers/math_lexer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8a3388a267e83e11abceed020018131ce80e4cef
--- /dev/null
+++ b/config/initializers/math_lexer.rb
@@ -0,0 +1,2 @@
+# Touch the lexers so it is registered with Rouge
+Rouge::Lexers::Math
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 26c30e523a765b939bba432f43148723c89df0ba..ab5a0561b8c6d71a2334ba121bc8e782587f6677 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -26,3 +26,9 @@ if Gitlab.config.omniauth.enabled
     end
   end
 end
+
+module OmniAuth
+  module Strategies
+    autoload :Bitbucket, Rails.root.join('lib', 'omniauth', 'strategies', 'bitbucket')
+  end
+end
diff --git a/config/initializers/public_key.rb b/config/initializers/public_key.rb
deleted file mode 100644
index e4f09a2d02058d36952d667b866e2ff603942d42..0000000000000000000000000000000000000000
--- a/config/initializers/public_key.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-path = File.expand_path("~/.ssh/bitbucket_rsa.pub")
-Gitlab::BitbucketImport.public_key = File.read(path) if File.exist?(path)
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 1d7a3f03ace85b9c0d89b995701ef783f7cf84ed..5a7365bb0f6fd64a7d55bf65fbb7300ae87fd116 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -34,7 +34,7 @@ Sidekiq.configure_server do |config|
   # Database pool should be at least `sidekiq_concurrency` + 2
   # For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
   config = ActiveRecord::Base.configurations[Rails.env] ||
-                Rails.application.config.database_configuration[Rails.env]
+    Rails.application.config.database_configuration[Rails.env]
   config['pool'] = Sidekiq.options[:concurrency] + 2
   ActiveRecord::Base.establish_connection(config)
   Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
diff --git a/config/initializers/workhorse_multipart.rb b/config/initializers/workhorse_multipart.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3e2f25c354aa142d1ecda70683009fd93ac3f211
--- /dev/null
+++ b/config/initializers/workhorse_multipart.rb
@@ -0,0 +1,3 @@
+Rails.application.configure do |config|
+  config.middleware.use(Gitlab::Middleware::Multipart)
+end
diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml
index a4032a2142092d837a4fb42dac67a66d081891ec..1d728282d90fcbf17d20ca88870a6a6903a55d99 100644
--- a/config/locales/doorkeeper.en.yml
+++ b/config/locales/doorkeeper.en.yml
@@ -59,6 +59,7 @@ en:
           unknown: "The access token is invalid"
     scopes:
       api: Access your API
+      read_user: Read user information
 
     flash:
       applications:
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 5ae985da56107ab0ad465af2710b763a771b7849..0dd2c8f7aef7df4646c3710da07874bcacbc8739 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -28,9 +28,19 @@ namespace :admin do
 
   resources :applications
 
-  resources :groups, constraints: { id: /[^\/]+/ } do
-    member do
+  resources :groups, only: [:index, :new, :create]
+
+  scope(path: 'groups/*id',
+        controller: :groups,
+        constraints: { id: Gitlab::Regex.namespace_route_regex }) do
+
+    scope(as: :group) do
       put :members_update
+      get :edit, action: :edit
+      get '/', action: :show
+      patch '/', action: :update
+      put '/', action: :update
+      delete '/', action: :destroy
     end
   end
 
@@ -50,14 +60,13 @@ namespace :admin do
   resource :system_info, controller: 'system_info', only: [:show]
   resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
 
-  resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
-    root to: 'projects#index', as: :projects
+  resources :projects, only: [:index]
 
+  scope(path: 'projects/*namespace_id', as: :namespace) do
     resources(:projects,
               path: '/',
-              constraints: { id: /[a-zA-Z.0-9_\-]+/ },
-              only: [:index, :show]) do
-      root to: 'projects#show'
+              constraints: { id: Gitlab::Regex.project_route_regex },
+              only: [:show]) do
 
       member do
         put :transfer
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 0754f0ec3b0a1cca0a6b98a6cc2d820f834fd763..e17d6bae10c003deed59cc9a339cfe78dbdd28e0 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -11,6 +11,18 @@ constraints(ProjectUrlConstrainer.new) do
           module: :projects,
           as: :project) do
 
+      resources :autocomplete_sources, only: [] do
+        collection do
+          get 'emojis'
+          get 'members'
+          get 'issues'
+          get 'merge_requests'
+          get 'labels'
+          get 'milestones'
+          get 'commands'
+        end
+      end
+
       #
       # Templates
       #
@@ -316,7 +328,6 @@ constraints(ProjectUrlConstrainer.new) do
         post :remove_export
         post :generate_new_export
         get :download_export
-        get :autocomplete_sources
         get :activity
         get :refs
         put :new_issue_address
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 08ad3097d343a26852412ca475e1bd02f7306ae3..19e001854d20490bada6125a394e18c9be9dfabc 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -115,7 +115,7 @@ class Gitlab::Seeder::Pipelines
 
   def job_attributes(pipeline, opts)
     { name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
-      ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
+      ref: pipeline.ref, tag: false, user: build_user, project: @project, pipeline: pipeline,
       created_at: Time.now, updated_at: Time.now
     }.merge(opts)
   end
diff --git a/db/fixtures/production/010_settings.rb b/db/fixtures/production/010_settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5522f31629ac65e84305b76c50d9e7a23073a3fb
--- /dev/null
+++ b/db/fixtures/production/010_settings.rb
@@ -0,0 +1,16 @@
+if ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'].present?
+  settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults
+  settings.set_runners_registration_token(ENV['GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN'])
+
+  if settings.save
+    puts "Saved Runner Registration Token".color(:green)
+  else
+    puts "Could not save Runner Registration Token".color(:red)
+    puts
+    settings.errors.full_messages.map do |message|
+      puts "--> #{message}".color(:red)
+    end
+    puts
+    exit 1
+  end
+end
diff --git a/db/migrate/20141006143943_move_slack_service_to_webhook.rb b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
index 8cb120f7007b358f02ed253c9111d25112730481..42e88d6d6e3904102d8a89612823bb1a7eaced38 100644
--- a/db/migrate/20141006143943_move_slack_service_to_webhook.rb
+++ b/db/migrate/20141006143943_move_slack_service_to_webhook.rb
@@ -1,7 +1,11 @@
 # rubocop:disable all
 class MoveSlackServiceToWebhook < ActiveRecord::Migration
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Move old fields "token" and "subdomain" to one single field "webhook"'
+
   def change
-    SlackService.all.each do |slack_service|
+    SlackNotificationService.all.each do |slack_service|
       if ["token", "subdomain"].all? { |property| slack_service.properties.key? property }
         token = slack_service.properties['token']
         subdomain = slack_service.properties['subdomain']
diff --git a/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb
new file mode 100644
index 0000000000000000000000000000000000000000..91479de840b779f24c73cbaa9c67334c8458daba
--- /dev/null
+++ b/db/migrate/20160823083941_add_column_scopes_to_personal_access_tokens.rb
@@ -0,0 +1,19 @@
+# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
+# It's easier to achieve this by adding the column with the `['api']` default, and then changing the default to
+# `[]`.
+
+class AddColumnScopesToPersonalAccessTokens < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  def up
+    add_column_with_default :personal_access_tokens, :scopes, :string, default: ['api'].to_yaml
+  end
+
+  def down
+    remove_column :personal_access_tokens, :scopes
+  end
+end
diff --git a/db/migrate/20161130095245_fill_routes_table.rb b/db/migrate/20161130095245_fill_routes_table.rb
index 6754e58300052bdc9165b16cace5e6c454f68e02..c3536d6d911a006d88e2a8df0377593b2a02f1bf 100644
--- a/db/migrate/20161130095245_fill_routes_table.rb
+++ b/db/migrate/20161130095245_fill_routes_table.rb
@@ -16,6 +16,6 @@ class FillRoutesTable < ActiveRecord::Migration
   end
 
   def down
-    Route.delete_all(source_type: 'Namespace')
+    execute("DELETE FROM routes WHERE source_type = 'Namespace'")
   end
 end
diff --git a/db/migrate/20161130101252_fill_projects_routes_table.rb b/db/migrate/20161130101252_fill_projects_routes_table.rb
index 14700583be55a409237e8b924e2786f429dcbaa5..56ba6fcdbe35cf1c2a58e5be5e7ead3f7d908a26 100644
--- a/db/migrate/20161130101252_fill_projects_routes_table.rb
+++ b/db/migrate/20161130101252_fill_projects_routes_table.rb
@@ -8,15 +8,23 @@ class FillProjectsRoutesTable < ActiveRecord::Migration
   DOWNTIME_REASON = 'No new projects should be created during data copy'
 
   def up
-    execute <<-EOF
-      INSERT INTO routes
-      (source_id, source_type, path)
-      (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path) FROM projects
-      INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
-    EOF
+    if Gitlab::Database.postgresql?
+      execute <<-EOF
+        INSERT INTO routes (source_id, source_type, path)
+        (SELECT DISTINCT ON (namespaces.path, projects.path) projects.id, 'Project', concat(namespaces.path, '/', projects.path)
+         FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id
+         ORDER BY namespaces.path, projects.path, projects.id DESC)
+      EOF
+    else
+      execute <<-EOF
+        INSERT INTO routes (source_id, source_type, path)
+        (SELECT projects.id, 'Project', concat(namespaces.path, '/', projects.path)
+         FROM projects INNER JOIN namespaces ON projects.namespace_id = namespaces.id)
+      EOF
+    end
   end
 
   def down
-    Route.delete_all(source_type: 'Project')
+    execute("DELETE FROM routes WHERE source_type = 'Project'")
   end
 end
diff --git a/db/migrate/20161202152031_remove_duplicates_from_routes.rb b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
index 510796e05f26e7040d359d47a78314c626cafb1d..d73b08475064dc444ea0da78d5ab2ecd4e508805 100644
--- a/db/migrate/20161202152031_remove_duplicates_from_routes.rb
+++ b/db/migrate/20161202152031_remove_duplicates_from_routes.rb
@@ -7,20 +7,21 @@ class RemoveDuplicatesFromRoutes < ActiveRecord::Migration
   DOWNTIME = false
 
   def up
-    select_all("SELECT path FROM #{quote_table_name(:routes)} GROUP BY path HAVING COUNT(*) > 1").each do |row|
-      path = connection.quote(row['path'])
-      execute(%Q{
-        DELETE FROM #{quote_table_name(:routes)}
-        WHERE path = #{path}
-        AND id != (
-          SELECT id FROM (
-            SELECT max(id) AS id
-            FROM #{quote_table_name(:routes)}
-            WHERE path = #{path}
-          ) max_ids
-        )
-      })
-    end
+    # We can skip this migration when running a PostgreSQL database because
+    # we use an optimized query in the "FillProjectsRoutesTable" migration
+    # to fill these values that avoid duplicate entries in the routes table.
+    return unless Gitlab::Database.mysql?
+
+    execute <<-EOF
+      DELETE duplicated_rows.*
+      FROM routes AS duplicated_rows
+        INNER JOIN (
+          SELECT path, MAX(id) as max_id
+          FROM routes
+          GROUP BY path
+          HAVING COUNT(*) > 1
+        ) AS good_rows ON good_rows.path = duplicated_rows.path AND good_rows.max_id <> duplicated_rows.id;
+    EOF
   end
 
   def down
diff --git a/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2977917f2d10585c2a1e0e68053d1d92f1c44a89
--- /dev/null
+++ b/db/migrate/20161206153749_remove_uniq_path_index_from_namespace.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUniqPathIndexFromNamespace < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    constraint_name = 'namespaces_path_key'
+
+    transaction do
+      if index_exists?(:namespaces, :path)
+        remove_index(:namespaces, :path)
+      end
+
+      # In some bizarre cases PostgreSQL might have a separate unique constraint
+      # that we'll need to drop.
+      if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+        execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};")
+      end
+    end
+  end
+
+  def down
+    unless index_exists?(:namespaces, :path)
+      add_concurrent_index(:namespaces, :path, unique: true)
+    end
+  end
+
+  def constraint_exists?(name)
+    indexes(:namespaces).map(&:name).include?(name)
+  end
+end
diff --git a/db/migrate/20161206153751_add_path_index_to_namespace.rb b/db/migrate/20161206153751_add_path_index_to_namespace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b0bac7d121e3763be9e104511b205c9265777db1
--- /dev/null
+++ b/db/migrate/20161206153751_add_path_index_to_namespace.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPathIndexToNamespace < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    add_concurrent_index :namespaces, :path
+  end
+
+  def down
+    if index_exists?(:namespaces, :path)
+      remove_index :namespaces, :path
+    end
+  end
+end
diff --git a/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cc9d4974baaffb85f24ac9871bd042c4f66bd1d0
--- /dev/null
+++ b/db/migrate/20161206153753_remove_uniq_name_index_from_namespace.rb
@@ -0,0 +1,36 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RemoveUniqNameIndexFromNamespace < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    constraint_name = 'namespaces_name_key'
+
+    transaction do
+      if index_exists?(:namespaces, :name)
+        remove_index(:namespaces, :name)
+      end
+
+      # In some bizarre cases PostgreSQL might have a separate unique constraint
+      # that we'll need to drop.
+      if constraint_exists?(constraint_name) && Gitlab::Database.postgresql?
+        execute("ALTER TABLE namespaces DROP CONSTRAINT IF EXISTS #{constraint_name};")
+      end
+    end
+  end
+
+  def down
+    unless index_exists?(:namespaces, :name)
+      add_concurrent_index(:namespaces, :name, unique: true)
+    end
+  end
+
+  def constraint_exists?(name)
+    indexes(:namespaces).map(&:name).include?(name)
+  end
+end
diff --git a/db/migrate/20161206153754_add_name_index_to_namespace.rb b/db/migrate/20161206153754_add_name_index_to_namespace.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b3f3cb68a99d72f64da87627d95c534e74e9d024
--- /dev/null
+++ b/db/migrate/20161206153754_add_name_index_to_namespace.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddNameIndexToNamespace < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  DOWNTIME = false
+
+  def up
+    add_concurrent_index(:namespaces, [:name, :parent_id], unique: true)
+  end
+
+  def down
+    if index_exists?(:namespaces, [:name, :parent_id])
+      remove_index :namespaces, [:name, :parent_id]
+    end
+  end
+end
diff --git a/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b74552e762dcd1657cb335ba50d148074305a529
--- /dev/null
+++ b/db/migrate/20161207231620_fixup_environment_name_uniqueness.rb
@@ -0,0 +1,53 @@
+class FixupEnvironmentNameUniqueness < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Renaming non-unique environments'
+
+  def up
+    environments = Arel::Table.new(:environments)
+
+    # Get all [project_id, name] pairs that occur more than once
+    finder_sql = environments.
+      group(environments[:project_id], environments[:name]).
+      having(Arel.sql("COUNT(1)").gt(1)).
+      project(environments[:project_id], environments[:name]).
+      to_sql
+
+    conflicting = connection.exec_query(finder_sql)
+
+    conflicting.rows.each do |project_id, name|
+      fix_duplicates(project_id, name)
+    end
+  end
+
+  def down
+    # Nothing to do
+  end
+
+  # Rename conflicting environments by appending "-#{id}" to all but the first
+  def fix_duplicates(project_id, name)
+    environments = Arel::Table.new(:environments)
+    finder_sql = environments.
+      where(environments[:project_id].eq(project_id)).
+      where(environments[:name].eq(name)).
+      order(environments[:id].asc).
+      project(environments[:id], environments[:name]).
+      to_sql
+
+    # Now we have the data for all the conflicting rows
+    conflicts = connection.exec_query(finder_sql).rows
+    conflicts.shift # Leave the first row alone
+
+    conflicts.each do |id, name|
+      update_sql =
+        Arel::UpdateManager.new(ActiveRecord::Base).
+        table(environments).
+        set(environments[:name] => name + "-" + id.to_s).
+        where(environments[:id].eq(id)).
+        to_sql
+
+      connection.exec_update(update_sql, self.class.name, [])
+    end
+  end
+end
diff --git a/db/migrate/20161207231621_create_environment_name_unique_index.rb b/db/migrate/20161207231621_create_environment_name_unique_index.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ac680c8d10f5334afbf8e45ed0cd64294891e5b9
--- /dev/null
+++ b/db/migrate/20161207231621_create_environment_name_unique_index.rb
@@ -0,0 +1,18 @@
+class CreateEnvironmentNameUniqueIndex < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Making a non-unique index into a unique index'
+
+  def up
+    remove_index :environments, [:project_id, :name]
+    add_concurrent_index :environments, [:project_id, :name], unique: true
+  end
+
+  def down
+    remove_index :environments, [:project_id, :name], unique: true
+    add_concurrent_index :environments, [:project_id, :name]
+  end
+end
diff --git a/db/migrate/20161207231626_add_environment_slug.rb b/db/migrate/20161207231626_add_environment_slug.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7153e6a32b1cdc93088899852d3c17824cefa8ba
--- /dev/null
+++ b/db/migrate/20161207231626_add_environment_slug.rb
@@ -0,0 +1,60 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddEnvironmentSlug < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Adding NOT NULL column environments.slug with dependent data'
+
+  # Used to generate random suffixes for the slug
+  NUMBERS = '0'..'9'
+  SUFFIX_CHARS = ('a'..'z').to_a + NUMBERS.to_a
+
+  def up
+    environments = Arel::Table.new(:environments)
+
+    add_column :environments, :slug, :string
+    finder = environments.project(:id, :name)
+
+    connection.exec_query(finder.to_sql).rows.each do |id, name|
+      updater = Arel::UpdateManager.new(ActiveRecord::Base).
+        table(environments).
+        set(environments[:slug] => generate_slug(name)).
+        where(environments[:id].eq(id))
+
+      connection.exec_update(updater.to_sql, self.class.name, [])
+    end
+
+    change_column_null :environments, :slug, false
+  end
+
+  def down
+    remove_column :environments, :slug
+  end
+
+  # Copy of the Environment#generate_slug implementation
+  def generate_slug(name)
+    # Lowercase letters and numbers only
+    slugified = name.to_s.downcase.gsub(/[^a-z0-9]/, '-')
+
+    # Must start with a letter
+    slugified = "env-" + slugified if NUMBERS.cover?(slugified[0])
+
+    # Maximum length: 24 characters (OpenShift limitation)
+    slugified = slugified[0..23]
+
+    # Cannot end with a "-" character (Kubernetes label limitation)
+    slugified = slugified[0..-2] if slugified[-1] == "-"
+
+    # Add a random suffix, shortening the current string if necessary, if it
+    # has been slugified. This ensures uniqueness.
+    slugified = slugified[0..16] + "-" + random_suffix if slugified != name
+
+    slugified
+  end
+
+  def random_suffix
+    (0..5).map { SUFFIX_CHARS.sample }.join
+  end
+end
diff --git a/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e9fcef1cd45601d53f132d211c7a743a3d2cdbcd
--- /dev/null
+++ b/db/migrate/20161209153400_add_unique_index_for_environment_slug.rb
@@ -0,0 +1,15 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddUniqueIndexForEnvironmentSlug < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Adding a *unique* index to environments.slug'
+
+  disable_ddl_transaction!
+
+  def change
+    add_concurrent_index :environments, [:project_id, :slug], unique: true
+  end
+end
diff --git a/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7278d7b5a629e06bdaf474c550de806678aaa89
--- /dev/null
+++ b/db/migrate/20161213172958_change_slack_service_to_slack_notification_service.rb
@@ -0,0 +1,14 @@
+class ChangeSlackServiceToSlackNotificationService < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Rename SlackService to SlackNotificationService'
+
+  def up
+    execute("UPDATE services SET type = 'SlackNotificationService' WHERE type = 'SlackService'")
+  end
+
+  def down
+    execute("UPDATE services SET type = 'SlackService' WHERE type = 'SlackNotificationService'")
+  end
+end
diff --git a/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7df561d82dd4ce7196f3837ef68e1eff7d1584a8
--- /dev/null
+++ b/db/post_migrate/20160824121037_change_personal_access_tokens_default_back_to_empty_array.rb
@@ -0,0 +1,19 @@
+# The default needs to be `[]`, but all existing access tokens need to have `scopes` set to `['api']`.
+# It's easier to achieve this by adding the column with the `['api']` default (regular migration), and
+# then changing the default to `[]` (in this post-migration).
+#
+# Details: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951#note_19721973
+
+class ChangePersonalAccessTokensDefaultBackToEmptyArray < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    change_column_default :personal_access_tokens, :scopes, [].to_yaml
+  end
+
+  def down
+    change_column_default :personal_access_tokens, :scopes, ['api'].to_yaml
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index ae47456084ffe42f17788b4e58f1ddb7641688e6..14801b581e6e846ce3a2b2934e532d33731da7aa 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20161212142807) do
+ActiveRecord::Schema.define(version: 20161213172958) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -98,14 +98,14 @@ ActiveRecord::Schema.define(version: 20161212142807) do
     t.text "help_page_text_html"
     t.text "shared_runners_text_html"
     t.text "after_sign_up_text_html"
+    t.boolean "sidekiq_throttling_enabled", default: false
+    t.string "sidekiq_throttling_queues"
+    t.decimal "sidekiq_throttling_factor"
     t.boolean "housekeeping_enabled", default: true, null: false
     t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
     t.integer "housekeeping_incremental_repack_period", default: 10, null: false
     t.integer "housekeeping_full_repack_period", default: 50, null: false
     t.integer "housekeeping_gc_period", default: 200, null: false
-    t.boolean "sidekiq_throttling_enabled", default: false
-    t.string "sidekiq_throttling_queues"
-    t.decimal "sidekiq_throttling_factor"
     t.boolean "html_emails_enabled", default: true
   end
 
@@ -428,9 +428,11 @@ ActiveRecord::Schema.define(version: 20161212142807) do
     t.string "external_url"
     t.string "environment_type"
     t.string "state", default: "available", null: false
+    t.string "slug", null: false
   end
 
-  add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", using: :btree
+  add_index "environments", ["project_id", "name"], name: "index_environments_on_project_id_and_name", unique: true, using: :btree
+  add_index "environments", ["project_id", "slug"], name: "index_environments_on_project_id_and_slug", unique: true, using: :btree
 
   create_table "events", force: :cascade do |t|
     t.string "target_type"
@@ -737,18 +739,18 @@ ActiveRecord::Schema.define(version: 20161212142807) do
     t.integer "visibility_level", default: 20, null: false
     t.boolean "request_access_enabled", default: false, null: false
     t.datetime "deleted_at"
-    t.text "description_html"
     t.boolean "lfs_enabled"
+    t.text "description_html"
     t.integer "parent_id"
   end
 
   add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
   add_index "namespaces", ["deleted_at"], name: "index_namespaces_on_deleted_at", using: :btree
-  add_index "namespaces", ["name"], name: "index_namespaces_on_name", unique: true, using: :btree
+  add_index "namespaces", ["name", "parent_id"], name: "index_namespaces_on_name_and_parent_id", unique: true, using: :btree
   add_index "namespaces", ["name"], name: "index_namespaces_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
   add_index "namespaces", ["owner_id"], name: "index_namespaces_on_owner_id", using: :btree
   add_index "namespaces", ["parent_id", "id"], name: "index_namespaces_on_parent_id_and_id", unique: true, using: :btree
-  add_index "namespaces", ["path"], name: "index_namespaces_on_path", unique: true, using: :btree
+  add_index "namespaces", ["path"], name: "index_namespaces_on_path", using: :btree
   add_index "namespaces", ["path"], name: "index_namespaces_on_path_trigram", using: :gin, opclasses: {"path"=>"gin_trgm_ops"}
   add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
 
@@ -852,6 +854,7 @@ ActiveRecord::Schema.define(version: 20161212142807) do
     t.datetime "expires_at"
     t.datetime "created_at", null: false
     t.datetime "updated_at", null: false
+    t.string   "scopes", default: "--- []\n", null: false
   end
 
   add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
@@ -1219,8 +1222,8 @@ ActiveRecord::Schema.define(version: 20161212142807) do
     t.datetime "otp_grace_period_started_at"
     t.boolean "ldap_email", default: false, null: false
     t.boolean "external", default: false
-    t.string "incoming_email_token"
     t.string "organization"
+    t.string "incoming_email_token"
     t.boolean "authorized_projects_populated"
   end
 
diff --git a/doc/README.md b/doc/README.md
index eba1e9845b10fe3fd986ddabd49c45a56d9aad3f..a60a53595400bad25aa6f0b6d7fd5aa14908dff9 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -8,7 +8,7 @@
 - [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
 - [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
 - [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
-- [Importing to GitLab](workflow/importing/README.md).
+- [Importing to GitLab](workflow/importing/README.md) Import your projects from GitHub, Bitbucket, GitLab.com, FogBugz and SVN into GitLab.
 - [Importing and exporting projects between instances](user/project/settings/import_export.md).
 - [Markdown](user/markdown.md) GitLab's advanced formatting system.
 - [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
index 07e548aaabe7a244a8abfbfe83a07ed4348c8006..2fc5d0355b59be93a6a0ca885713667fcf4afd95 100644
--- a/doc/administration/auth/README.md
+++ b/doc/administration/auth/README.md
@@ -7,5 +7,6 @@ providers.
   and 389 Server
 - [OmniAuth](../../integration/omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google,
   Bitbucket, Facebook, Shibboleth, Crowd and Azure
-- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
 - [CAS](../../integration/cas.md) Configure GitLab to sign in using CAS
+- [SAML](../../integration/saml.md) Configure GitLab as a SAML 2.0 Service Provider
+- [Okta](okta.md) Configure GitLab to sign in using Okta
diff --git a/doc/administration/auth/img/okta_admin_panel.png b/doc/administration/auth/img/okta_admin_panel.png
new file mode 100644
index 0000000000000000000000000000000000000000..12e219567158d918c4aac56b6c28e80360418de4
Binary files /dev/null and b/doc/administration/auth/img/okta_admin_panel.png differ
diff --git a/doc/administration/auth/img/okta_saml_settings.png b/doc/administration/auth/img/okta_saml_settings.png
new file mode 100644
index 0000000000000000000000000000000000000000..ee275ece369f9cc5ddf4d9de2ff78aba8d14e016
Binary files /dev/null and b/doc/administration/auth/img/okta_saml_settings.png differ
diff --git a/doc/administration/auth/okta.md b/doc/administration/auth/okta.md
new file mode 100644
index 0000000000000000000000000000000000000000..cb42b7743c5f782d16d43c66e4c8c0c449f33bfb
--- /dev/null
+++ b/doc/administration/auth/okta.md
@@ -0,0 +1,160 @@
+# Okta SSO provider
+
+Okta is a [Single Sign-on provider][okta-sso] that can be used to authenticate
+with GitLab.
+
+The following documentation enables Okta as a SAML provider.
+
+## Configure the Okta application
+
+1. On Okta go to the admin section and choose to **Add an App**.
+1. When the app screen comes up you see another button to **Create an App** and
+   choose SAML 2.0 on the next screen.
+1. Now, very important, add a logo
+   (you can choose it from https://about.gitlab.com/press/). You'll have to
+   crop and resize it.
+1. Next, you'll need the to fill in the SAML general config. Here's an example
+   image.
+
+    ![Okta admin panel view](img/okta_admin_panel.png)
+
+1. The last part of the configuration is the feedback section where you can
+   just say you're a customer and creating an app for internal use.
+1. When you have your app you'll have a few tabs on the top of the app's
+   profile. Click on the SAML 2.0 config instructions button which should
+   look like the following:
+
+    ![Okta SAML settings](img/okta_saml_settings.png)
+
+1. On the screen that comes up take note of the
+   **Identity Provider Single Sign-On URL** which you'll use for the
+   `idp_sso_target_url` on your GitLab config file.
+
+1. **Before you leave Okta make sure you add your user and groups if any.**
+
+---
+
+Now that the Okta app is configured, it's time to enable it in GitLab.
+
+## Configure GitLab
+
+1.  On your GitLab server, open the configuration file:
+
+    **For Omnibus GitLab installations**
+
+    ```sh
+    sudo editor /etc/gitlab/gitlab.rb
+    ```
+
+    **For installations from source**
+
+    ```sh
+    cd /home/git/gitlab
+    sudo -u git -H editor config/gitlab.yml
+    ```
+
+1.  See [Initial OmniAuth Configuration](../../integration/omniauth.md#initial-omniauth-configuration)
+    for initial settings.
+
+1.  To allow your users to use Okta to sign up without having to manually create
+    an account first, don't forget to add the following values to your
+    configuration:
+
+    **For Omnibus GitLab installations**
+
+    ```ruby
+    gitlab_rails['omniauth_allow_single_sign_on'] = ['saml']
+    gitlab_rails['omniauth_block_auto_created_users'] = false
+    ```
+
+    **For installations from source**
+
+    ```yaml
+    allow_single_sign_on: ["saml"]
+    block_auto_created_users: false
+    ```
+
+1.  You can also automatically link Okta users with existing GitLab users if
+    their email addresses match by adding the following setting:
+
+    **For Omnibus GitLab installations**
+
+    ```ruby
+    gitlab_rails['omniauth_auto_link_saml_user'] = true
+    ```
+
+    **For installations from source**
+
+    ```yaml
+    auto_link_saml_user: true
+    ```
+
+1.  Add the provider configuration.
+
+      >**Notes:**
+      >- Change the value for `assertion_consumer_service_url` to match the HTTPS endpoint
+         of GitLab (append `users/auth/saml/callback` to the HTTPS URL of your GitLab
+         installation to generate the correct value).
+      >- To get the `idp_cert_fingerprint` fingerprint, first download the
+         certificate from the Okta app you registered and then run:
+         `openssl x509 -in okta.cert -noout -fingerprint`. Substitute `okta.cert`
+         with the location of your certificate.
+      >- Change the value of `idp_sso_target_url`, with the value of the
+         **Identity Provider Single Sign-On URL** from the step when you
+         configured the Okta app.
+      >- Change the value of `issuer` to a unique name, which will identify the application
+         to the IdP.
+      >- Leave `name_identifier_format` as-is.
+
+    **For Omnibus GitLab installations**
+
+    ```ruby
+    gitlab_rails['omniauth_providers'] = [
+      {
+        name: 'saml',
+        args: {
+                 assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+                 idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+                 idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+                 issuer: 'https://gitlab.example.com',
+                 name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+               },
+        label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+      }
+    ]
+    ```
+
+    **For installations from source**
+
+    ```yaml
+    - {
+        name: 'saml',
+        args: {
+               assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
+               idp_cert_fingerprint: '43:51:43:a1:b5:fc:8b:b7:0a:3a:a9:b1:0f:66:73:a8',
+               idp_sso_target_url: 'https://gitlab.oktapreview.com/app/gitlabdev773716_gitlabsaml_1/exk8odl81tBrjpD4B0h7/sso/saml',
+               issuer: 'https://gitlab.example.com',
+               name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
+             },
+        label: 'Okta' # optional label for SAML login button, defaults to "Saml"
+      }
+    ```
+
+
+1. [Reconfigure][reconf] or [restart] GitLab for Omnibus and installations
+   from source respectively for the changes to take effect.
+
+You might want to try this out on a incognito browser window.
+
+## Configuring groups
+
+>**Note:**
+Make sure the groups exist and are assigned to the Okta app.
+
+You can take a look of the [SAML documentation][saml] on external groups since
+it works the same.
+
+[okta-sso]: https://www.okta.com/products/single-sign-on/
+[saml]: ../../integration/saml.md#external-groups
+[reconf]: ../restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart]: ../restart_gitlab.md#installations-from-source
diff --git a/doc/administration/custom_hooks.md b/doc/administration/custom_hooks.md
index 06291705702b8fc8c755758a667c4b791f3b8aa3..80e5d80aa41fa8757c0f660b724cebaf3ae5a790 100644
--- a/doc/administration/custom_hooks.md
+++ b/doc/administration/custom_hooks.md
@@ -44,22 +44,30 @@ as appropriate.
 
 ## Chained hooks support
 
-> [Introduced][93] in GitLab Shell 4.1.0.
+> [Introduced][93] in GitLab Shell 4.1.0 and GitLab 8.15.
 
-The hooks could be also placed in `hooks/<hook_name>.d` (global) or `custom_hooks/<hook_name>.d` (per project)
-directories supporting chained execution of the hooks.
+Hooks can be also placed in `hooks/<hook_name>.d` (global) or
+`custom_hooks/<hook_name>.d` (per project) directories supporting chained
+execution of the hooks.
+
+To look in a different directory for the global custom hooks (those in
+`hooks/<hook_name.d>`), set `custom_hooks_dir` in gitlab-shell config. For
+Omnibus installations, this can be set in `gitlab.rb`; and in source
+installations, this can be set in `gitlab-shell/config.yml`.
 
 The hooks are searched and executed in this order:
+
 1. `<project>.git/hooks/` - symlink to `gitlab-shell/hooks` global dir
 1. `<project>.git/hooks/<hook_name>` -  executed by `git` itself, this is `gitlab-shell/hooks/<hook_name>`
 1. `<project>.git/custom_hooks/<hook_name>` - per project hook (this is already existing behavior)
 1. `<project>.git/custom_hooks/<hook_name>.d/*` - per project hooks
-1. `<project>.git/hooks/<hook_name>.d/*` - global hooks: all executable files (minus editor backup files)
+1. `<project>.git/hooks/<hook_name>.d/*` OR `<custom_hooks_dir>/<hook_name.d>/*` - global hooks: all executable files (minus editor backup files)
 
-Files in `.d` directories need to be executable and not match the backup file pattern (`*~`).
+Files in `.d` directories need to be executable and not match the backup file
+pattern (`*~`).
 
-The hooks of the same type are executed in order and execution stops on the first
-script exiting with non-zero value.
+The hooks of the same type are executed in order and execution stops on the
+first script exiting with a non-zero value.
 
 ## Custom error messages
 
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index b4a953d1ccc0a0aea42cf07335b463f9b4f6c1d3..76029b30dd87be973182508c1a46e5145b2346e8 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -13,17 +13,18 @@ override certain values.
 
 Variable | Type | Description
 -------- | ---- | -----------
-`GITLAB_ROOT_PASSWORD`        | string  | Sets the password for the `root` user on installation
-`GITLAB_HOST`                 | string  | The full URL of the GitLab server (including `http://` or `https://`)
-`RAILS_ENV`                   | string  | The Rails environment; can be one of `production`, `development`, `staging` or `test`
-`DATABASE_URL`                | string  | The database URL; is of the form: `postgresql://localhost/blog_development`
-`GITLAB_EMAIL_FROM`           | string  | The e-mail address used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_DISPLAY_NAME`   | string  | The name used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO`       | string  | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO`       | string  | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_EMAIL_SUBJECT_SUFFIX` | string  | The e-mail subject suffix used in e-mails sent by GitLab
-`GITLAB_UNICORN_MEMORY_MIN`   | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
-`GITLAB_UNICORN_MEMORY_MAX`   | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_ROOT_PASSWORD`                     | string  | Sets the password for the `root` user on installation
+`GITLAB_HOST`                              | string  | The full URL of the GitLab server (including `http://` or `https://`)
+`RAILS_ENV`                                | string  | The Rails environment; can be one of `production`, `development`, `staging` or `test`
+`DATABASE_URL`                             | string  | The database URL; is of the form: `postgresql://localhost/blog_development`
+`GITLAB_EMAIL_FROM`                        | string  | The e-mail address used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_DISPLAY_NAME`                | string  | The name used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO`                    | string  | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO`                    | string  | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_SUBJECT_SUFFIX`              | string  | The e-mail subject suffix used in e-mails sent by GitLab
+`GITLAB_UNICORN_MEMORY_MIN`                | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_UNICORN_MEMORY_MAX`                | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN` | string  | Sets the initial registration token used for GitLab Runners
 
 ## Complete database variables
 
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index b36cf18d459325fb3dcc091fce45c6e86b861684..e4f94eb7cb68d44fbbd04216f94f72ae27f8cee8 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -44,6 +44,9 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
     gitlab_rails['db_password'] = 'DB password'
     postgresql['md5_auth_cidr_addresses'] = ['0.0.0.0/0']
     postgresql['listen_address'] = '0.0.0.0'
+
+    # Disable automatic database migrations
+    gitlab_rails['auto_migrate'] = false
     ```
 
 1. Run `sudo gitlab-ctl reconfigure` to install and configure PostgreSQL.
@@ -102,9 +105,6 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
 1. Exit the database prompt by typing `\q` and Enter.
 1. Exit the `gitlab-psql` user by running `exit` twice.
 1. Run `sudo gitlab-ctl reconfigure` a final time.
-1. Run `sudo touch /etc/gitlab/skip-auto-migrations` to prevent database migrations
-   from running on upgrade. Only the primary GitLab application server should
-   handle migrations.
 
 ---
 
diff --git a/doc/administration/high_availability/redis.md b/doc/administration/high_availability/redis.md
index f532a106bc6462de19ab32a66c4e66622c72c922..b4e7bf21e359143592bc1f1cae65fb08b5be11f7 100644
--- a/doc/administration/high_availability/redis.md
+++ b/doc/administration/high_availability/redis.md
@@ -287,14 +287,14 @@ The prerequisites for a HA Redis setup are the following:
     redis['password'] = 'redis-password-goes-here'
     ```
 
-1. To prevent database migrations from running on upgrade, run:
+1. Only the primary GitLab application server should handle migrations. To
+   prevent database migrations from running on upgrade, add the following
+   configuration to your `/etc/gitlab/gitlab.rb` file:
 
     ```
-    sudo touch /etc/gitlab/skip-auto-migrations
+    gitlab_rails['auto_migrate'] = false
     ```
 
-    Only the primary GitLab application server should handle migrations.
-
 1. [Reconfigure Omnibus GitLab][reconfigure] for the changes to take effect.
 
 ### Step 2. Configuring the slave Redis instances
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index d757a3c2a6661c6bd9b91e6a085eb541fe75f5c8..4b8d5c5cc87f73be4a966743a7ad4054d1f8035b 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -1,4 +1,4 @@
-## Log system
+# Log system
 
 GitLab has an advanced log system where everything is logged so that you
 can analyze your instance using various system log files. In addition to
@@ -9,10 +9,10 @@ documentation](http://docs.gitlab.com/ee/administration/audit_events.html)
 System log files are typically plain text in a standard log file format.
 This guide talks about how to read and use these system log files.
 
-### production.log
+## `production.log`
 
 This file lives in `/var/log/gitlab/gitlab-rails/production.log` for
-omnibus package or in `/home/git/gitlab/log/production.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/production.log` for
 installations from source. (When Gitlab is running in an environment
 other than production, the corresponding logfile is shown here.)
 
@@ -46,10 +46,10 @@ In this example we can see that server processed an HTTP request with URL
 19:34:53 +0200. Also we can see that request was processed by
 `Projects::TreeController`.
 
-### application.log
+## `application.log`
 
 This file lives in `/var/log/gitlab/gitlab-rails/application.log` for
-omnibus package or in `/home/git/gitlab/log/application.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/application.log` for
 installations from source.
 
 It helps you discover events happening in your instance such as user creation,
@@ -63,10 +63,10 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk)  was
 October 07, 2014 11:25: Project "project133" was removed
 ```
 
-### githost.log
+## `githost.log`
 
 This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for
-omnibus package or in `/home/git/gitlab/log/githost.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/githost.log` for
 installations from source.
 
 GitLab has to interact with Git repositories but in some rare cases
@@ -81,10 +81,10 @@ December 03, 2014 13:20 -> ERROR -> Command failed [1]: /usr/bin/git --git-dir=/
 error: failed to push some refs to '/Users/vsizov/gitlab-development-kit/repositories/gitlabhq/gitlab_git.git'
 ```
 
-### sidekiq.log
+## `sidekiq.log`
 
 This file lives in `/var/log/gitlab/gitlab-rails/sidekiq.log` for
-omnibus package or in `/home/git/gitlab/log/sidekiq.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq.log` for
 installations from source.
 
 GitLab uses background jobs for processing tasks which can take a long
@@ -96,10 +96,10 @@ this file. For example:
 2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
 ```
 
-### gitlab-shell.log
+## `gitlab-shell.log`
 
 This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for
-omnibus package or in `/home/git/gitlab-shell/gitlab-shell.log` for
+Omnibus GitLab packages or in `/home/git/gitlab-shell/gitlab-shell.log` for
 installations from source.
 
 GitLab shell is used by Gitlab for executing Git commands and provide
@@ -110,10 +110,10 @@ I, [2015-02-13T06:17:00.671315 #9291]  INFO -- : Adding project root/example.git
 I, [2015-02-13T06:17:00.679433 #9291]  INFO -- : Moving existing hooks directory and symlinking global hooks directory for /var/opt/gitlab/git-data/repositories/root/example.git.
 ```
 
-### unicorn\_stderr.log
+## `unicorn\_stderr.log`
 
 This file lives in `/var/log/gitlab/unicorn/unicorn_stderr.log` for
-omnibus package or in `/home/git/gitlab/log/unicorn_stderr.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/unicorn_stderr.log` for
 installations from source.
 
 Unicorn is a high-performance forking Web server which is used for
@@ -136,3 +136,13 @@ I, [2015-02-13T07:16:01.530733 #9047]  INFO -- : reaped #<Process::Status: pid 9
 I, [2015-02-13T07:16:01.534501 #13379]  INFO -- : worker=1 spawned pid=13379
 I, [2015-02-13T07:16:01.534848 #13379]  INFO -- : worker=1 ready
 ```
+
+## `repocheck.log`
+
+This file lives in `/var/log/gitlab/gitlab-rails/repocheck.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/repocheck.log` for
+installations from source.
+
+It logs information whenever a [repository check is run][repocheck] on a project.
+
+[repocheck]: repository_checks.md
diff --git a/doc/api/enviroments.md b/doc/api/enviroments.md
index 87a5fa67124996dcf9e80d93800ecb6d581b7353..1299aca8c45c7f499930034ec5578c106f188d7e 100644
--- a/doc/api/enviroments.md
+++ b/doc/api/enviroments.md
@@ -22,8 +22,9 @@ Example response:
 [
   {
     "id": 1,
-    "name": "Env1",
-    "external_url": "https://env1.example.gitlab.com"
+    "name": "review/fix-foo",
+    "slug": "review-fix-foo-dfjre3",
+    "external_url": "https://review-fix-foo-dfjre3.example.gitlab.com"
   }
 ]
 ```
@@ -54,6 +55,7 @@ Example response:
 {
   "id": 1,
   "name": "deploy",
+  "slug": "deploy",
   "external_url": "https://deploy.example.gitlab.com"
 }
 ```
@@ -85,6 +87,7 @@ Example response:
 {
   "id": 1,
   "name": "staging",
+  "slug": "staging",
   "external_url": "https://staging.example.gitlab.com"
 }
 ```
@@ -112,6 +115,7 @@ Example response:
 {
   "id": 1,
   "name": "deploy",
+  "slug": "deploy",
   "external_url": "https://deploy.example.gitlab.com"
 }
 ```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 0bc2a5210aa92c2b8cd359b58f565373502266dc..edffad555a584270f364cc9a8d9dfe6845cc2c64 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -664,7 +664,6 @@ Parameters:
 | `path` | string | no | Custom repository name for new project. By default generated based on name |
 | `default_branch` | string | no | `master` by default |
 | `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
-| `default_branch` | string | no | `master` by default |
 | `description` | string | no | Short project description |
 | `issues_enabled` | boolean | no | Enable issues for this project |
 | `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
diff --git a/doc/api/services.md b/doc/api/services.md
index 3dad953cd1e0e60ef6087ae50749f1605cc87d69..1466b8189b0386797073b88eb40e4e776fa04f41 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -703,9 +703,9 @@ Get Redmine service settings for a project.
 GET /projects/:id/services/redmine
 ```
 
-## Slack
+## Slack notifications
 
-A team communication tool for the 21st century
+Receive event notifications in Slack
 
 ### Create/Edit Slack service
 
@@ -737,6 +737,40 @@ Get Slack service settings for a project.
 GET /projects/:id/services/slack
 ```
 
+## Mattermost notifications
+
+Receive event notifications in Mattermost
+
+### Create/Edit Mattermost notifications service
+
+Set Mattermost service for a project.
+
+```
+PUT /projects/:id/services/mattermost
+```
+
+Parameters:
+
+- `webhook` (**required**) - https://mattermost.example/hooks/1298aff...
+- `username` (optional) - username
+- `channel` (optional) - #channel
+
+### Delete Mattermost notifications service
+
+Delete Mattermost Notifications service for a project.
+
+```
+DELETE /projects/:id/services/mattermost
+```
+
+### Get Mattermost notifications service settings
+
+Get Mattermost notifications service settings for a project.
+
+```
+GET /projects/:id/services/mattermost
+```
+
 ## JetBrains TeamCity CI
 
 A continuous integration and build server
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 9dd84a5ff81a257f95482eb6872c9776de499d00..bad0233a9ce117df5ab8b6ce838649c56f208b9a 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -86,6 +86,13 @@ will later see, is exposed in various places within GitLab. Each time a job that
 has an environment specified and succeeds, a deployment is recorded, remembering
 the Git SHA and environment name.
 
+>**Note:**
+Starting with GitLab 8.15, the environment name is exposed to the Runner in
+two forms: `$CI_ENVIRONMENT_NAME`, and `$CI_ENVIRONMENT_SLUG`. The first is
+the name given in `.gitlab-ci.yml` (with any variables expanded), while the
+second is a "cleaned-up" version of the name, suitable for use in URLs, DNS,
+etc.
+
 To sum up, with the above `.gitlab-ci.yml` we have achieved that:
 
 - All branches will run the `test` and `build` jobs.
@@ -157,7 +164,7 @@ that can be found in the deployments page
 job with the commit associated with it.
 
 >**Note:**
-Bare in mind that your mileage will vary and it's entirely up to how you define
+Bear in mind that your mileage will vary and it's entirely up to how you define
 the deployment process in the job's `script` whether the rollback succeeds or not.
 GitLab CI is just following orders.
 
@@ -248,7 +255,7 @@ deploy_review:
     - echo "Deploy a review app"
   environment:
     name: review/$CI_BUILD_REF_NAME
-    url: https://$CI_BUILD_REF_NAME.example.com
+    url: https://$CI_BUILD_REF_SLUG.review.example.com
   only:
     - branches
   except:
@@ -259,15 +266,25 @@ Let's break it down in pieces. The job's name is `deploy_review` and it runs
 on the `deploy` stage. The `script` at this point is fictional, you'd have to
 use your own based on your deployment. Then, we set the `environment` with the
 `environment:name` being `review/$CI_BUILD_REF_NAME`. Now that's an interesting
-one. Since the [environment name][env-name] can contain also slashes (`/`), we
-can use this pattern to distinguish between dynamic environments and the regular
+one. Since the [environment name][env-name] can contain slashes (`/`), we can
+use this pattern to distinguish between dynamic environments and the regular
 ones.
 
 So, the first part is `review`, followed by a `/` and then `$CI_BUILD_REF_NAME`
-which takes the value of the branch name. We also use the same
-`$CI_BUILD_REF_NAME` value in the `environment:url` so that the environment
-can get a specific and distinct URL for each branch. Again, the way you set up
-the webserver to serve these requests is based on your setup.
+which takes the value of the branch name. Since `$CI_BUILD_REF_NAME` itself may
+also contain `/`, or other characters that would be invalid in a domain name or
+URL, we use `$CI_ENVIRONMENT_SLUG` in the `environment:url` so that the
+environment can get a specific and distinct URL for each branch. In this case,
+given a `$CI_BUILD_REF_NAME` of `100-Do-The-Thing`, the URL will be something
+like `https://review-100-do-the-4f99a2.example.com`. Again, the way you set up
+the web server to serve these requests is based on your setup.
+
+You could also use `$CI_BUILD_REF_SLUG` in `environment:url`, e.g.:
+`https://$CI_BUILD_REF_SLUG.review.example.com`. We use `$CI_ENVIRONMENT_SLUG`
+here because it is guaranteed to be unique, but if you're using a workflow like
+[GitLab Flow][gitlab-flow], collisions are very unlikely, and you may prefer
+environment names to be more closely based on the branch name - the example
+above would give you an URL like `https://100-do-the-thing.review.example.com`
 
 Last but not least, we tell the job to run [`only`][only] on branches
 [`except`][only] master.
@@ -299,7 +316,7 @@ deploy_review:
     - echo "Deploy a review app"
   environment:
     name: review/$CI_BUILD_REF_NAME
-    url: https://$CI_BUILD_REF_NAME.example.com
+    url: https://$CI_ENVIRONMENT_SLUG.example.com
   only:
     - branches
   except:
@@ -329,16 +346,16 @@ deploy_prod:
 
 A more realistic example would include copying files to a location where a
 webserver (NGINX) could then read and serve. The example below will copy the
-`public` directory to `/srv/nginx/$CI_BUILD_REF_NAME/public`:
+`public` directory to `/srv/nginx/$CI_BUILD_REF_SLUG/public`:
 
 ```yaml
 review_app:
   stage: deploy
   script:
-    - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_NAME
+    - rsync -av --delete public /srv/nginx/$CI_BUILD_REF_SLUG
   environment:
     name: review/$CI_BUILD_REF_NAME
-    url: https://$CI_BUILD_REF_NAME.example.com
+    url: https://$CI_BUILD_REF_SLUG.example.com
 ```
 
 It is assumed that the user has already setup NGINX and GitLab Runner in the
@@ -346,7 +363,7 @@ server this job will run on.
 
 >**Note:**
 Be sure to check out the [limitations](#limitations) section for some edge
-cases regarding naming of you branches and Review Apps.
+cases regarding naming of your branches and Review Apps.
 
 ---
 
@@ -418,7 +435,7 @@ deploy_review:
     - echo "Deploy a review app"
   environment:
     name: review/$CI_BUILD_REF_NAME
-    url: https://$CI_BUILD_REF_NAME.example.com
+    url: https://$CI_ENVIRONMENT_SLUG.example.com
     on_stop: stop_review
   only:
     - branches
@@ -480,9 +497,8 @@ exist, you should see something like:
 
 ## Checkout deployments locally
 
-Since 8.13, a reference in the git repository is saved for each deployment. So
-knowing what the state is of your current environments is only a `git fetch`
-away.
+Since 8.13, a reference in the git repository is saved for each deployment, so
+knowing the state of your current environments is only a `git fetch` away.
 
 In your git config, append the `[remote "<your-remote>"]` block with an extra
 fetch line:
@@ -493,10 +509,6 @@ fetch = +refs/environments/*:refs/remotes/origin/environments/*
 
 ## Limitations
 
-1. If the branch name contains special characters (`/`), and you use the
-   `$CI_BUILD_REF_NAME` variable to dynamically create environments, there might
-   be complications during your Review Apps deployment. Follow the
-   [issue 22849][ce-22849] for more information.
 1. You are limited to use only the [CI predefined variables][variables] in the
    `environment: name`. If you try to re-use variables defined inside `script`
    as part of the environment name, it will not work.
@@ -520,6 +532,6 @@ Below are some links you may find interesting:
 [only]: yaml/README.md#only-and-except
 [onstop]: yaml/README.md#environment-on_stop
 [ce-7015]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7015
-[ce-22849]: https://gitlab.com/gitlab-org/gitlab-ce/issues/22849
+[gitlab-flow]: ../workflow/gitlab_flow.md
 [gitlab runner]: https://docs.gitlab.com/runner/
 [git-strategy]: yaml/README.md#git-strategy
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index d142fe266a2b2ea78b099d50eae548c6545cc543..d3b9611b02ea4628a652c1705af343b712376848 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -13,6 +13,7 @@ this order:
 1. [Secret variables](#secret-variables)
 1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
 1. YAML-defined [global variables](../yaml/README.md#variables)
+1. [Deployment variables](#deployment-variables)
 1. [Predefined variables](#predefined-variables-environment-variables) (are the
    lowest in the chain)
 
@@ -40,6 +41,7 @@ version of Runner required.
 | **CI_BUILD_NAME**       | all    | 0.5    | The name of the build as defined in `.gitlab-ci.yml` |
 | **CI_BUILD_STAGE**      | all    | 0.5    | The name of the stage as defined in `.gitlab-ci.yml` |
 | **CI_BUILD_REF_NAME**   | all    | all    | The branch or tag name for which project is built |
+| **CI_BUILD_REF_SLUG**   | 8.15   | all    | `$CI_BUILD_REF_NAME` lowercased, shortened to 63 bytes, and with everything except `0-9` and `a-z` replaced with `-`. Use in URLs and domain names. |
 | **CI_BUILD_REPO**       | all    | all    | The URL to clone the Git repository |
 | **CI_BUILD_TRIGGERED**  | all    | 0.5    | The flag to indicate that build was [triggered] |
 | **CI_BUILD_MANUAL**     | 8.12   | all    | The flag to indicate that build was manually started |
@@ -51,12 +53,17 @@ version of Runner required.
 | **CI_PROJECT_PATH**     | 8.10   | 0.5    | The namespace with project name |
 | **CI_PROJECT_URL**      | 8.10   | 0.5    | The HTTP address to access project |
 | **CI_PROJECT_DIR**      | all    | all    | The full path where the repository is cloned and where the build is run |
+| **CI_ENVIRONMENT_NAME** | 8.15   | all    | The name of the environment for this build |
+| **CI_ENVIRONMENT_SLUG** | 8.15   | all    | A simplified version of the environment name, suitable for inclusion in DNS, URLs, Kubernetes labels, etc. |
 | **CI_REGISTRY**         | 8.10   | 0.5    | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
 | **CI_REGISTRY_IMAGE**   | 8.10   | 0.5    | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
 | **CI_RUNNER_ID**        | 8.10   | 0.5    | The unique id of runner being used |
 | **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5    | The description of the runner as saved in GitLab |
 | **CI_RUNNER_TAGS**      | 8.10   | 0.5    | The defined runner tags |
 | **CI_DEBUG_TRACE**      | all    | 1.7    | Whether [debug tracing](#debug-tracing) is enabled |
+| **GET_SOURCES_ATTEMPTS** | 8.15    | 1.9    | Number of attempts to fetch sources running a build |
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | 8.15    | 1.9    | Number of attempts to download artifacts running a build |
+| **RESTORE_CACHE_ATTEMPTS** | 8.15    | 1.9    | Number of attempts to restore the cache running a build |
 | **GITLAB_USER_ID**      | 8.12   | all    | The id of the user who started the build |
 | **GITLAB_USER_EMAIL**   | 8.12   | all    | The email of the user who started the build |
 
@@ -145,6 +152,20 @@ Secret variables can be added by going to your project's
 
 Once you set them, they will be available for all subsequent builds.
 
+## Deployment variables
+
+>**Note:**
+This feature requires GitLab CI 8.15 or higher.
+
+[Project services](../../project_services/project_services.md) that are
+responsible for deployment configuration may define their own variables that
+are set in the build environment. These variables are only defined for
+[deployment builds](../environments.md). Please consult the documentation of
+the project services that you are using to learn which variables they define.
+
+An example project service that defines deployment variables is
+[Kubernetes Service](../../project_services/kubernetes.md).
+
 ## Debug tracing
 
 > Introduced in GitLab Runner 1.7.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 5f88974d36001ba1dc8b8c070f268770e7042f7f..7158b2e7895619241bf71dddcd79b2443eddc945 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -690,18 +690,12 @@ The `stop_review_app` job is **required** to have the following keywords defined
 #### dynamic environments
 
 > [Introduced][ce-6323] in GitLab 8.12 and GitLab Runner 1.6.
+  `$CI_ENVIRONMENT_SLUG` was [introduced][ce-7983] in GitLab 8.15
 
 `environment` can also represent a configuration hash with `name` and `url`.
 These parameters can use any of the defined [CI variables](#variables)
 (including predefined, secure variables and `.gitlab-ci.yml` variables).
 
->**Note:**
-Be aware than if the branch name contains special characters and you use the
-`$CI_BUILD_REF_NAME` variable to dynamically create environments, there might
-be complications during deployment. Follow the
-[issue 22849](https://gitlab.com/gitlab-org/gitlab-ce/issues/22849) for more
-information.
-
 For example:
 
 ```
@@ -709,15 +703,17 @@ deploy as review app:
   stage: deploy
   script: make deploy
   environment:
-    name: review-apps/$CI_BUILD_REF_NAME
-    url: https://$CI_BUILD_REF_NAME.review.example.com/
+    name: review/$CI_BUILD_REF_NAME
+    url: https://$CI_ENVIRONMENT_SLUG.example.com/
 ```
 
 The `deploy as review app` job will be marked as deployment to dynamically
-create the `review-apps/$CI_BUILD_REF_NAME` environment, which `$CI_BUILD_REF_NAME`
-is an [environment variable][variables] set by the Runner. If for example the
-`deploy as review app` job was run in a branch named `pow`, this environment
-should be accessible under `https://pow.review.example.com/`.
+create the `review/$CI_BUILD_REF_NAME` environment, where `$CI_BUILD_REF_NAME`
+is an [environment variable][variables] set by the Runner. The
+`$CI_ENVIRONMENT_SLUG` variable is based on the environment name, but suitable
+for inclusion in URLs. In this case, if the `deploy as review app` job was run
+in a branch named `pow`, this environment would be accessible with an URL like
+`https://review-pow-aaaaaa.example.com/`.
 
 This of course implies that the underlying server which hosts the application
 is properly configured.
@@ -1038,6 +1034,31 @@ variables:
   GIT_STRATEGY: none
 ```
 
+## Build stages attempts
+
+> Introduced in GitLab, it requires GitLab Runner v1.9+.
+
+You can set the number for attempts the running build will try to execute each
+of the following stages:
+
+| Variable                | Description |
+|-------------------------|-------------|
+| **GET_SOURCES_ATTEMPTS** | Number of attempts to fetch sources running a build |
+| **ARTIFACT_DOWNLOAD_ATTEMPTS** | Number of attempts to download artifacts running a build |
+| **RESTORE_CACHE_ATTEMPTS** | Number of attempts to restore the cache running a build |
+
+The default is one single attempt.
+
+Example:
+
+```
+variables:
+  GET_SOURCES_ATTEMPTS: "3"
+```
+
+You can set them in the global [`variables`](#variables) section or the [`variables`](#job-variables)
+section for individual jobs.
+
 ## Shallow cloning
 
 > Introduced in GitLab 8.9 as an experimental feature. May change in future
@@ -1246,3 +1267,4 @@ CI with various languages.
 [environment]: ../environments.md
 [ce-6669]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6669
 [variables]: ../variables/README.md
+[ce-7983]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7983
diff --git a/doc/development/ux_guide/basics.md b/doc/development/ux_guide/basics.md
index 76b739386a598eafa3fbc9a67921efa31c43e0bf..e81729556d87c99c9d1827a26e75fb5d2207fd15 100644
--- a/doc/development/ux_guide/basics.md
+++ b/doc/development/ux_guide/basics.md
@@ -5,6 +5,7 @@
 * [Typography](#typography)
 * [Icons](#icons)
 * [Color](#color)
+* [Cursors](#cursors)
 
 ---
 
@@ -59,3 +60,18 @@ GitLab uses Font Awesome icons throughout our interface.
 
 > TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
 
+---
+
+## Cursors
+The mouse cursor is key in helping users understand how to interact with elements on the screen.
+
+| | |
+| :------: | :------- |
+| ![Default cursor](img/cursors-default.png) | Default cursor |
+| ![Pointer cursor](img/cursors-pointer.png) | Pointer cursor: used to indicate that you can click on an element to invoke a command or navigate, such as links and buttons |
+| ![Move cursor](img/cursors-move.png) | Move cursor: used to indicate that you can move an element around on the screen |
+| ![Pan opened cursor](img/cursors-panopened.png) | Pan cursor (opened): indicates that you can grab and move the entire canvas, affecting what is seen in the view port. |
+| ![Pan closed cursor](img/cursors-panclosed.png) | Pan cursor (closed): indicates that you are actively panning the canvas. |
+| ![I-beam cursor](img/cursors-ibeam.png) | I-beam cursor: indicates that this is either text that you can select and copy, or a text field that you can click into to enter text. |
+
+
diff --git a/doc/development/ux_guide/img/cursors-default.png b/doc/development/ux_guide/img/cursors-default.png
new file mode 100644
index 0000000000000000000000000000000000000000..c188ec4e3512da6b03c0fc9cd8463c60e11af9ff
Binary files /dev/null and b/doc/development/ux_guide/img/cursors-default.png differ
diff --git a/doc/development/ux_guide/img/cursors-ibeam.png b/doc/development/ux_guide/img/cursors-ibeam.png
new file mode 100644
index 0000000000000000000000000000000000000000..86f286399826d632d6f8e1b9f7f26ac2620dc8e7
Binary files /dev/null and b/doc/development/ux_guide/img/cursors-ibeam.png differ
diff --git a/doc/development/ux_guide/img/cursors-move.png b/doc/development/ux_guide/img/cursors-move.png
new file mode 100644
index 0000000000000000000000000000000000000000..a9c610eaa882105d4011de7ba220fa0e4256a207
Binary files /dev/null and b/doc/development/ux_guide/img/cursors-move.png differ
diff --git a/doc/development/ux_guide/img/cursors-panclosed.png b/doc/development/ux_guide/img/cursors-panclosed.png
new file mode 100644
index 0000000000000000000000000000000000000000..6d247a765aca5cf4f4412d607c12c868c688e848
Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panclosed.png differ
diff --git a/doc/development/ux_guide/img/cursors-panopened.png b/doc/development/ux_guide/img/cursors-panopened.png
new file mode 100644
index 0000000000000000000000000000000000000000..76f2eeda8316cdf3764aeeb48d6c0eab139bceb2
Binary files /dev/null and b/doc/development/ux_guide/img/cursors-panopened.png differ
diff --git a/doc/development/ux_guide/img/cursors-pointer.png b/doc/development/ux_guide/img/cursors-pointer.png
new file mode 100644
index 0000000000000000000000000000000000000000..d86dd955fa78b158255739595a50def06f5d31dc
Binary files /dev/null and b/doc/development/ux_guide/img/cursors-pointer.png differ
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 9122dc62e39f984ed9d5e16b7d3fb5edc93a7ede..5df6e103f424b8cfa43798458d04e7742342970d 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -18,8 +18,10 @@ Bitbucket.org.
 ## Bitbucket OmniAuth provider
 
 > **Note:**
-Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
-before proceeding with setting up the Bitbucket integration.
+GitLab 8.15 significantly simplified the way to integrate Bitbucket.org with
+GitLab. You are encouraged to upgrade your GitLab instance if you haven't done
+already. If you're using GitLab 8.14 and below, [use the previous integration
+docs][bb-old].
 
 To enable the Bitbucket OmniAuth provider you must register your application
 with Bitbucket.org. Bitbucket will generate an application ID and secret key for
@@ -44,14 +46,12 @@ you to use.
     And grant at least the following permissions:
 
     ```
-    Account: Email
-    Repositories: Read, Admin
+    Account: Email, Read
+    Repositories: Read
+    Pull Requests: Read
+    Issues: Read
     ```
 
-    >**Note:**
-    It may seem a little odd to giving GitLab admin permissions to repositories,
-    but this is needed in order for GitLab to be able to clone the repositories.
-
     ![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
 
 1.  Select **Save**.
@@ -93,7 +93,8 @@ you to use.
     ```yaml
     - { name: 'bitbucket',
         app_id: 'BITBUCKET_APP_KEY',
-        app_secret: 'BITBUCKET_APP_SECRET' }
+        app_secret: 'BITBUCKET_APP_SECRET',
+        url: 'https://bitbucket.org/' }
     ```
 
     ---
@@ -112,100 +113,12 @@ well, the user will be returned to GitLab and will be signed in.
 
 ## Bitbucket project import
 
-To allow projects to be imported directly into GitLab, Bitbucket requires two
-extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
-
-Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
-instead requires GitLab to use SSH and identify itself using your GitLab
-server's SSH key.
-
-To be able to access repositories on Bitbucket, GitLab will automatically
-register your public key with Bitbucket as a deploy key for the repositories to
-be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
-translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
-`/home/git/.ssh/bitbucket_rsa` for installations from source.
-
----
-
-Below are the steps that will allow GitLab to be able to import your projects
-from Bitbucket.
-
-1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
-1. Create a new SSH key with an **empty passphrase**:
-
-    ```sh
-    sudo -u git -H ssh-keygen
-    ```
-
-    When asked to 'Enter file in which to save the key' enter:
-    `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
-    `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
-    important so make sure to get it right.
-
-    > **Warning:**
-    This key must NOT be associated with ANY existing Bitbucket accounts. If it
-    is, the import will fail with an `Access denied! Please verify you can add
-    deploy keys to this repository.` error.
-
-1. Next, you need to to configure the SSH client to use your new key. Open the
-   SSH configuration file of the `git` user:
-
-    ```
-    # For Omnibus packages
-    sudo editor /var/opt/gitlab/.ssh/config
-
-    # For installations from source
-    sudo editor /home/git/.ssh/config
-    ```
-
-1. Add a host configuration for `bitbucket.org`:
-
-    ```sh
-    Host bitbucket.org
-      IdentityFile ~/.ssh/bitbucket_rsa
-      User git
-    ```
-
-1. Save the file and exit.
-1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
-   user that GitLab will use:
-
-    ```sh
-    sudo -u git -H ssh bitbucket.org
-    ```
-
-    That step is performed because GitLab needs to connect to Bitbucket over SSH,
-    in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
-
-1.  Verify the RSA key fingerprint you'll see in the response matches the one
-    in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
-    doesn't matter):
-
-    ```sh
-    The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
-    RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
-    Are you sure you want to continue connecting (yes/no)?
-    ```
-
-1. If the fingerprint matches, type `yes` to continue connecting and have
-   `bitbucket.org` be added to your known SSH hosts. After confirming you should
-   see a permission denied message. If you see an authentication successful
-   message you have done something wrong. The key you are using has already been
-   added to a Bitbucket account and will cause the import script to fail. Ensure
-   the key you are using CANNOT authenticate with Bitbucket.
-1. Restart GitLab to allow it to find the new public key.
-
-Your GitLab server is now able to connect to Bitbucket over SSH. You should be
-able to see the "Import projects from Bitbucket" option on the New Project page
-enabled.
-
-## Acknowledgements
-
-Special thanks to the writer behind the following article:
-
-- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
+Once the above configuration is set up, you can use Bitbucket to sign into
+GitLab and [start importing your projects][bb-import].
 
 [init-oauth]: omniauth.md#initial-omniauth-configuration
+[bb-import]: ../workflow/importing/import_projects_from_bitbucket.md
+[bb-old]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-14-stable/doc/integration/bitbucket.md
 [bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
 [reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
 [restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png
index 8dbee9762d7510f9e553643c61dd52a353ebbc39..21ce82a6074e2a5b1d52331a5567634a8c5c7914 100644
Binary files a/doc/integration/img/bitbucket_oauth_settings_page.png and b/doc/integration/img/bitbucket_oauth_settings_page.png differ
diff --git a/doc/project_services/img/kubernetes_configuration.png b/doc/project_services/img/kubernetes_configuration.png
new file mode 100644
index 0000000000000000000000000000000000000000..349a2dc8456e405b129611a53aacc8af41e59d6c
Binary files /dev/null and b/doc/project_services/img/kubernetes_configuration.png differ
diff --git a/doc/project_services/img/mattermost_configuration.png b/doc/project_services/img/mattermost_configuration.png
new file mode 100644
index 0000000000000000000000000000000000000000..3c5ff5ee317ff47596210461924bdd635faeaab1
Binary files /dev/null and b/doc/project_services/img/mattermost_configuration.png differ
diff --git a/doc/project_services/kubernetes.md b/doc/project_services/kubernetes.md
new file mode 100644
index 0000000000000000000000000000000000000000..fda364b864eedc7d9e3954ba003b42ecb1d4e8fd
--- /dev/null
+++ b/doc/project_services/kubernetes.md
@@ -0,0 +1,49 @@
+# GitLab Kubernetes / OpenShift integration
+
+GitLab can be configured to interact with Kubernetes, or other systems using the
+Kubernetes API (such as OpenShift).
+
+Each project can be configured to connect to a different Kubernetes cluster, see
+the [configuration](#configuration) section.
+
+If you have a single cluster that you want to use for all your projects,
+you can pre-fill the settings page with a default template. To configure the
+template, see the [Services Templates](services-templates.md) document.
+
+## Configuration
+
+![Kubernetes configuration settings](img/kubernetes_configuration.png)
+
+The Kubernetes service takes the following arguments:
+
+1. Kubernetes namespace
+1. API URL
+1. Service token
+1. Custom CA bundle
+
+The API URL is the URL that GitLab uses to access the Kubernetes API. Kubernetes
+exposes several APIs - we want the "base" URL that is common to all of them,
+e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
+
+GitLab authenticates against Kubernetes using service tokens, which are
+scoped to a particular `namespace`. If you don't have a service token yet,
+you can follow the
+[Kubernetes documentation](http://kubernetes.io/docs/user-guide/service-accounts/)
+to create one. You can also view or create service tokens in the
+[Kubernetes dashboard](http://kubernetes.io/docs/user-guide/ui/) - visit
+`Config -> Secrets`.
+
+Fill in the service token and namespace according to the values you just got.
+If the API is using a self-signed TLS certificate, you'll also need to include
+the `ca.crt` contents as the `Custom CA bundle`.
+
+## Deployment variables
+
+The Kubernetes service exposes following
+[deployment variables](../ci/variables/README.md#deployment-variables) in the
+GitLab CI build environment:
+
+- `KUBE_URL` - equal to the API URL
+- `KUBE_TOKEN`
+- `KUBE_NAMESPACE`
+- `KUBE_CA_PEM` - only if a custom CA bundle was specified
diff --git a/doc/project_services/mattermost.md b/doc/project_services/mattermost.md
new file mode 100644
index 0000000000000000000000000000000000000000..fbc7dfeee6d0637cb01ad4fcada6af1cd675bc39
--- /dev/null
+++ b/doc/project_services/mattermost.md
@@ -0,0 +1,45 @@
+# Mattermost Notifications Service
+
+## On Mattermost
+
+To enable Mattermost integration you must create an incoming webhook integration:
+
+1. Sign in to your Mattermost instance
+1. Visit incoming webhooks, that will be something like: https://mattermost.example/your_team_name/integrations/incoming_webhooks/add
+1. Choose a display name, description and channel, those can be overridden on GitLab
+1. Save it, copy the **Webhook URL**, we'll need this later for GitLab.
+
+There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable
+it on https://mattermost.example/admin_console/integrations/custom.
+
+Display name override is not enabled by default, you need to ask your admin to enable it on that same section.
+
+## On GitLab
+
+After you set up Mattermost, it's time to set up GitLab.
+
+Go to your project's **Settings > Services > Mattermost Notifications** and you will see a
+checkbox with the following events that can be triggered:
+
+- Push
+- Issue
+- Merge request
+- Note
+- Tag push
+- Build
+- Wiki page
+
+Bellow each of these event checkboxes, you will have an input field to insert
+which Mattermost channel you want to send that event message, with `#town-square`
+being the default. The hash sign is optional.
+
+At the end, fill in your Mattermost details:
+
+| Field | Description |
+| ----- | ----------- |
+| **Webhook**  | The incoming webhooks which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo... |
+| **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
+| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
+
+
+![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/project_services/mattermost_slash_commands.md b/doc/project_services/mattermost_slash_commands.md
index 6fcbf6f1642c145eba773491767742641f0cba44..1a7c13a29b4cdf025f845eecf83fc547a6320d69 100644
--- a/doc/project_services/mattermost_slash_commands.md
+++ b/doc/project_services/mattermost_slash_commands.md
@@ -65,7 +65,7 @@ the administrator console.
 
 ### Step 3. Create a new custom slash command in Mattermost
 
-Now that you have enabled the custom slash commands in Mattermost and opened
+Now that you have enabled custom slash commands in Mattermost and opened
 the Mattermost slash commands service in GitLab, it's time to copy these values
 in a new slash command.
 
@@ -128,20 +128,16 @@ GitLab using the Mattermost commands.
 
 ## Available slash commands
 
-The available slash commands so far are:
+The available slash commands are:
 
 | Command | Description | Example |
 | ------- | ----------- | ------- |
-| `/<trigger> issue create <title>\n<description>` | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | `/trigger issue create We need to change the homepage` |
-| `/<trigger> issue show <issue-number>` | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | `/trigger issue show 42` |
-| `/<trigger> deploy <environment> to <environment>` | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | `/trigger deploy staging to production` |
+| <kbd>/&lt;trigger&gt; issue new &lt;title&gt; <kbd>⇧ Shift</kbd>+<kbd>↵ Enter</kbd> &lt;description&gt;</kbd> | Create a new issue in the project that `<trigger>` is tied to. `<description>` is optional. | <samp>/gitlab issue new We need to change the homepage</samp> |
+| <kbd>/&lt;trigger&gt; issue show &lt;issue-number&gt;</kbd> | Show the issue with ID `<issue-number>` from the project that `<trigger>` is tied to. | <samp>/gitlab issue show 42</samp> |
+| <kbd>/&lt;trigger&gt; deploy &lt;environment&gt; to &lt;environment&gt;</kbd> | Start the CI job that deploys from one environment to another, for example `staging` to `production`. CI/CD must be [properly configured][ciyaml]. | <samp>/gitlab deploy staging to production</samp> |
 
-To see a list of available commands that can interact with GitLab, type the
-trigger word followed by `help`:
-
-```
-/my-project help
-```
+To see a list of available commands to interact with GitLab, type the
+trigger word followed by <kbd>help</kbd>. Example: <samp>/gitlab help</samp>
 
 ![Mattermost bot available commands](img/mattermost_bot_available_commands.png)
 
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 890f7525b0e745da9f5d48d3e2b2583577bdbbba..0f398874b8fc6db774c9b8b8ca698bd83699ae46 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -42,11 +42,13 @@ further configuration instructions and details. Contributions are welcome.
 | [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
 | [JIRA](jira.md) | JIRA issue tracker |
 | JetBrains TeamCity CI | A continuous integration and build server |
+| [Kubernetes](kubernetes.md) | A containerized deployment service |
 | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
+| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| [Slack Notifications](slack.md) | Receive event notifications in Slack |
 | PivotalTracker | Project Management Software (Source Commits Endpoint) |
 | Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
 | [Redmine](redmine.md) | Redmine issue tracker |
-| [Slack](slack.md) | A team communication tool for the 21st century |
 
 ## Services Templates
 
diff --git a/doc/project_services/slack.md b/doc/project_services/slack.md
index 3cfe77c9f851464350afc323ea9e4773e60b0ce9..0b682b43810853a9fd1f868f4162563fa0a42b9e 100644
--- a/doc/project_services/slack.md
+++ b/doc/project_services/slack.md
@@ -1,4 +1,4 @@
-# Slack Service
+# Slack Notifications Service
 
 ## On Slack
 
@@ -15,7 +15,7 @@ Slack:
 
 After you set up Slack, it's time to set up GitLab.
 
-Go to your project's **Settings > Services > Slack** and you will see a
+Go to your project's **Settings > Services > Slack Notifications** and you will see a
 checkbox with the following events that can be triggered:
 
 - Push
diff --git a/doc/update/8.14-to-8.15.md b/doc/update/8.14-to-8.15.md
index 5556dae2551e321dd6c891bb5dddcd003c5ee14e..4eacab0c890a1d8c1e731f384ae4e29cb7d9fb57 100644
--- a/doc/update/8.14-to-8.15.md
+++ b/doc/update/8.14-to-8.15.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-15-stable-ee
 ```bash
 cd /home/git/gitlab-shell
 sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v4.0.3
+sudo -u git -H git checkout v4.1.0
 ```
 
 ### 6. Update gitlab-workhorse
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 4d24eb219769d82827ed63c963a851c8ee253e9d..f64846887219288d314ea6c4b3baea1d2dc2e947 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -319,6 +319,40 @@ Here's a sample video:
 
 ![Sample Video](img/markdown_video.mp4)
 
+### Math
+
+> If this is not rendered correctly, see
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#math
+
+It is possible to have math written with the LaTeX syntax rendered using [KaTeX][katex].
+
+Math written inside ```$``$``` will be rendered inline with the text.
+
+Math written inside triple back quotes, with the language declared as `math`, will be rendered on a separate line.
+
+Example:
+
+    This math is inline $`a^2+b^2=c^2`$.
+
+    This is on a separate line
+    ```math
+    a^2+b^2=c^2
+    ```
+
+Becomes:
+
+This math is inline $`a^2+b^2=c^2`$.
+
+This is on a separate line
+```math
+a^2+b^2=c^2
+```
+
+_Be advised that KaTeX only supports a [subset][katex-subset] of LaTeX._
+
+>**Note:**
+This also works for the asciidoctor `:stem: latexmath`. For details see the [asciidoctor user manual][asciidoctor-manual].
+
 ## Standard Markdown
 
 ### Headers
@@ -764,3 +798,6 @@ A link starting with a `/` is relative to the wiki root.
 [markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
 [rouge]: http://rouge.jneen.net/ "Rouge website"
 [redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
+[katex]: https://github.com/Khan/KaTeX "KaTeX website"
+[katex-subset]: https://github.com/Khan/KaTeX/wiki/Function-Support-in-KaTeX "Macros supported by KaTeX"
+[asciidoctor-manual]: http://asciidoctor.org/docs/user-manual/#activating-stem-support "Asciidoctor user manual"
\ No newline at end of file
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png
deleted file mode 100644
index df55a081803b646c89a09205c4fd16bf141788bd..0000000000000000000000000000000000000000
Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_grant_access.png and /dev/null differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png
deleted file mode 100644
index 5253889d251b6c70a375de4f7a6fef494804af11..0000000000000000000000000000000000000000
Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_new_project.png and /dev/null differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png
deleted file mode 100644
index ffa87ce5b2eeb8fa3eddc740f70b3f8e3a3fbaa0..0000000000000000000000000000000000000000
Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_bitbucket.png and /dev/null differ
diff --git a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png b/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png
deleted file mode 100644
index 1a5661de75d556dea016fe86a67ca043a7c2cd93..0000000000000000000000000000000000000000
Binary files a/doc/workflow/importing/bitbucket_importer/bitbucket_import_select_project.png and /dev/null differ
diff --git a/doc/workflow/importing/img/bitbucket_import_grant_access.png b/doc/workflow/importing/img/bitbucket_import_grant_access.png
new file mode 100644
index 0000000000000000000000000000000000000000..429904e621d26b0628c32d8e7145e779f77e2c4a
Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_grant_access.png differ
diff --git a/doc/workflow/importing/img/bitbucket_import_new_project.png b/doc/workflow/importing/img/bitbucket_import_new_project.png
new file mode 100644
index 0000000000000000000000000000000000000000..8ed528c2f09bb5d89d7f9c8e0dc34999e25f6906
Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_new_project.png differ
diff --git a/doc/workflow/importing/img/bitbucket_import_select_project.png b/doc/workflow/importing/img/bitbucket_import_select_project.png
new file mode 100644
index 0000000000000000000000000000000000000000..1bca6166ec8e3f902bfa9f128e31fd84ac1324bb
Binary files /dev/null and b/doc/workflow/importing/img/bitbucket_import_select_project.png differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png b/doc/workflow/importing/img/import_projects_from_github_new_project_page.png
deleted file mode 100644
index b23ade4480c4ee2ddd1a9f81cd03a6ba735a8e53..0000000000000000000000000000000000000000
Binary files a/doc/workflow/importing/img/import_projects_from_github_new_project_page.png and /dev/null differ
diff --git a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png
index f50d92669916e0a250083ee0bf792fa16fd9fe50..1ccb38a815e486a44a06d249b0f332982068fc3a 100644
Binary files a/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png and b/doc/workflow/importing/img/import_projects_from_github_select_auth_method.png differ
diff --git a/doc/workflow/importing/img/import_projects_from_new_project_page.png b/doc/workflow/importing/img/import_projects_from_new_project_page.png
new file mode 100644
index 0000000000000000000000000000000000000000..97ca30b20874d405b64fb181a54c5395da203542
Binary files /dev/null and b/doc/workflow/importing/img/import_projects_from_new_project_page.png differ
diff --git a/doc/workflow/importing/import_projects_from_bitbucket.md b/doc/workflow/importing/import_projects_from_bitbucket.md
index 520c42162957bf9e1981a53dca79c03ff2d7a9fe..b6d47e5afa2c706919972d055afe5c3020567921 100644
--- a/doc/workflow/importing/import_projects_from_bitbucket.md
+++ b/doc/workflow/importing/import_projects_from_bitbucket.md
@@ -1,26 +1,61 @@
 # Import your project from Bitbucket to GitLab
 
-It takes just a few steps to import your existing Bitbucket projects to GitLab. But keep in mind that it is possible only if Bitbucket support is enabled on your GitLab instance. You can read more about Bitbucket support [here](../../integration/bitbucket.md).
+Import your projects from Bitbucket to GitLab with minimal effort.
 
-* Sign in to GitLab.com and go to your dashboard
+## Overview
 
-* Click on "New project"
+>**Note:**
+The [Bitbucket integration][bb-import] must be first enabled in order to be
+able to import your projects from Bitbucket. Ask your GitLab administrator
+to enable this if not already.
 
-![New project in GitLab](bitbucket_importer/bitbucket_import_new_project.png)
+- At its current state, the Bitbucket importer can import:
+  - the repository description (GitLab 7.7+)
+  - the Git repository data (GitLab 7.7+)
+  - the issues (GitLab 7.7+)
+  - the issue comments (GitLab 8.15+)
+  - the pull requests (GitLab 8.4+)
+  - the pull request comments (GitLab 8.15+)
+  - the milestones (GitLab 8.15+)
+- References to pull requests and issues are preserved (GitLab 8.7+)
+- Repository public access is retained. If a repository is private in Bitbucket
+  it will be created as private in GitLab as well.
 
-* Click on the "Bitbucket" button
 
-![Bitbucket](bitbucket_importer/bitbucket_import_select_bitbucket.png)
+## How it works
 
-* Grant GitLab access to your Bitbucket account
+When issues/pull requests are being imported, the Bitbucket importer tries to find
+the Bitbucket author/assignee in GitLab's database using the Bitbucket ID. For this
+to work, the Bitbucket author/assignee should have signed in beforehand in GitLab
+and [**associated their Bitbucket account**][social sign-in]. If the user is not
+found in GitLab's database, the project creator (most of the times the current
+user that started the import process) is set as the author, but a reference on
+the issue about the original Bitbucket author is kept.
 
-![Grant access](bitbucket_importer/bitbucket_import_grant_access.png)
+The importer will create any new namespaces (groups) if they don't exist or in
+the case the namespace is taken, the repository will be imported under the user's
+namespace that started the import process.
 
-* Click on the projects that you'd like to import or "Import all projects"
+## Importing your Bitbucket repositories
 
-![Import projects](bitbucket_importer/bitbucket_import_select_project.png)
+1. Sign in to GitLab and go to your dashboard.
+1. Click on **New project**.
 
-A new GitLab project will be created with your imported data.
+    ![New project in GitLab](img/bitbucket_import_new_project.png)
 
-### Note
-Milestones and wiki pages are not imported from Bitbucket.
+1. Click on the "Bitbucket" button
+
+    ![Bitbucket](img/import_projects_from_new_project_page.png)
+
+1. Grant GitLab access to your Bitbucket account
+
+    ![Grant access](img/bitbucket_import_grant_access.png)
+
+1. Click on the projects that you'd like to import or **Import all projects**.
+   You can also select the namespace under which each project will be
+   imported.
+
+    ![Import projects](img/bitbucket_import_select_project.png)
+
+[bb-import]: ../../integration/bitbucket.md
+[social sign-in]: ../../user/profile/account/social_sign_in.md
diff --git a/doc/workflow/importing/import_projects_from_github.md b/doc/workflow/importing/import_projects_from_github.md
index c36dfdb78ecdcbd3996b7c2812a6f4f55c586dd3..b3660aa8030762fffb166789c50942172a133033 100644
--- a/doc/workflow/importing/import_projects_from_github.md
+++ b/doc/workflow/importing/import_projects_from_github.md
@@ -40,7 +40,7 @@ namespace that started the import process.
 
 The importer page is visible when you create a new project.
 
-![New project page on GitLab](img/import_projects_from_github_new_project_page.png)
+![New project page on GitLab](img/import_projects_from_new_project_page.png)
 
 Click on the **GitHub** link and the import authorization process will start.
 There are two ways to authorize access to your GitHub repositories:
diff --git a/features/admin/active_tab.feature b/features/admin/active_tab.feature
deleted file mode 100644
index f5bb06dea7d76f8c9ca42edd220b6428fbdf1510..0000000000000000000000000000000000000000
--- a/features/admin/active_tab.feature
+++ /dev/null
@@ -1,54 +0,0 @@
-@admin
-Feature: Admin Active Tab
-  Background:
-    Given I sign in as an admin
-
-  Scenario: On Admin Home
-    Given I visit admin page
-    Then the active main tab should be Overview
-    And no other main tabs should be active
-
-  Scenario: On Admin Projects
-    Given I visit admin projects page
-    Then the active main tab should be Overview
-    And the active sub tab should be Projects
-    And no other main tabs should be active
-    And no other sub tabs should be active
-
-  Scenario: On Admin Groups
-    Given I visit admin groups page
-    Then the active main tab should be Overview
-    And the active sub tab should be Groups
-    And no other main tabs should be active
-    And no other sub tabs should be active
-
-  Scenario: On Admin Users
-    Given I visit admin users page
-    Then the active main tab should be Overview
-    And the active sub tab should be Users
-    And no other main tabs should be active
-    And no other sub tabs should be active
-
-  Scenario: On Admin Logs
-    Given I visit admin logs page
-    Then the active main tab should be Monitoring
-    And the active sub tab should be Logs
-    And no other main tabs should be active
-    And no other sub tabs should be active
-
-  Scenario: On Admin Messages
-    Given I visit admin messages page
-    Then the active main tab should be Messages
-    And no other main tabs should be active
-
-  Scenario: On Admin Hooks
-    Given I visit admin hooks page
-    Then the active main tab should be Hooks
-    And no other main tabs should be active
-
-  Scenario: On Admin Resque
-    Given I visit admin Resque page
-    Then the active main tab should be Monitoring
-    And the active sub tab should be Resque
-    And no other main tabs should be active
-    And no other sub tabs should be active
diff --git a/features/admin/applications.feature b/features/admin/applications.feature
deleted file mode 100644
index 2a00e1666c01239e7c44dc1bd40e2a0344124955..0000000000000000000000000000000000000000
--- a/features/admin/applications.feature
+++ /dev/null
@@ -1,18 +0,0 @@
-@admin
-Feature: Admin Applications
-  Background:
-    Given I sign in as an admin
-    And I visit applications page
-    
-  Scenario: I can manage application
-    Then I click on new application button
-    And I should see application form
-    Then I fill application form out and submit
-    And I see application
-    Then I click edit
-    And I see edit application form
-    Then I change name of application and submit
-    And I see that application was changed
-    Then I visit applications page
-    And I click to remove application
-    Then I see that application is removed
\ No newline at end of file
diff --git a/features/admin/deploy_keys.feature b/features/admin/deploy_keys.feature
deleted file mode 100644
index 33439cd1e85d9678ef4b485a3ee400853867ca11..0000000000000000000000000000000000000000
--- a/features/admin/deploy_keys.feature
+++ /dev/null
@@ -1,16 +0,0 @@
-@admin
-Feature: Admin Deploy Keys
-  Background:
-    Given I sign in as an admin
-    And there are public deploy keys in system
-
-  Scenario: Deploy Keys list
-    When I visit admin deploy keys page
-    Then I should see all public deploy keys
-
-  Scenario: Deploy Keys new
-    When I visit admin deploy keys page
-    And I click 'New Deploy Key'
-    And I submit new deploy key
-    Then I should be on admin deploy keys page
-    And I should see newly created deploy key
diff --git a/features/steps/admin/active_tab.rb b/features/steps/admin/active_tab.rb
deleted file mode 100644
index 9b1689a8198212d1a00e40495f74d7b06e40ba58..0000000000000000000000000000000000000000
--- a/features/steps/admin/active_tab.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-class Spinach::Features::AdminActiveTab < Spinach::FeatureSteps
-  include SharedAuthentication
-  include SharedPaths
-  include SharedActiveTab
-
-  step 'the active main tab should be Overview' do
-    ensure_active_main_tab('Overview')
-  end
-
-  step 'the active sub tab should be Projects' do
-    ensure_active_sub_tab('Projects')
-  end
-
-  step 'the active sub tab should be Groups' do
-    ensure_active_sub_tab('Groups')
-  end
-
-  step 'the active sub tab should be Users' do
-    ensure_active_sub_tab('Users')
-  end
-
-  step 'the active main tab should be Hooks' do
-    ensure_active_main_tab('Hooks')
-  end
-
-  step 'the active main tab should be Monitoring' do
-    ensure_active_main_tab('Monitoring')
-  end
-
-  step 'the active sub tab should be Resque' do
-    ensure_active_sub_tab('Background Jobs')
-  end
-
-  step 'the active sub tab should be Logs' do
-    ensure_active_sub_tab('Logs')
-  end
-
-  step 'the active main tab should be Messages' do
-    ensure_active_main_tab('Messages')
-  end
-end
diff --git a/features/steps/admin/applications.rb b/features/steps/admin/applications.rb
deleted file mode 100644
index 7c12cb969211bac8776bc3bae7c88e936cd0b356..0000000000000000000000000000000000000000
--- a/features/steps/admin/applications.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-class Spinach::Features::AdminApplications < Spinach::FeatureSteps
-  include SharedAuthentication
-  include SharedPaths
-  include SharedAdmin
-
-  step 'I click on new application button' do
-    click_on 'New Application'
-  end
-
-  step 'I should see application form' do
-    expect(page).to have_content "New application"
-  end
-
-  step 'I fill application form out and submit' do
-    fill_in :doorkeeper_application_name, with: 'test'
-    fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
-    click_on "Submit"
-  end
-
-  step 'I see application' do
-    expect(page).to have_content "Application: test"
-    expect(page).to have_content "Application Id"
-    expect(page).to have_content "Secret"
-  end
-
-  step 'I click edit' do
-    click_on "Edit"
-  end
-
-  step 'I see edit application form' do
-    expect(page).to have_content "Edit application"
-  end
-
-  step 'I change name of application and submit' do
-    expect(page).to have_content "Edit application"
-    fill_in :doorkeeper_application_name, with: 'test_changed'
-    click_on "Submit"
-  end
-
-  step 'I see that application was changed' do
-    expect(page).to have_content "test_changed"
-    expect(page).to have_content "Application Id"
-    expect(page).to have_content "Secret"
-  end
-
-  step 'I click to remove application' do
-    page.within '.oauth-applications' do
-      click_on "Destroy"
-    end
-  end
-
-  step "I see that application is removed" do
-    expect(page.find(".oauth-applications")).not_to have_content "test_changed"
-  end
-end
diff --git a/features/steps/admin/deploy_keys.rb b/features/steps/admin/deploy_keys.rb
deleted file mode 100644
index 56787eeb6b3b92d7172003859930665fd90a81f0..0000000000000000000000000000000000000000
--- a/features/steps/admin/deploy_keys.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-class Spinach::Features::AdminDeployKeys < Spinach::FeatureSteps
-  include SharedAuthentication
-  include SharedPaths
-  include SharedAdmin
-
-  step 'there are public deploy keys in system' do
-    create(:deploy_key, public: true)
-    create(:another_deploy_key, public: true)
-  end
-
-  step 'I should see all public deploy keys' do
-    DeployKey.are_public.each do |p|
-      expect(page).to have_content p.title
-    end
-  end
-
-  step 'I visit admin deploy key page' do
-    visit admin_deploy_key_path(deploy_key)
-  end
-
-  step 'I visit admin deploy keys page' do
-    visit admin_deploy_keys_path
-  end
-
-  step 'I click \'New Deploy Key\'' do
-    click_link 'New Deploy Key'
-  end
-
-  step 'I submit new deploy key' do
-    fill_in "deploy_key_title", with: "laptop"
-    fill_in "deploy_key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
-    click_button "Create"
-  end
-
-  step 'I should be on admin deploy keys page' do
-    expect(current_path).to eq admin_deploy_keys_path
-  end
-
-  step 'I should see newly created deploy key' do
-    expect(page).to have_content(deploy_key.title)
-  end
-
-  def deploy_key
-    @deploy_key ||= DeployKey.are_public.first
-  end
-end
diff --git a/features/steps/group/milestones.rb b/features/steps/group/milestones.rb
index f5fddab357df5c64d1d8db5f4a88bd33f59e7368..c1d1eca9116e1076e3e360490587c529fc320ae2 100644
--- a/features/steps/group/milestones.rb
+++ b/features/steps/group/milestones.rb
@@ -131,5 +131,7 @@ class Spinach::Features::GroupMilestones < Spinach::FeatureSteps
       issue.labels << project.labels.find_by(title: 'bug')
       issue.labels << project.labels.find_by(title: 'feature')
     end
+
+    current_user.refresh_authorized_projects
   end
 end
diff --git a/features/steps/shared/authentication.rb b/features/steps/shared/authentication.rb
index 735e0ef6108a6945ba466bc04d73378f7955d5e8..5c3e724746b069db1bc79bf401820e1f9d18f665 100644
--- a/features/steps/shared/authentication.rb
+++ b/features/steps/shared/authentication.rb
@@ -33,6 +33,6 @@ module SharedAuthentication
   end
 
   def current_user
-    @user || User.first
+    @user || User.reorder(nil).first
   end
 end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index d36eff5cf16d647bdec52741fcc35b3c07e37538..a78d0a775ba91f1bad7d9662f8c7c5104767128f 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -168,7 +168,7 @@ module SharedPaths
   end
 
   step 'I visit admin projects page' do
-    visit admin_namespaces_projects_path
+    visit admin_projects_path
   end
 
   step 'I visit admin users page' do
@@ -207,10 +207,6 @@ module SharedPaths
     visit admin_spam_logs_path
   end
 
-  step 'I visit applications page' do
-    visit admin_applications_path
-  end
-
   # ----------------------------------------
   # Generic Project
   # ----------------------------------------
diff --git a/lib/api/api.rb b/lib/api/api.rb
index cec2702e44d91fe71f39dee9c3177914c75895c9..9d5adffd8f4510864137aec6129edbfd0e129499 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,6 +3,8 @@ module API
     include APIGuard
     version 'v3', using: :path
 
+    before { allow_access_with_scope :api }
+
     rescue_from Gitlab::Access::AccessDeniedError do
       rack_response({ 'message' => '403 Forbidden' }.to_json, 403)
     end
diff --git a/lib/api/api_guard.rb b/lib/api/api_guard.rb
index 8cc7a26f1fa71249e79d28f71308a5fdc097a251..df6db140d0e724e6292bbcce49a745054da8ea54 100644
--- a/lib/api/api_guard.rb
+++ b/lib/api/api_guard.rb
@@ -6,6 +6,9 @@ module API
   module APIGuard
     extend ActiveSupport::Concern
 
+    PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
+    PRIVATE_TOKEN_PARAM = :private_token
+
     included do |base|
       # OAuth2 Resource Server Authentication
       use Rack::OAuth2::Server::Resource::Bearer, 'The API' do |request|
@@ -44,27 +47,60 @@ module API
         access_token = find_access_token
         return nil unless access_token
 
-        case validate_access_token(access_token, scopes)
-        when Oauth2::AccessTokenValidationService::INSUFFICIENT_SCOPE
+        case AccessTokenValidationService.new(access_token).validate(scopes: scopes)
+        when AccessTokenValidationService::INSUFFICIENT_SCOPE
           raise InsufficientScopeError.new(scopes)
 
-        when Oauth2::AccessTokenValidationService::EXPIRED
+        when AccessTokenValidationService::EXPIRED
           raise ExpiredError
 
-        when Oauth2::AccessTokenValidationService::REVOKED
+        when AccessTokenValidationService::REVOKED
           raise RevokedError
 
-        when Oauth2::AccessTokenValidationService::VALID
+        when AccessTokenValidationService::VALID
           @current_user = User.find(access_token.resource_owner_id)
         end
       end
 
+      def find_user_by_private_token(scopes: [])
+        token_string = (params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]).to_s
+
+        return nil unless token_string.present?
+
+        find_user_by_authentication_token(token_string) || find_user_by_personal_access_token(token_string, scopes)
+      end
+
       def current_user
         @current_user
       end
 
+      # Set the authorization scope(s) allowed for the current request.
+      #
+      # Note: A call to this method adds to any previous scopes in place. This is done because
+      # `Grape` callbacks run from the outside-in: the top-level callback (API::API) runs first, then
+      # the next-level callback (API::API::Users, for example) runs. All these scopes are valid for the
+      # given endpoint (GET `/api/users` is accessible by the `api` and `read_user` scopes), and so they
+      # need to be stored.
+      def allow_access_with_scope(*scopes)
+        @scopes ||= []
+        @scopes.concat(scopes.map(&:to_s))
+      end
+
       private
 
+      def find_user_by_authentication_token(token_string)
+        User.find_by_authentication_token(token_string)
+      end
+
+      def find_user_by_personal_access_token(token_string, scopes)
+        access_token = PersonalAccessToken.active.find_by_token(token_string)
+        return unless access_token
+
+        if AccessTokenValidationService.new(access_token).include_any_scope?(scopes)
+          User.find(access_token.user_id)
+        end
+      end
+
       def find_access_token
         @access_token ||= Doorkeeper.authenticate(doorkeeper_request, Doorkeeper.configuration.access_token_methods)
       end
@@ -72,10 +108,6 @@ module API
       def doorkeeper_request
         @doorkeeper_request ||= ActionDispatch::Request.new(env)
       end
-
-      def validate_access_token(access_token, scopes)
-        Oauth2::AccessTokenValidationService.validate(access_token, scopes: scopes)
-      end
     end
 
     module ClassMethods
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 01c0f5072ba40dc1b29fc4b5996989e5a9ecdc03..dfbb3ab86dd23c00f55c84a9e978290862068808 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -629,7 +629,7 @@ module API
     end
 
     class EnvironmentBasic < Grape::Entity
-      expose :id, :name, :external_url
+      expose :id, :name, :slug, :external_url
     end
 
     class Environment < EnvironmentBasic
diff --git a/lib/api/environments.rb b/lib/api/environments.rb
index 80bbd9bb6e4d4fb963ba61f3186cb4edf439581d..1a7e68f0528f3d15f628d48fb3a47c824ce3c2e5 100644
--- a/lib/api/environments.rb
+++ b/lib/api/environments.rb
@@ -1,6 +1,7 @@
 module API
   # Environments RESTfull API endpoints
   class Environments < Grape::API
+    include ::API::Helpers::CustomValidators
     include PaginationParams
 
     before { authenticate! }
@@ -29,6 +30,7 @@ module API
       params do
         requires :name,           type: String,   desc: 'The name of the environment to be created'
         optional :external_url,   type: String,   desc: 'URL on which this deployment is viewable'
+        optional :slug, absence: { message: "is automatically generated and cannot be changed" }
       end
       post ':id/environments' do
         authorize! :create_environment, user_project
@@ -50,6 +52,7 @@ module API
         requires :environment_id, type: Integer,  desc: 'The environment ID'
         optional :name,           type: String,   desc: 'The new environment name'
         optional :external_url,   type: String,   desc: 'The new URL on which this deployment is viewable'
+        optional :slug, absence: { message: "is automatically generated and cannot be changed" }
       end
       put ':id/environments/:environment_id' do
         authorize! :update_environment, user_project
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 746849ef4c01d75bd12adfc9e9554f733154ff62..4be659fc20bd50df9a9ba4ff8b071b2e7bfafd25 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -2,8 +2,6 @@ module API
   module Helpers
     include Gitlab::Utils
 
-    PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
-    PRIVATE_TOKEN_PARAM = :private_token
     SUDO_HEADER = "HTTP_SUDO"
     SUDO_PARAM = :sudo
 
@@ -308,7 +306,7 @@ module API
     private
 
     def private_token
-      params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
+      params[APIGuard::PRIVATE_TOKEN_PARAM] || env[APIGuard::PRIVATE_TOKEN_HEADER]
     end
 
     def warden
@@ -323,18 +321,11 @@ module API
       warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
     end
 
-    def find_user_by_private_token
-      token = private_token
-      return nil unless token.present?
-
-      User.find_by_authentication_token(token) || User.find_by_personal_access_token(token)
-    end
-
     def initial_current_user
       return @initial_current_user if defined?(@initial_current_user)
 
-      @initial_current_user ||= find_user_by_private_token
-      @initial_current_user ||= doorkeeper_guard
+      @initial_current_user ||= find_user_by_private_token(scopes: @scopes)
+      @initial_current_user ||= doorkeeper_guard(scopes: @scopes)
       @initial_current_user ||= find_user_from_warden
 
       unless @initial_current_user && Gitlab::UserAccess.new(@initial_current_user).allowed?
diff --git a/lib/api/helpers/custom_validators.rb b/lib/api/helpers/custom_validators.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0a8f3073a50e1fc191e6eeee21ea9314d2b93f32
--- /dev/null
+++ b/lib/api/helpers/custom_validators.rb
@@ -0,0 +1,14 @@
+module API
+  module Helpers
+    module CustomValidators
+      class Absence < Grape::Validations::Base
+        def validate_param!(attr_name, params)
+          return if params.respond_to?(:key?) && !params.key?(attr_name)
+          raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name)], message: message(:absence)
+        end
+      end
+    end
+  end
+end
+
+Grape::Validations.register_validator(:absence, ::API::Helpers::CustomValidators::Absence)
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index eb223c1101db27c0d914a552cc4f5ed0fc880249..e8975eb57e0f05e98d2b611cd118fd9c1732802f 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -52,6 +52,14 @@ module API
           :push_code
         ]
       end
+
+      def parse_allowed_environment_variables
+        return if params[:env].blank?
+
+        JSON.parse(params[:env])
+
+      rescue JSON::ParserError
+      end
     end
   end
 end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 7087ce114014f325dfa23c66897fa096c80942d7..db2d18f935d0423282aa7233622b89cefdfef3ea 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -32,7 +32,11 @@ module API
           if wiki?
             Gitlab::GitAccessWiki.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
           else
-            Gitlab::GitAccess.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
+            Gitlab::GitAccess.new(actor,
+                                  project,
+                                  protocol,
+                                  authentication_abilities: ssh_authentication_abilities,
+                                  env: parse_allowed_environment_variables)
           end
 
         access_status = access.check(params[:action], params[:changes])
diff --git a/lib/api/services.rb b/lib/api/services.rb
index fde2e2746f1bb1cc12fc1982d3de9049e447c5ba..59232c84c24dd1b32eb93af4dab2fc5900e05634 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -351,6 +351,34 @@ module API
           desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
         }
       ],
+
+      'kubernetes' => [
+        {
+          required: true,
+          name: :namespace,
+          type: String,
+          desc: 'The Kubernetes namespace to use'
+        },
+        {
+          required: true,
+          name: :api_url,
+          type: String,
+          desc: 'The URL to the Kubernetes cluster API, e.g., https://kubernetes.example.com'
+        },
+        {
+          required: true,
+          name: :token,
+          type: String,
+          desc: 'The service token to authenticate against the Kubernetes cluster with'
+        },
+        {
+          required: false,
+          name: :ca_pem,
+          type: String,
+          desc: 'A custom certificate authority bundle to verify the Kubernetes cluster with (PEM format)'
+        },
+      ],
+
       'mattermost-slash-commands' => [
         {
           required: true,
@@ -445,7 +473,7 @@ module API
           desc: 'The description of the tracker'
         }
       ],
-      'slack' => [
+      'slack-notification' => [
         {
           required: true,
           name: :webhook,
@@ -465,6 +493,14 @@ module API
           desc: 'The channel name'
         }
       ],
+      'mattermost-notification' => [
+        {
+          required: true,
+          name: :webhook,
+          type: String,
+          desc: 'The Mattermost webhook. e.g. http://mattermost_host/hooks/...'
+        }
+      ],
       'teamcity' => [
         {
           required: true,
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 8a53d9c0095d5355224bb19e40ce174b734b49f4..e23f99256a59e2fea3a74a1ae4c61438bab9ccce 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -8,6 +8,10 @@ module API
       gitlab_ci_ymls: {
         klass: Gitlab::Template::GitlabCiYmlTemplate,
         gitlab_version: 8.9
+      },
+      dockerfiles: {
+        klass: Gitlab::Template::DockerfileTemplate,
+        gitlab_version: 8.15
       }
     }.freeze
     PROJECT_TEMPLATE_REGEX =
@@ -51,7 +55,7 @@ module API
       end
       params do
         optional :popular, type: Boolean, desc: 'If passed, returns only popular licenses'
-      end 
+      end
       get route do
         options = {
           featured: declared(params).popular.present? ? true : nil
@@ -69,7 +73,7 @@ module API
       end
       params do
         requires :name, type: String, desc: 'The name of the template'
-      end 
+      end
       get route, requirements: { name: /[\w\.-]+/ } do
         not_found!('License') unless Licensee::License.find(declared(params).name)
 
@@ -78,7 +82,7 @@ module API
         present template, with: Entities::RepoLicense
       end
     end
-      
+
     GLOBAL_TEMPLATE_TYPES.each do |template_type, properties|
       klass = properties[:klass]
       gitlab_version = properties[:gitlab_version]
@@ -104,7 +108,7 @@ module API
         end
         params do
           requires :name, type: String, desc: 'The name of the template'
-        end 
+        end
         get route do
           new_template = klass.find(declared(params).name)
 
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c7db2d7101715d324dcb8ba82c71a32e5a9dd9b0..0842c3874c5a4a981ac57f97125e064c0d741745 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -2,7 +2,10 @@ module API
   class Users < Grape::API
     include PaginationParams
 
-    before { authenticate! }
+    before do
+      allow_access_with_scope :read_user if request.get?
+      authenticate!
+    end
 
     resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
       helpers do
diff --git a/lib/banzai/filter/math_filter.rb b/lib/banzai/filter/math_filter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..cb037f89337c860515dfa358c44317772078676e
--- /dev/null
+++ b/lib/banzai/filter/math_filter.rb
@@ -0,0 +1,51 @@
+require 'uri'
+
+module Banzai
+  module Filter
+    # HTML filter that adds class="code math" and removes the dollar sign in $`2+2`$.
+    #
+    class MathFilter < HTML::Pipeline::Filter
+      # This picks out <code>...</code>.
+      INLINE_MATH = 'descendant-or-self::code'.freeze
+
+      # Pick out a code block which is declared math
+      DISPLAY_MATH = "descendant-or-self::pre[contains(@class, 'math') and contains(@class, 'code')]".freeze
+
+      # Attribute indicating inline or display math.
+      STYLE_ATTRIBUTE = 'data-math-style'.freeze
+
+      # Class used for tagging elements that should be rendered
+      TAG_CLASS = 'js-render-math'.freeze
+
+      INLINE_CLASSES = "code math #{TAG_CLASS}".freeze
+
+      DOLLAR_SIGN = '$'.freeze
+
+      def call
+        doc.xpath(INLINE_MATH).each do |code|
+          closing = code.next
+          opening = code.previous
+
+          # We need a sibling before and after.
+          # They should end and start with $ respectively.
+          if closing && opening &&
+              closing.content.first == DOLLAR_SIGN &&
+              opening.content.last == DOLLAR_SIGN
+
+            code[:class] = INLINE_CLASSES
+            code[STYLE_ATTRIBUTE] = 'inline'
+            closing.content = closing.content[1..-1]
+            opening.content = opening.content[0..-2]
+          end
+        end
+
+        doc.xpath(DISPLAY_MATH).each do |el|
+          el[STYLE_ATTRIBUTE] = 'display'
+          el[:class] += " #{TAG_CLASS}"
+        end
+
+        doc
+      end
+    end
+  end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index 5da2d0b008c860ad9c27c49506aabcc05ce2c6b4..5a1f873496cec2c8659f550844a687d05c95d408 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -6,6 +6,7 @@ module Banzai
           Filter::SyntaxHighlightFilter,
           Filter::SanitizationFilter,
 
+          Filter::MathFilter,
           Filter::UploadLinkFilter,
           Filter::VideoLinkFilter,
           Filter::ImageLinkFilter,
diff --git a/lib/bitbucket/client.rb b/lib/bitbucket/client.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f8ee7e0f9aed5bd884f56ce1029781559c9f44f8
--- /dev/null
+++ b/lib/bitbucket/client.rb
@@ -0,0 +1,58 @@
+module Bitbucket
+  class Client
+    attr_reader :connection
+
+    def initialize(options = {})
+      @connection = Connection.new(options)
+    end
+
+    def issues(repo)
+      path = "/repositories/#{repo}/issues"
+      get_collection(path, :issue)
+    end
+
+    def issue_comments(repo, issue_id)
+      path = "/repositories/#{repo}/issues/#{issue_id}/comments"
+      get_collection(path, :comment)
+    end
+
+    def pull_requests(repo)
+      path = "/repositories/#{repo}/pullrequests?state=ALL"
+      get_collection(path, :pull_request)
+    end
+
+    def pull_request_comments(repo, pull_request)
+      path = "/repositories/#{repo}/pullrequests/#{pull_request}/comments"
+      get_collection(path, :pull_request_comment)
+    end
+
+    def pull_request_diff(repo, pull_request)
+      path = "/repositories/#{repo}/pullrequests/#{pull_request}/diff"
+      connection.get(path)
+    end
+
+    def repo(name)
+      parsed_response = connection.get("/repositories/#{name}")
+      Representation::Repo.new(parsed_response)
+    end
+
+    def repos
+      path = "/repositories?role=member"
+      get_collection(path, :repo)
+    end
+
+    def user
+      @user ||= begin
+        parsed_response = connection.get('/user')
+        Representation::User.new(parsed_response)
+      end
+    end
+
+    private
+
+    def get_collection(path, type)
+      paginator = Paginator.new(connection, path, type)
+      Collection.new(paginator)
+    end
+  end
+end
diff --git a/lib/bitbucket/collection.rb b/lib/bitbucket/collection.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3a9379ff680b6e89531d72f1104167a7f40b4a41
--- /dev/null
+++ b/lib/bitbucket/collection.rb
@@ -0,0 +1,21 @@
+module Bitbucket
+  class Collection < Enumerator
+    def initialize(paginator)
+      super() do |yielder|
+        loop do
+          paginator.items.each { |item| yielder << item }
+        end
+      end
+
+      lazy
+    end
+
+    def method_missing(method, *args)
+      return super unless self.respond_to?(method)
+
+      self.send(method, *args) do |item|
+        block_given? ? yield(item) : item
+      end
+    end
+  end
+end
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7e55cf4deabfc2b338fdd12a76cc6e260b41bb68
--- /dev/null
+++ b/lib/bitbucket/connection.rb
@@ -0,0 +1,69 @@
+module Bitbucket
+  class Connection
+    DEFAULT_API_VERSION = '2.0'
+    DEFAULT_BASE_URI    = 'https://api.bitbucket.org/'
+    DEFAULT_QUERY       = {}
+
+    attr_reader :expires_at, :expires_in, :refresh_token, :token
+
+    def initialize(options = {})
+      @api_version   = options.fetch(:api_version, DEFAULT_API_VERSION)
+      @base_uri      = options.fetch(:base_uri, DEFAULT_BASE_URI)
+      @default_query = options.fetch(:query, DEFAULT_QUERY)
+
+      @token         = options[:token]
+      @expires_at    = options[:expires_at]
+      @expires_in    = options[:expires_in]
+      @refresh_token = options[:refresh_token]
+    end
+
+    def get(path, extra_query = {})
+      refresh! if expired?
+
+      response = connection.get(build_url(path), params: @default_query.merge(extra_query))
+      response.parsed
+    end
+
+    def expired?
+      connection.expired?
+    end
+
+    def refresh!
+      response = connection.refresh!
+
+      @token         = response.token
+      @expires_at    = response.expires_at
+      @expires_in    = response.expires_in
+      @refresh_token = response.refresh_token
+      @connection    = nil
+    end
+
+    private
+
+    def client
+      @client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
+    end
+
+    def connection
+      @connection ||= OAuth2::AccessToken.new(client, @token, refresh_token: @refresh_token, expires_at: @expires_at, expires_in: @expires_in)
+    end
+
+    def build_url(path)
+      return path if path.starts_with?(root_url)
+
+      "#{root_url}#{path}"
+    end
+
+    def root_url
+      @root_url ||= "#{@base_uri}#{@api_version}"
+    end
+
+    def provider
+      Gitlab::OAuth::Provider.config_for('bitbucket')
+    end
+
+    def options
+      OmniAuth::Strategies::Bitbucket.default_options[:client_options].deep_symbolize_keys
+    end
+  end
+end
diff --git a/lib/bitbucket/error/unauthorized.rb b/lib/bitbucket/error/unauthorized.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5e2eb57bb0ee37dad8158dcaf6bcf001e5e0ebf4
--- /dev/null
+++ b/lib/bitbucket/error/unauthorized.rb
@@ -0,0 +1,6 @@
+module Bitbucket
+  module Error
+    class Unauthorized < StandardError
+    end
+  end
+end
diff --git a/lib/bitbucket/page.rb b/lib/bitbucket/page.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2b0a3fe7b1a88d136589c6970e022f4108cc9464
--- /dev/null
+++ b/lib/bitbucket/page.rb
@@ -0,0 +1,34 @@
+module Bitbucket
+  class Page
+    attr_reader :attrs, :items
+
+    def initialize(raw, type)
+      @attrs = parse_attrs(raw)
+      @items = parse_values(raw, representation_class(type))
+    end
+
+    def next?
+      attrs.fetch(:next, false)
+    end
+
+    def next
+      attrs.fetch(:next)
+    end
+
+    private
+
+    def parse_attrs(raw)
+      raw.slice(*%w(size page pagelen next previous)).symbolize_keys
+    end
+
+    def parse_values(raw, bitbucket_rep_class)
+      return [] unless raw['values'] && raw['values'].is_a?(Array)
+
+      bitbucket_rep_class.decorate(raw['values'])
+    end
+
+    def representation_class(type)
+      Bitbucket::Representation.const_get(type.to_s.camelize)
+    end
+  end
+end
diff --git a/lib/bitbucket/paginator.rb b/lib/bitbucket/paginator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..135d0d556743f658cfa54a05126cfe5f606e5f5c
--- /dev/null
+++ b/lib/bitbucket/paginator.rb
@@ -0,0 +1,36 @@
+module Bitbucket
+  class Paginator
+    PAGE_LENGTH = 50 # The minimum length is 10 and the maximum is 100.
+
+    def initialize(connection, url, type)
+      @connection = connection
+      @type = type
+      @url = url
+      @page = nil
+    end
+
+    def items
+      raise StopIteration unless has_next_page?
+
+      @page = fetch_next_page
+      @page.items
+    end
+
+    private
+
+    attr_reader :connection, :page, :url, :type
+
+    def has_next_page?
+      page.nil? || page.next?
+    end
+
+    def next_url
+      page.nil? ? url : page.next
+    end
+
+    def fetch_next_page
+      parsed_response = connection.get(next_url, pagelen: PAGE_LENGTH, sort: :created_on)
+      Page.new(parsed_response, type)
+    end
+  end
+end
diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..94adaacc9b5598a85c2415ae8053e28380265892
--- /dev/null
+++ b/lib/bitbucket/representation/base.rb
@@ -0,0 +1,17 @@
+module Bitbucket
+  module Representation
+    class Base
+      def initialize(raw)
+        @raw = raw
+      end
+
+      def self.decorate(entries)
+        entries.map { |entry| new(entry)}
+      end
+
+      private
+
+      attr_reader :raw
+    end
+  end
+end
diff --git a/lib/bitbucket/representation/comment.rb b/lib/bitbucket/representation/comment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4937aa9728f97fe1b3f7aba6d1a0f23b36576a2f
--- /dev/null
+++ b/lib/bitbucket/representation/comment.rb
@@ -0,0 +1,27 @@
+module Bitbucket
+  module Representation
+    class Comment < Representation::Base
+      def author
+        user['username']
+      end
+
+      def note
+        raw.fetch('content', {}).fetch('raw', nil)
+      end
+
+      def created_at
+        raw['created_on']
+      end
+
+      def updated_at
+        raw['updated_on'] || raw['created_on']
+      end
+
+      private
+
+      def user
+        raw.fetch('user', {})
+      end
+    end
+  end
+end
diff --git a/lib/bitbucket/representation/issue.rb b/lib/bitbucket/representation/issue.rb
new file mode 100644
index 0000000000000000000000000000000000000000..054064395c397102ce15535144e502a9c42055ce
--- /dev/null
+++ b/lib/bitbucket/representation/issue.rb
@@ -0,0 +1,53 @@
+module Bitbucket
+  module Representation
+    class Issue < Representation::Base
+      CLOSED_STATUS = %w(resolved invalid duplicate wontfix closed).freeze
+
+      def iid
+        raw['id']
+      end
+
+      def kind
+        raw['kind']
+      end
+
+      def author
+        raw.fetch('reporter', {}).fetch('username', nil)
+      end
+
+      def description
+        raw.fetch('content', {}).fetch('raw', nil)
+      end
+
+      def state
+        closed? ? 'closed' : 'opened'
+      end
+
+      def title
+        raw['title']
+      end
+
+      def milestone
+        raw['milestone']['name'] if raw['milestone'].present?
+      end
+
+      def created_at
+        raw['created_on']
+      end
+
+      def updated_at
+        raw['edited_on']
+      end
+
+      def to_s
+        iid
+      end
+
+      private
+
+      def closed?
+        CLOSED_STATUS.include?(raw['state'])
+      end
+    end
+  end
+end
diff --git a/lib/bitbucket/representation/pull_request.rb b/lib/bitbucket/representation/pull_request.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eebf8093380723de7a3078f4005a12a5b694dccc
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request.rb
@@ -0,0 +1,65 @@
+module Bitbucket
+  module Representation
+    class PullRequest < Representation::Base
+      def author
+        raw.fetch('author', {}).fetch('username', nil)
+      end
+
+      def description
+        raw['description']
+      end
+
+      def iid
+        raw['id']
+      end
+
+      def state
+        if raw['state'] == 'MERGED'
+          'merged'
+        elsif raw['state'] == 'DECLINED'
+          'closed'
+        else
+          'opened'
+        end
+      end
+
+      def created_at
+        raw['created_on']
+      end
+
+      def updated_at
+        raw['updated_on']
+      end
+
+      def title
+        raw['title']
+      end
+
+      def source_branch_name
+        source_branch.fetch('branch', {}).fetch('name', nil)
+      end
+
+      def source_branch_sha
+        source_branch.fetch('commit', {}).fetch('hash', nil)
+      end
+
+      def target_branch_name
+        target_branch.fetch('branch', {}).fetch('name', nil)
+      end
+
+      def target_branch_sha
+        target_branch.fetch('commit', {}).fetch('hash', nil)
+      end
+
+      private
+
+      def source_branch
+        raw['source']
+      end
+
+      def target_branch
+        raw['destination']
+      end
+    end
+  end
+end
diff --git a/lib/bitbucket/representation/pull_request_comment.rb b/lib/bitbucket/representation/pull_request_comment.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f8efe03bae9b671adb01d1edccba9b94aa67df8
--- /dev/null
+++ b/lib/bitbucket/representation/pull_request_comment.rb
@@ -0,0 +1,39 @@
+module Bitbucket
+  module Representation
+    class PullRequestComment < Comment
+      def iid
+        raw['id']
+      end
+
+      def file_path
+        inline.fetch('path')
+      end
+
+      def old_pos
+        inline.fetch('from')
+      end
+
+      def new_pos
+        inline.fetch('to')
+      end
+
+      def parent_id
+        raw.fetch('parent', {}).fetch('id', nil)
+      end
+
+      def inline?
+        raw.has_key?('inline')
+      end
+
+      def has_parent?
+        raw.has_key?('parent')
+      end
+
+      private
+
+      def inline
+        raw.fetch('inline', {})
+      end
+    end
+  end
+end
diff --git a/lib/bitbucket/representation/repo.rb b/lib/bitbucket/representation/repo.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8969ecd1c195a6b4d4fa4f75b40413babefc9e29
--- /dev/null
+++ b/lib/bitbucket/representation/repo.rb
@@ -0,0 +1,67 @@
+module Bitbucket
+  module Representation
+    class Repo < Representation::Base
+      attr_reader :owner, :slug
+
+      def initialize(raw)
+        super(raw)
+      end
+
+      def owner_and_slug
+        @owner_and_slug ||= full_name.split('/', 2)
+      end
+
+      def owner
+        owner_and_slug.first
+      end
+
+      def slug
+        owner_and_slug.last
+      end
+
+      def clone_url(token = nil)
+        url = raw['links']['clone'].find { |link| link['name'] == 'https' }.fetch('href')
+
+        if token.present?
+          clone_url = URI::parse(url)
+          clone_url.user = "x-token-auth:#{token}"
+          clone_url.to_s
+        else
+          url
+        end
+      end
+
+      def description
+        raw['description']
+      end
+
+      def full_name
+        raw['full_name']
+      end
+
+      def issues_enabled?
+        raw['has_issues']
+      end
+
+      def name
+        raw['name']
+      end
+
+      def valid?
+        raw['scm'] == 'git'
+      end
+
+      def visibility_level
+        if raw['is_private']
+          Gitlab::VisibilityLevel::PRIVATE
+        else
+          Gitlab::VisibilityLevel::PUBLIC
+        end
+      end
+
+      def to_s
+        full_name
+      end
+    end
+  end
+end
diff --git a/lib/bitbucket/representation/user.rb b/lib/bitbucket/representation/user.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ba6b7667b49f8610f3932c3111b35d798219e805
--- /dev/null
+++ b/lib/bitbucket/representation/user.rb
@@ -0,0 +1,9 @@
+module Bitbucket
+  module Representation
+    class User < Representation::Base
+      def username
+        raw['username']
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/asciidoc.rb b/lib/gitlab/asciidoc.rb
index 9667df4ffb8467b0029c97ee0dcdea4a90284662..fa23428436129d3c734d3d1329077ab60ecd7f96 100644
--- a/lib/gitlab/asciidoc.rb
+++ b/lib/gitlab/asciidoc.rb
@@ -1,4 +1,5 @@
 require 'asciidoctor'
+require 'asciidoctor/converter/html5'
 
 module Gitlab
   # Parser/renderer for the AsciiDoc format that uses Asciidoctor and filters
@@ -23,7 +24,7 @@ module Gitlab
     def self.render(input, context, asciidoc_opts = {})
       asciidoc_opts.reverse_merge!(
         safe: :secure,
-        backend: :html5,
+        backend: :gitlab_html5,
         attributes: []
       )
       asciidoc_opts[:attributes].unshift(*DEFAULT_ADOC_ATTRS)
@@ -34,5 +35,29 @@ module Gitlab
 
       html.html_safe
     end
+
+    class Html5Converter < Asciidoctor::Converter::Html5Converter
+      extend Asciidoctor::Converter::Config
+
+      register_for 'gitlab_html5'
+
+      def stem(node)
+        return super unless node.style.to_sym == :latexmath
+
+        %(<pre#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="display"><code>#{node.content}</code></pre>)
+      end
+
+      def inline_quoted(node)
+        return super unless node.type.to_sym == :latexmath
+
+        %(<code#{id_attribute(node)} class="code math js-render-math #{node.role}" data-math-style="inline">#{node.text}</code>)
+      end
+
+      private
+
+      def id_attribute(node)
+        node.id ? %( id="#{node.id}") : nil
+      end
+    end
   end
 end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index aca5d0020cf5874313bcbac619dee3501ae77136..8dda65c71ef61644b1b1127598f81da5642b693b 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -2,6 +2,10 @@ module Gitlab
   module Auth
     class MissingPersonalTokenError < StandardError; end
 
+    SCOPES = [:api, :read_user]
+    DEFAULT_SCOPES = [:api]
+    OPTIONAL_SCOPES = SCOPES - DEFAULT_SCOPES
+
     class << self
       def find_for_git_client(login, password, project:, ip:)
         raise "Must provide an IP for rate limiting" if ip.nil?
@@ -88,7 +92,7 @@ module Gitlab
       def oauth_access_token_check(login, password)
         if login == "oauth2" && password.present?
           token = Doorkeeper::AccessToken.by_token(password)
-          if token && token.accessible?
+          if valid_oauth_token?(token)
             user = User.find_by(id: token.resource_owner_id)
             Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)
           end
@@ -97,12 +101,27 @@ module Gitlab
 
       def personal_access_token_check(login, password)
         if login && password
-          user = User.find_by_personal_access_token(password)
+          token = PersonalAccessToken.active.find_by_token(password)
           validation = User.by_login(login)
-          Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities) if user.present? && user == validation
+
+          if valid_personal_access_token?(token, validation)
+            Gitlab::Auth::Result.new(validation, nil, :personal_token, full_authentication_abilities)
+          end
         end
       end
 
+      def valid_oauth_token?(token)
+        token && token.accessible? && valid_api_token?(token)
+      end
+
+      def valid_personal_access_token?(token, user)
+        token && token.user == user && valid_api_token?(token)
+      end
+
+      def valid_api_token?(token)
+        AccessTokenValidationService.new(token).include_any_scope?(['api'])
+      end
+
       def lfs_token_check(login, password)
         deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
 
diff --git a/lib/gitlab/badge/build/status.rb b/lib/gitlab/badge/build/status.rb
index 50aa45e540674456729a3c66977a446312d74e1b..b762d85b6e5378b56a1693bc03c0f229f2ec7b7e 100644
--- a/lib/gitlab/badge/build/status.rb
+++ b/lib/gitlab/badge/build/status.rb
@@ -20,8 +20,8 @@ module Gitlab
 
         def status
           @project.pipelines
-            .where(sha: @sha, ref: @ref)
-            .status || 'unknown'
+            .where(sha: @sha)
+            .latest_status(@ref) || 'unknown'
         end
 
         def metadata
diff --git a/lib/gitlab/bitbucket_import.rb b/lib/gitlab/bitbucket_import.rb
deleted file mode 100644
index 7298152e7e98515524815f239706ca9abf9f558d..0000000000000000000000000000000000000000
--- a/lib/gitlab/bitbucket_import.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Gitlab
-  module BitbucketImport
-    mattr_accessor :public_key
-    @public_key = nil
-  end
-end
diff --git a/lib/gitlab/bitbucket_import/client.rb b/lib/gitlab/bitbucket_import/client.rb
deleted file mode 100644
index 8d1ad62fae0b088e51944a88ea45adc86a5c8844..0000000000000000000000000000000000000000
--- a/lib/gitlab/bitbucket_import/client.rb
+++ /dev/null
@@ -1,142 +0,0 @@
-module Gitlab
-  module BitbucketImport
-    class Client
-      class Unauthorized < StandardError; end
-
-      attr_reader :consumer, :api
-
-      def self.from_project(project)
-        import_data_credentials = project.import_data.credentials if project.import_data
-        if import_data_credentials && import_data_credentials[:bb_session]
-          token = import_data_credentials[:bb_session][:bitbucket_access_token]
-          token_secret = import_data_credentials[:bb_session][:bitbucket_access_token_secret]
-          new(token, token_secret)
-        else
-          raise Projects::ImportService::Error, "Unable to find project import data credentials for project ID: #{project.id}"
-        end
-      end
-
-      def initialize(access_token = nil, access_token_secret = nil)
-        @consumer = ::OAuth::Consumer.new(
-          config.app_id,
-          config.app_secret,
-          bitbucket_options
-        )
-
-        if access_token && access_token_secret
-          @api = ::OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
-        end
-      end
-
-      def request_token(redirect_uri)
-        request_token = consumer.get_request_token(oauth_callback: redirect_uri)
-
-        {
-          oauth_token:              request_token.token,
-          oauth_token_secret:       request_token.secret,
-          oauth_callback_confirmed: request_token.callback_confirmed?.to_s
-        }
-      end
-
-      def authorize_url(request_token, redirect_uri)
-        request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
-        if request_token.callback_confirmed?
-          request_token.authorize_url
-        else
-          request_token.authorize_url(oauth_callback: redirect_uri)
-        end
-      end
-
-      def get_token(request_token, oauth_verifier, redirect_uri)
-        request_token = ::OAuth::RequestToken.from_hash(consumer, request_token) if request_token.is_a?(Hash)
-
-        if request_token.callback_confirmed?
-          request_token.get_access_token(oauth_verifier: oauth_verifier)
-        else
-          request_token.get_access_token(oauth_callback: redirect_uri)
-        end
-      end
-
-      def user
-        JSON.parse(get("/api/1.0/user").body)
-      end
-
-      def issues(project_identifier)
-        all_issues = []
-        offset = 0
-        per_page = 50 # Maximum number allowed by Bitbucket
-        index = 0
-
-        begin
-          issues = JSON.parse(get(issue_api_endpoint(project_identifier, per_page, offset)).body)
-          # Find out how many total issues are present
-          total = issues["count"] if index == 0
-          all_issues.concat(issues["issues"])
-          offset += issues["issues"].count
-          index += 1
-        end while all_issues.count < total
-
-        all_issues
-      end
-
-      def issue_comments(project_identifier, issue_id)
-        comments = JSON.parse(get("/api/1.0/repositories/#{project_identifier}/issues/#{issue_id}/comments").body)
-        comments.sort_by { |comment| comment["utc_created_on"] }
-      end
-
-      def project(project_identifier)
-        JSON.parse(get("/api/1.0/repositories/#{project_identifier}").body)
-      end
-
-      def find_deploy_key(project_identifier, key)
-        JSON.parse(get("/api/1.0/repositories/#{project_identifier}/deploy-keys").body).find do |deploy_key|
-          deploy_key["key"].chomp == key.chomp
-        end
-      end
-
-      def add_deploy_key(project_identifier, key)
-        deploy_key = find_deploy_key(project_identifier, key)
-        return if deploy_key
-
-        JSON.parse(api.post("/api/1.0/repositories/#{project_identifier}/deploy-keys", key: key, label: "GitLab import key").body)
-      end
-
-      def delete_deploy_key(project_identifier, key)
-        deploy_key = find_deploy_key(project_identifier, key)
-        return unless deploy_key
-
-        api.delete("/api/1.0/repositories/#{project_identifier}/deploy-keys/#{deploy_key["pk"]}").code == "204"
-      end
-
-      def projects
-        JSON.parse(get("/api/1.0/user/repositories").body).select { |repo| repo["scm"] == "git" }
-      end
-
-      def incompatible_projects
-        JSON.parse(get("/api/1.0/user/repositories").body).reject { |repo| repo["scm"] == "git" }
-      end
-
-      private
-
-      def get(url)
-        response = api.get(url)
-        raise Unauthorized if (400..499).cover?(response.code.to_i)
-
-        response
-      end
-
-      def issue_api_endpoint(project_identifier, per_page, offset)
-        "/api/1.0/repositories/#{project_identifier}/issues?sort=utc_created_on&limit=#{per_page}&start=#{offset}"
-      end
-
-      def config
-        Gitlab.config.omniauth.providers.find { |provider| provider.name == "bitbucket" }
-      end
-
-      def bitbucket_options
-        OmniAuth::Strategies::Bitbucket.default_options[:client_options].symbolize_keys
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index f4b5097adb1f4c18d915ed781378c3ff157d22f3..7d2f92d577a426a3435fd9ac278c0ce3c91d2103 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -1,84 +1,234 @@
 module Gitlab
   module BitbucketImport
     class Importer
-      attr_reader :project, :client
+      LABELS = [{ title: 'bug', color: '#FF0000' },
+                { title: 'enhancement', color: '#428BCA' },
+                { title: 'proposal', color: '#69D100' },
+                { title: 'task', color: '#7F8C8D' }].freeze
+
+      attr_reader :project, :client, :errors, :users
 
       def initialize(project)
         @project = project
-        @client = Client.from_project(@project)
+        @client = Bitbucket::Client.new(project.import_data.credentials)
         @formatter = Gitlab::ImportFormatter.new
+        @labels = {}
+        @errors = []
+        @users = {}
       end
 
       def execute
-        import_issues if has_issues?
+        import_issues
+        import_pull_requests
+        handle_errors
 
         true
-      rescue ActiveRecord::RecordInvalid => e
-        raise Projects::ImportService::Error.new, e.message
-      ensure
-        Gitlab::BitbucketImport::KeyDeleter.new(project).execute
       end
 
       private
 
-      def gitlab_user_id(project, bitbucket_id)
-        if bitbucket_id
-          user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
-          (user && user.id) || project.creator_id
-        else
-          project.creator_id
-        end
+      def handle_errors
+        return unless errors.any?
+
+        project.update_column(:import_error, {
+          message: 'The remote data could not be fully imported.',
+          errors: errors
+        }.to_json)
       end
 
-      def identifier
-        project.import_source
+      def gitlab_user_id(project, username)
+        find_user_id(username) || project.creator_id
       end
 
-      def has_issues?
-        client.project(identifier)["has_issues"]
+      def find_user_id(username)
+        return nil unless username
+
+        return users[username] if users.key?(username)
+
+        users[username] = User.select(:id)
+                              .joins(:identities)
+                              .find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", username)
+                              .try(:id)
+      end
+
+      def repo
+        @repo ||= client.repo(project.import_source)
       end
 
       def import_issues
-        issues = client.issues(identifier)
+        return unless repo.issues_enabled?
+
+        create_labels
+
+        client.issues(repo).each do |issue|
+          begin
+            description = ''
+            description += @formatter.author_line(issue.author) unless find_user_id(issue.author)
+            description += issue.description
+
+            label_name = issue.kind
+            milestone = issue.milestone ? project.milestones.find_or_create_by(title: issue.milestone) : nil
+
+            gitlab_issue = project.issues.create!(
+              iid: issue.iid,
+              title: issue.title,
+              description: description,
+              state: issue.state,
+              author_id: gitlab_user_id(project, issue.author),
+              milestone: milestone,
+              created_at: issue.created_at,
+              updated_at: issue.updated_at
+            )
+
+            gitlab_issue.labels << @labels[label_name]
+
+            import_issue_comments(issue, gitlab_issue) if gitlab_issue.persisted?
+          rescue StandardError => e
+            errors << { type: :issue, iid: issue.iid, errors: e.message }
+          end
+        end
+      end
+
+      def import_issue_comments(issue, gitlab_issue)
+        client.issue_comments(repo, issue.iid).each do |comment|
+          # The note can be blank for issue service messages like "Changed title: ..."
+          # We would like to import those comments as well but there is no any
+          # specific parameter that would allow to process them, it's just an empty comment.
+          # To prevent our importer from just crashing or from creating useless empty comments
+          # we do this check.
+          next unless comment.note.present?
+
+          note = ''
+          note += @formatter.author_line(comment.author) unless find_user_id(comment.author)
+          note += comment.note
+
+          begin
+            gitlab_issue.notes.create!(
+              project: project,
+              note: note,
+              author_id: gitlab_user_id(project, comment.author),
+              created_at: comment.created_at,
+              updated_at: comment.updated_at
+            )
+          rescue StandardError => e
+            errors << { type: :issue_comment, iid: issue.iid, errors: e.message }
+          end
+        end
+      end
 
-        issues.each do |issue|
-          body = ''
-          reporter = nil
-          author = 'Anonymous'
+      def create_labels
+        LABELS.each do |label|
+          @labels[label[:title]] = project.labels.create!(label)
+        end
+      end
 
-          if issue["reported_by"] && issue["reported_by"]["username"]
-            reporter = issue["reported_by"]["username"]
-            author = reporter
+      def import_pull_requests
+        pull_requests = client.pull_requests(repo)
+
+        pull_requests.each do |pull_request|
+          begin
+            description = ''
+            description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
+            description += pull_request.description
+
+            merge_request = project.merge_requests.create(
+              iid: pull_request.iid,
+              title: pull_request.title,
+              description: description,
+              source_project: project,
+              source_branch: pull_request.source_branch_name,
+              source_branch_sha: pull_request.source_branch_sha,
+              target_project: project,
+              target_branch: pull_request.target_branch_name,
+              target_branch_sha: pull_request.target_branch_sha,
+              state: pull_request.state,
+              author_id: gitlab_user_id(project, pull_request.author),
+              assignee_id: nil,
+              created_at: pull_request.created_at,
+              updated_at: pull_request.updated_at
+            )
+
+            import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
+          rescue StandardError => e
+            errors << { type: :pull_request, iid: pull_request.iid, errors: e.message }
           end
+        end
+      end
 
-          body = @formatter.author_line(author)
-          body += issue["content"]
+      def import_pull_request_comments(pull_request, merge_request)
+        comments = client.pull_request_comments(repo, pull_request.iid)
 
-          comments = client.issue_comments(identifier, issue["local_id"])
+        inline_comments, pr_comments = comments.partition(&:inline?)
 
-          if comments.any?
-            body += @formatter.comments_header
-          end
+        import_inline_comments(inline_comments, pull_request, merge_request)
+        import_standalone_pr_comments(pr_comments, merge_request)
+      end
 
-          comments.each do |comment|
-            author = 'Anonymous'
+      def import_inline_comments(inline_comments, pull_request, merge_request)
+        line_code_map = {}
 
-            if comment["author_info"] && comment["author_info"]["username"]
-              author = comment["author_info"]["username"]
-            end
+        children, parents = inline_comments.partition(&:has_parent?)
+
+        # The Bitbucket API returns threaded replies as parent-child
+        # relationships. We assume that the child can appear in any order in
+        # the JSON.
+        parents.each do |comment|
+          line_code_map[comment.iid] = generate_line_code(comment)
+        end
 
-            body += @formatter.comment(author, comment["utc_created_on"], comment["content"])
+        children.each do |comment|
+          line_code_map[comment.iid] = line_code_map.fetch(comment.parent_id, nil)
+        end
+
+        inline_comments.each do |comment|
+          begin
+            attributes = pull_request_comment_attributes(comment)
+            attributes.merge!(
+              position: build_position(merge_request, comment),
+              line_code: line_code_map.fetch(comment.iid),
+              type: 'DiffNote')
+
+            merge_request.notes.create!(attributes)
+          rescue StandardError => e
+            errors << { type: :pull_request, iid: comment.iid, errors: e.message }
           end
+        end
+      end
 
-          project.issues.create!(
-            description: body,
-            title: issue["title"],
-            state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
-            author_id: gitlab_user_id(project, reporter)
-          )
+      def build_position(merge_request, pr_comment)
+        params = {
+          diff_refs: merge_request.diff_refs,
+          old_path: pr_comment.file_path,
+          new_path: pr_comment.file_path,
+          old_line: pr_comment.old_pos,
+          new_line: pr_comment.new_pos
+        }
+
+        Gitlab::Diff::Position.new(params)
+      end
+
+      def import_standalone_pr_comments(pr_comments, merge_request)
+        pr_comments.each do |comment|
+          begin
+            merge_request.notes.create!(pull_request_comment_attributes(comment))
+          rescue StandardError => e
+            errors << { type: :pull_request, iid: comment.iid, errors: e.message }
+          end
         end
-      rescue ActiveRecord::RecordInvalid => e
-        raise Projects::ImportService::Error, e.message
+      end
+
+      def generate_line_code(pr_comment)
+        Gitlab::Diff::LineCode.generate(pr_comment.file_path, pr_comment.new_pos, pr_comment.old_pos)
+      end
+
+      def pull_request_comment_attributes(comment)
+        {
+          project: project,
+          note: comment.note,
+          author_id: gitlab_user_id(project, comment.author),
+          created_at: comment.created_at,
+          updated_at: comment.updated_at
+        }
       end
     end
   end
diff --git a/lib/gitlab/bitbucket_import/key_adder.rb b/lib/gitlab/bitbucket_import/key_adder.rb
deleted file mode 100644
index 0b63f025d0ad45900d8c4816e4703ab9200935c5..0000000000000000000000000000000000000000
--- a/lib/gitlab/bitbucket_import/key_adder.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module Gitlab
-  module BitbucketImport
-    class KeyAdder
-      attr_reader :repo, :current_user, :client
-
-      def initialize(repo, current_user, access_params)
-        @repo, @current_user = repo, current_user
-        @client = Client.new(access_params[:bitbucket_access_token],
-                             access_params[:bitbucket_access_token_secret])
-      end
-
-      def execute
-        return false unless BitbucketImport.public_key.present?
-
-        project_identifier = "#{repo["owner"]}/#{repo["slug"]}"
-        client.add_deploy_key(project_identifier, BitbucketImport.public_key)
-
-        true
-      rescue
-        false
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/bitbucket_import/key_deleter.rb b/lib/gitlab/bitbucket_import/key_deleter.rb
deleted file mode 100644
index e03c3155b3ebeebf18e9e80fe62d135596fc2e88..0000000000000000000000000000000000000000
--- a/lib/gitlab/bitbucket_import/key_deleter.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-module Gitlab
-  module BitbucketImport
-    class KeyDeleter
-      attr_reader :project, :current_user, :client
-
-      def initialize(project)
-        @project = project
-        @current_user = project.creator
-        @client = Client.from_project(@project)
-      end
-
-      def execute
-        return false unless BitbucketImport.public_key.present?
-
-        client.delete_deploy_key(project.import_source, BitbucketImport.public_key)
-
-        true
-      rescue
-        false
-      end
-    end
-  end
-end
diff --git a/lib/gitlab/bitbucket_import/project_creator.rb b/lib/gitlab/bitbucket_import/project_creator.rb
index b90ef0b0fba3e942bfbdeaf3951284d7676d509c..eb03882ab269657add6ecc59c053be1fb2a60fef 100644
--- a/lib/gitlab/bitbucket_import/project_creator.rb
+++ b/lib/gitlab/bitbucket_import/project_creator.rb
@@ -1,10 +1,11 @@
 module Gitlab
   module BitbucketImport
     class ProjectCreator
-      attr_reader :repo, :namespace, :current_user, :session_data
+      attr_reader :repo, :name, :namespace, :current_user, :session_data
 
-      def initialize(repo, namespace, current_user, session_data)
+      def initialize(repo, name, namespace, current_user, session_data)
         @repo = repo
+        @name = name
         @namespace = namespace
         @current_user = current_user
         @session_data = session_data
@@ -13,15 +14,15 @@ module Gitlab
       def execute
         ::Projects::CreateService.new(
           current_user,
-          name: repo["name"],
-          path: repo["slug"],
-          description: repo["description"],
+          name: name,
+          path: name,
+          description: repo.description,
           namespace_id: namespace.id,
-          visibility_level: repo["is_private"] ? Gitlab::VisibilityLevel::PRIVATE : Gitlab::VisibilityLevel::PUBLIC,
-          import_type: "bitbucket",
-          import_source: "#{repo["owner"]}/#{repo["slug"]}",
-          import_url: "ssh://git@bitbucket.org/#{repo["owner"]}/#{repo["slug"]}.git",
-          import_data: { credentials: { bb_session: session_data } }
+          visibility_level: repo.visibility_level,
+          import_type: 'bitbucket',
+          import_source: repo.full_name,
+          import_url: repo.clone_url(session_data[:token]),
+          import_data: { credentials: session_data }
         ).execute
       end
     end
diff --git a/lib/gitlab/chat_commands/issue_create.rb b/lib/gitlab/chat_commands/issue_create.rb
index 1dba85c1b513959444b4fd115d422e772a932790..cefb6775db854becff484cc09f75073489a1611f 100644
--- a/lib/gitlab/chat_commands/issue_create.rb
+++ b/lib/gitlab/chat_commands/issue_create.rb
@@ -8,7 +8,7 @@ module Gitlab
       end
 
       def self.help_message
-        'issue new <title>\n<description>'
+        'issue new <title> *`⇧ Shift`*+*`↵ Enter`* <description>'
       end
 
       def self.allowed?(project, user)
diff --git a/lib/gitlab/checks/change_access.rb b/lib/gitlab/checks/change_access.rb
index cb1065223d4d65b9de3602a5dbc8df63a977137f..3d203017d9f4a1311c600fd7d0a5e3a726e45c59 100644
--- a/lib/gitlab/checks/change_access.rb
+++ b/lib/gitlab/checks/change_access.rb
@@ -3,11 +3,12 @@ module Gitlab
     class ChangeAccess
       attr_reader :user_access, :project
 
-      def initialize(change, user_access:, project:)
+      def initialize(change, user_access:, project:, env: {})
         @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
         @branch_name = Gitlab::Git.branch_name(@ref)
         @user_access = user_access
         @project = project
+        @env = env
       end
 
       def exec
@@ -68,7 +69,7 @@ module Gitlab
       end
 
       def forced_push?
-        Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev)
+        Gitlab::Checks::ForcePush.force_push?(@project, @oldrev, @newrev, env: @env)
       end
 
       def matching_merge_request?
diff --git a/lib/gitlab/checks/force_push.rb b/lib/gitlab/checks/force_push.rb
index 5fe86553bd014486944dc6fa936457e490e93628..de0c9049ebf346f9b7a4e79e2603d10b33c8005c 100644
--- a/lib/gitlab/checks/force_push.rb
+++ b/lib/gitlab/checks/force_push.rb
@@ -1,15 +1,20 @@
 module Gitlab
   module Checks
     class ForcePush
-      def self.force_push?(project, oldrev, newrev)
+      def self.force_push?(project, oldrev, newrev, env: {})
         return false if project.empty_repo?
 
         # Created or deleted branch
         if Gitlab::Git.blank_ref?(oldrev) || Gitlab::Git.blank_ref?(newrev)
           false
         else
-          missed_ref, _ = Gitlab::Popen.popen(%W(#{Gitlab.config.git.bin_path} --git-dir=#{project.repository.path_to_repo} rev-list --max-count=1 #{oldrev} ^#{newrev}))
-          missed_ref.present?
+          missed_ref, exit_status = Gitlab::Git::RevList.new(oldrev, newrev, project: project, env: env).execute
+
+          if exit_status == 0
+            missed_ref.present?
+          else
+            raise "Got a non-zero exit code while calling out to `git rev-list` in the force-push check."
+          end
         end
       end
     end
diff --git a/lib/gitlab/email/reply_parser.rb b/lib/gitlab/email/reply_parser.rb
index 85402c2a278b6ffa9f153618f9e9763a22e28e33..f586c5ab062100f3cf08b1bc2ec200ec30135e62 100644
--- a/lib/gitlab/email/reply_parser.rb
+++ b/lib/gitlab/email/reply_parser.rb
@@ -69,7 +69,7 @@ module Gitlab
           # This one might be controversial but so many reply lines have years, times and end with a colon.
           # Let's try it and see how well it works.
           break if (l =~ /\d{4}/ && l =~ /\d:\d\d/ && l =~ /\:$/) ||
-                   (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
+              (l =~ /On \w+ \d+,? \d+,?.*wrote:/)
 
           # Headers on subsequent lines
           break if (0..2).all? { |off| lines[idx + off] =~ REPLYING_HEADER_REGEX }
diff --git a/lib/gitlab/gfm/uploads_rewriter.rb b/lib/gitlab/gfm/uploads_rewriter.rb
index abc8c8c55e6f130952078f59231491318ef6ee4d..8fab54896160c69119f5cd646239df43a977916d 100644
--- a/lib/gitlab/gfm/uploads_rewriter.rb
+++ b/lib/gitlab/gfm/uploads_rewriter.rb
@@ -1,3 +1,5 @@
+require 'fileutils'
+
 module Gitlab
   module Gfm
     ##
@@ -22,7 +24,9 @@ module Gitlab
           return markdown unless file.try(:exists?)
 
           new_uploader = FileUploader.new(target_project)
-          new_uploader.store!(file)
+          with_link_in_tmp_dir(file.file) do |open_tmp_file|
+            new_uploader.store!(open_tmp_file)
+          end
           new_uploader.to_markdown
         end
       end
@@ -46,6 +50,19 @@ module Gitlab
         uploader.retrieve_from_store!(file)
         uploader.file
       end
+
+      # Because the uploaders use 'move_to_store' we must have a temporary
+      # file that is allowed to be (re)moved.
+      def with_link_in_tmp_dir(file)
+        dir = Dir.mktmpdir('UploadsRewriter', File.dirname(file))
+        # The filename matters to Carrierwave so we make sure to preserve it
+        tmp_file = File.join(dir, File.basename(file))
+        File.link(file, tmp_file)
+        # Open the file to placate Carrierwave
+        File.open(tmp_file) { |open_file| yield open_file }
+      ensure
+        FileUtils.rm_rf(dir)
+      end
     end
   end
 end
diff --git a/lib/gitlab/git/rev_list.rb b/lib/gitlab/git/rev_list.rb
new file mode 100644
index 0000000000000000000000000000000000000000..25e9d619697e36798e30820299f70e2642484781
--- /dev/null
+++ b/lib/gitlab/git/rev_list.rb
@@ -0,0 +1,42 @@
+module Gitlab
+  module Git
+    class RevList
+      attr_reader :project, :env
+
+      ALLOWED_VARIABLES = %w[GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES].freeze
+
+      def initialize(oldrev, newrev, project:, env: nil)
+        @project = project
+        @env = env.presence || {}
+        @args = [Gitlab.config.git.bin_path,
+                 "--git-dir=#{project.repository.path_to_repo}",
+                 "rev-list",
+                 "--max-count=1",
+                 oldrev,
+                 "^#{newrev}"]
+      end
+
+      def execute
+        Gitlab::Popen.popen(@args, nil, parse_environment_variables)
+      end
+
+      def valid?
+        environment_variables.all? do |(name, value)|
+          value.start_with?(project.repository.path_to_repo)
+        end
+      end
+
+      private
+
+      def parse_environment_variables
+        return {} unless valid?
+
+        environment_variables
+      end
+
+      def environment_variables
+        @environment_variables ||= env.slice(*ALLOWED_VARIABLES)
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index db07b7c5fcc5756dd160e44f7b3cee02c41a96ab..c6b6efda360cb9a752e8128e2a6cb6c942977050 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -17,12 +17,13 @@ module Gitlab
 
     attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
 
-    def initialize(actor, project, protocol, authentication_abilities:)
+    def initialize(actor, project, protocol, authentication_abilities:, env: {})
       @actor    = actor
       @project  = project
       @protocol = protocol
       @authentication_abilities = authentication_abilities
       @user_access = UserAccess.new(user, project: project)
+      @env = env
     end
 
     def check(cmd, changes)
@@ -103,7 +104,7 @@ module Gitlab
     end
 
     def change_access_check(change)
-      Checks::ChangeAccess.new(change, user_access: user_access, project: project).exec
+      Checks::ChangeAccess.new(change, user_access: user_access, project: project, env: @env).exec
     end
 
     def protocol_allowed?
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 2c21804fe7a6328b2f1d027b0dcacc918119d753..4d4e04e9e35a37570e619cbd49c493d55a6c3aab 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -8,6 +8,8 @@ module Gitlab
       gon.shortcuts_path         = help_page_path('shortcuts')
       gon.user_color_scheme      = Gitlab::ColorSchemes.for_user(current_user).css_class
       gon.award_menu_url         = emojis_path
+      gon.katex_css_url          = ActionController::Base.helpers.asset_path('katex.css')
+      gon.katex_js_url           = ActionController::Base.helpers.asset_path('katex.js')
 
       if current_user
         gon.current_user_id = current_user.id
diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb
new file mode 100644
index 0000000000000000000000000000000000000000..65713e73a59c050ca28721291951dac0287fdd02
--- /dev/null
+++ b/lib/gitlab/middleware/multipart.rb
@@ -0,0 +1,99 @@
+# Gitlab::Middleware::Multipart - a Rack::Multipart replacement
+#
+# Rack::Multipart leaves behind tempfiles in /tmp and uses valuable Ruby
+# process time to copy files around. This alternative solution uses
+# gitlab-workhorse to clean up the tempfiles and puts the tempfiles in a
+# location where copying should not be needed.
+#
+# When gitlab-workhorse finds files in a multipart MIME body it sends
+# a signed message via a request header. This message lists the names of
+# the multipart entries that gitlab-workhorse filtered out of the
+# multipart structure and saved to tempfiles. Workhorse adds new entries
+# in the multipart structure with paths to the tempfiles.
+#
+# The job of this Rack middleware is to detect and decode the message
+# from workhorse. If present, it walks the Rack 'params' hash for the
+# current request, opens the respective tempfiles, and inserts the open
+# Ruby File objects in the params hash where Rack::Multipart would have
+# put them. The goal is that application code deeper down can keep
+# working the way it did with Rack::Multipart without changes.
+#
+# CAVEAT: the code that modifies the params hash is a bit complex. It is
+# conceivable that certain Rack params structures will not be modified
+# correctly. We are not aware of such bugs at this time though.
+#
+
+module Gitlab
+  module Middleware
+    class Multipart
+      RACK_ENV_KEY = 'HTTP_GITLAB_WORKHORSE_MULTIPART_FIELDS'
+
+      class Handler
+        def initialize(env, message)
+          @request = Rack::Request.new(env)
+          @rewritten_fields = message['rewritten_fields']
+          @open_files = []
+        end
+
+        def with_open_files
+          @rewritten_fields.each do |field, tmp_path|
+            parsed_field = Rack::Utils.parse_nested_query(field)
+            raise "unexpected field: #{field.inspect}" unless parsed_field.count == 1
+
+            key, value = parsed_field.first
+            if value.nil?
+              value = File.open(tmp_path)
+              @open_files << value
+            else
+              value = decorate_params_value(value, @request.params[key], tmp_path)
+            end
+            @request.update_param(key, value)
+          end
+
+          yield
+        ensure
+          @open_files.each(&:close)
+        end
+
+        # This function calls itself recursively
+        def decorate_params_value(path_hash, value_hash, tmp_path)
+          unless path_hash.is_a?(Hash) && path_hash.count == 1
+            raise "invalid path: #{path_hash.inspect}"
+          end
+          path_key, path_value = path_hash.first
+
+          unless value_hash.is_a?(Hash) && value_hash[path_key]
+            raise "invalid value hash: #{value_hash.inspect}"
+          end
+
+          case path_value
+          when nil
+            value_hash[path_key] = File.open(tmp_path)
+            @open_files << value_hash[path_key]
+            value_hash
+          when Hash
+            decorate_params_value(path_value, value_hash[path_key], tmp_path)
+            value_hash
+          else
+            raise "unexpected path value: #{path_value.inspect}"
+          end
+        end
+      end
+
+      def initialize(app)
+        @app = app
+      end
+
+      def call(env)
+        encoded_message = env.delete(RACK_ENV_KEY)
+        return @app.call(env) if encoded_message.blank?
+
+        message = Gitlab::Workhorse.decode_jwt(encoded_message)[0]
+
+        Handler.new(env, message).with_open_files do
+          @app.call(env)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index cc74bb29087c7fd56cc0d7d07b3f57131fd5af53..4bc5cda8cb587ee32ab9f3fd1e88a66ef5925367 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -5,13 +5,13 @@ module Gitlab
   module Popen
     extend self
 
-    def popen(cmd, path = nil)
+    def popen(cmd, path = nil, vars = {})
       unless cmd.is_a?(Array)
         raise "System commands must be given as an array of strings"
       end
 
       path ||= Dir.pwd
-      vars = { "PWD" => path }
+      vars['PWD'] = path
       options = { chdir: path }
 
       unless File.directory?(path)
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 66e6b29e798b2d37a2edbff6200b468195064eb4..6bdf3db9cb88892b01053fd001bf750e8a4b6fab 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -110,7 +110,7 @@ module Gitlab
     end
 
     def notes
-      @notes ||= project.notes.user.search(query, as_user: @current_user).order('updated_at DESC')
+      @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC')
     end
 
     def commits
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index d9d1e3cccca858ea5960e998b2f44d7253e020de..9e0b0e5ea98ea9c831f5b2efbb4ea3a3235af933 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -123,5 +123,22 @@ module Gitlab
     def environment_name_regex_message
       "can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.' and spaces"
     end
+
+    def kubernetes_namespace_regex
+      /\A[a-z0-9]([-a-z0-9]*[a-z0-9])?\z/
+    end
+
+    def kubernetes_namespace_regex_message
+      "can contain only letters, digits or '-', and cannot start or end with '-'"
+    end
+
+    def environment_slug_regex
+      @environment_slug_regex ||= /\A[a-z]([a-z0-9-]*[a-z0-9])?\z/.freeze
+    end
+
+    def environment_slug_regex_message
+      "can contain only lowercase letters, digits, and '-'. " \
+      "Must start with a letter, and cannot end with '-'"
+    end
   end
 end
diff --git a/lib/gitlab/sql/union.rb b/lib/gitlab/sql/union.rb
index 1cd89b3a9c40b5de369023805e0942c014be5e3d..222021e8802e5e970a6074eb0185f85f6c45e905 100644
--- a/lib/gitlab/sql/union.rb
+++ b/lib/gitlab/sql/union.rb
@@ -22,9 +22,7 @@ module Gitlab
         # By using "unprepared_statements" we remove the usage of placeholders
         # (thus fixing this problem), at a slight performance cost.
         fragments = ActiveRecord::Base.connection.unprepared_statement do
-          @relations.map do |rel|
-            rel.reorder(nil).to_sql
-          end
+          @relations.map { |rel| rel.reorder(nil).to_sql }.reject(&:blank?)
         end
 
         fragments.join("\nUNION\n")
diff --git a/lib/gitlab/template/dockerfile_template.rb b/lib/gitlab/template/dockerfile_template.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d5d3e045a42d8db432f7436cdbdd30c2a4a581c6
--- /dev/null
+++ b/lib/gitlab/template/dockerfile_template.rb
@@ -0,0 +1,30 @@
+module Gitlab
+  module Template
+    class DockerfileTemplate < BaseTemplate
+      def content
+        explanation = "# This file is a template, and might need editing before it works on your project."
+        [explanation, super].join("\n")
+      end
+
+      class << self
+        def extension
+          'Dockerfile'
+        end
+
+        def categories
+          {
+            "General" => ''
+          }
+        end
+
+        def base_dir
+          Rails.root.join('vendor/dockerfile')
+        end
+
+        def finder(project = nil)
+          Gitlab::Template::Finders::GlobalTemplateFinder.new(self.base_dir, self.extension, self.categories)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 594439a5d4b3a6f979d362e58ebf39187faf651e..aeb1a26e1bae22bc2f16ad185ab1ac9975aff699 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -117,8 +117,12 @@ module Gitlab
       end
 
       def verify_api_request!(request_headers)
+        decode_jwt(request_headers[INTERNAL_API_REQUEST_HEADER])
+      end
+
+      def decode_jwt(encoded_message)
         JWT.decode(
-          request_headers[INTERNAL_API_REQUEST_HEADER],
+          encoded_message,
           secret,
           true,
           { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
diff --git a/lib/mattermost/session.rb b/lib/mattermost/session.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fb8d7d97f8a57138c8e3d488175780371b5cbb30
--- /dev/null
+++ b/lib/mattermost/session.rb
@@ -0,0 +1,115 @@
+module Mattermost
+  class NoSessionError < StandardError; end
+  # This class' prime objective is to obtain a session token on a Mattermost
+  # instance with SSO configured where this GitLab instance is the provider.
+  #
+  # The process depends on OAuth, but skips a step in the authentication cycle.
+  # For example, usually a user would click the 'login in GitLab' button on
+  # Mattermost, which would yield a 302 status code and redirects you to GitLab
+  # to approve the use of your account on Mattermost. Which would trigger a
+  # callback so Mattermost knows this request is approved and gets the required
+  # data to create the user account etc.
+  #
+  # This class however skips the button click, and also the approval phase to
+  # speed up the process and keep it without manual action and get a session
+  # going.
+  class Session
+    include Doorkeeper::Helpers::Controller
+    include HTTParty
+
+    base_uri Settings.mattermost.host
+
+    attr_accessor :current_resource_owner, :token
+
+    def initialize(current_user)
+      @current_resource_owner = current_user
+    end
+
+    def with_session
+      raise NoSessionError unless create
+
+      begin
+        yield self
+      ensure
+        destroy
+      end
+    end
+
+    # Next methods are needed for Doorkeeper
+    def pre_auth
+      @pre_auth ||= Doorkeeper::OAuth::PreAuthorization.new(
+        Doorkeeper.configuration, server.client_via_uid, params)
+    end
+
+    def authorization
+      @authorization ||= strategy.request
+    end
+
+    def strategy
+      @strategy ||= server.authorization_request(pre_auth.response_type)
+    end
+
+    def request
+      @request ||= OpenStruct.new(parameters: params)
+    end
+
+    def params
+      Rack::Utils.parse_query(oauth_uri.query).symbolize_keys
+    end
+
+    def get(path, options = {})
+      self.class.get(path, options.merge(headers: @headers))
+    end
+
+    def post(path, options = {})
+      self.class.post(path, options.merge(headers: @headers))
+    end
+
+    private
+
+    def create
+      return unless oauth_uri
+      return unless token_uri
+
+      @token = request_token
+      @headers = {
+        Authorization: "Bearer #{@token}"
+      }
+
+      @token
+    end
+
+    def destroy
+      post('/api/v3/users/logout')
+    end
+
+    def oauth_uri
+      return @oauth_uri if defined?(@oauth_uri)
+
+      @oauth_uri = nil
+
+      response = get("/api/v3/oauth/gitlab/login", follow_redirects: false)
+      return unless 300 <= response.code && response.code < 400
+
+      redirect_uri = response.headers['location']
+      return unless redirect_uri
+
+      @oauth_uri = URI.parse(redirect_uri)
+    end
+
+    def token_uri
+      @token_uri ||=
+        if oauth_uri
+          authorization.authorize.redirect_uri if pre_auth.authorizable?
+        end
+    end
+
+    def request_token
+      response = get(token_uri, follow_redirects: false)
+
+      if 200 <= response.code && response.code < 400
+        response.headers['token']
+      end
+    end
+  end
+end
diff --git a/lib/omniauth/strategies/bitbucket.rb b/lib/omniauth/strategies/bitbucket.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5a7d67c23903e3efb6163f7abd859e8e414d1678
--- /dev/null
+++ b/lib/omniauth/strategies/bitbucket.rb
@@ -0,0 +1,41 @@
+require 'omniauth-oauth2'
+
+module OmniAuth
+  module Strategies
+    class Bitbucket < OmniAuth::Strategies::OAuth2
+      option :name, 'bitbucket'
+
+      option :client_options, {
+        site: 'https://bitbucket.org',
+        authorize_url: 'https://bitbucket.org/site/oauth2/authorize',
+        token_url: 'https://bitbucket.org/site/oauth2/access_token'
+      }
+
+      uid do
+        raw_info['username']
+      end
+
+      info do
+        {
+          name: raw_info['display_name'],
+          avatar: raw_info['links']['avatar']['href'],
+          email: primary_email
+        }
+      end
+
+      def raw_info
+        @raw_info ||= access_token.get('api/2.0/user').parsed
+      end
+
+      def primary_email
+        primary = emails.find { |i| i['is_primary'] && i['is_confirmed'] }
+        primary && primary['email'] || nil
+      end
+
+      def emails
+        email_response = access_token.get('api/2.0/user/emails').parsed
+        @emails ||= email_response && email_response['values'] || nil
+      end
+    end
+  end
+end
diff --git a/lib/rouge/lexers/math.rb b/lib/rouge/lexers/math.rb
new file mode 100644
index 0000000000000000000000000000000000000000..80784adfd76930d603e3d7f74e60e5cde2d77a8a
--- /dev/null
+++ b/lib/rouge/lexers/math.rb
@@ -0,0 +1,21 @@
+module Rouge
+  module Lexers
+    class Math < Lexer
+      title "A passthrough lexer used for LaTeX input"
+      desc "A boring lexer that doesn't highlight anything"
+
+      tag 'math'
+      mimetypes 'text/plain'
+
+      default_options token: 'Text'
+
+      def token
+        @token ||= Token[option :token]
+      end
+
+      def stream_tokens(string, &b)
+        yield self.token, string
+      end
+    end
+  end
+end
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index d521de28e8a293a943f5bfa57f54e1cf6ed2f9f0..2f7c34a3f31f1e04b2b4be1a5c4a907b901b375a 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -20,6 +20,11 @@ upstream gitlab-workhorse {
   server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
 }
 
+map $http_upgrade $connection_upgrade_gitlab {
+    default upgrade;
+    ''      close;
+}
+
 ## Normal HTTP host
 server {
   ## Either remove "default_server" from the listen line below,
@@ -53,6 +58,8 @@ server {
     proxy_set_header    X-Real-IP           $remote_addr;
     proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
     proxy_set_header    X-Forwarded-Proto   $scheme;
+    proxy_set_header    Upgrade             $http_upgrade;
+    proxy_set_header    Connection          $connection_upgrade_gitlab;
 
     proxy_pass http://gitlab-workhorse;
   }
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index bf014b56cf68062d3da68c1416aea7e7d09c83de..5661394058db939834dc44df3ede3035fb27dac6 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -24,6 +24,11 @@ upstream gitlab-workhorse {
   server unix:/home/git/gitlab/tmp/sockets/gitlab-workhorse.socket fail_timeout=0;
 }
 
+map $http_upgrade $connection_upgrade_gitlab_ssl {
+    default upgrade;
+    ''      close;
+}
+
 ## Redirects all HTTP traffic to the HTTPS host
 server {
   ## Either remove "default_server" from the listen line below,
@@ -98,6 +103,9 @@ server {
     proxy_set_header    X-Forwarded-Ssl     on;
     proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;
     proxy_set_header    X-Forwarded-Proto   $scheme;
+    proxy_set_header    Upgrade             $http_upgrade;
+    proxy_set_header    Connection          $connection_upgrade_gitlab_ssl;
+
     proxy_pass http://gitlab-workhorse;
   }
 
diff --git a/public/404.html b/public/404.html
index 11b29d09a82981401004e1d4da06b65bfc28cbb7..b3b3a0fa3f30f7d03cebe562a3925da3a2b438fc 100644
--- a/public/404.html
+++ b/public/404.html
@@ -46,6 +46,14 @@
       margin: 40px auto;
     }
 
+    a {
+      line-height: 100px;
+      font-weight: normal;
+      color: #4A8BEE;
+      font-size: 18px;
+      text-decoration: none;
+    }
+
     .container {
       margin: auto 20px;
     }
@@ -63,6 +71,7 @@
     <hr />
     <p>Make sure the address is correct and that the page hasn't moved.</p>
     <p>Please contact your GitLab administrator if you think this is a mistake.</p>
+    <a href="javascript:history.back()">Go back</a>
   </div>
 </body>
 </html>
diff --git a/public/422.html b/public/422.html
index 9bd7cb4b7c82f7c4aac87ac639a1670cd30d4982..119e54ad8bd4fc2cc90390748e4d65c95e54244b 100644
--- a/public/422.html
+++ b/public/422.html
@@ -46,6 +46,14 @@
       margin: 40px auto;
     }
 
+    a {
+      line-height: 100px;
+      font-weight: normal;
+      color: #4A8BEE;
+      font-size: 18px;
+      text-decoration: none;
+    }
+
     .container {
       margin: auto 20px;
     }
@@ -63,6 +71,7 @@
     <hr />
     <p>Make sure you have access to the thing you tried to change.</p>
     <p>Please contact your GitLab administrator if you think this is a mistake.</p>
+    <a href="javascript:history.back()">Go back</a>
   </div>
 </body>
 </html>
diff --git a/public/500.html b/public/500.html
index f92e8839f8d9601e53373a810a5f4281c1ab3598..226ef3c40eaebe2b90b779635a54e50f084d6b70 100644
--- a/public/500.html
+++ b/public/500.html
@@ -46,6 +46,14 @@
       margin: 40px auto;
     }
 
+    a {
+      line-height: 100px;
+      font-weight: normal;
+      color: #4A8BEE;
+      font-size: 18px;
+      text-decoration: none;
+    }
+
     .container {
       margin: auto 20px;
     }
@@ -63,6 +71,7 @@
     <hr />
     <p>Try refreshing the page, or going back and attempting the action again.</p>
     <p>Please contact your GitLab administrator if this problem persists.</p>
+    <a href="javascript:history.back()">Go back</a>
   </div>
 </body>
 </html>
diff --git a/public/502.html b/public/502.html
index c2be4f130a95d603aa480053a378385e7ec5fc5a..f037b81bace0a3d160721ed30b57e7a0ac3adfd2 100644
--- a/public/502.html
+++ b/public/502.html
@@ -46,6 +46,14 @@
       margin: 40px auto;
     }
 
+    a {
+      line-height: 100px;
+      font-weight: normal;
+      color: #4A8BEE;
+      font-size: 18px;
+      text-decoration: none;
+    }
+
     .container {
       margin: auto 20px;
     }
@@ -63,6 +71,7 @@
     <hr />
     <p>Try refreshing the page, or going back and attempting the action again.</p>
     <p>Please contact your GitLab administrator if this problem persists.</p>
+    <a href="javascript:history.back()">Go back</a>
   </div>
 </body>
 </html>
diff --git a/public/503.html b/public/503.html
index 8850ffce362bc91f7053aee28d3a0c358a907848..f946a087871782c3fdce154ce0e74d2a71082fdc 100644
--- a/public/503.html
+++ b/public/503.html
@@ -46,6 +46,14 @@
       margin: 40px auto;
     }
 
+    a {
+      line-height: 100px;
+      font-weight: normal;
+      color: #4A8BEE;
+      font-size: 18px;
+      text-decoration: none;
+    }
+
     .container {
       margin: auto 20px;
     }
@@ -63,6 +71,7 @@
     <hr />
     <p>Try refreshing the page, or going back and attempting the action again.</p>
     <p>Please contact your GitLab administrator if this problem persists.</p>
+    <a href="javascript:history.back()">Go back</a>
   </div>
 </body>
 </html>
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 1d3c9fbbe2f49d5a0d109ae616f2838478c5317e..ce7c0b334ee071358b935322165ed9874c6bdc2d 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -6,11 +6,11 @@ describe Import::BitbucketController do
   let(:user) { create(:user) }
   let(:token) { "asdasd12345" }
   let(:secret) { "sekrettt" }
-  let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
+  let(:refresh_token) { SecureRandom.hex(15) }
+  let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } }
 
   def assign_session_tokens
-    session[:bitbucket_access_token] = token
-    session[:bitbucket_access_token_secret] = secret
+    session[:bitbucket_token] = token
   end
 
   before do
@@ -24,29 +24,36 @@ describe Import::BitbucketController do
     end
 
     it "updates access token" do
-      access_token = double(token: token, secret: secret)
-      allow_any_instance_of(Gitlab::BitbucketImport::Client).
+      expires_at = Time.now + 1.day
+      expires_in = 1.day
+      access_token = double(token: token,
+                            secret: secret,
+                            expires_at: expires_at,
+                            expires_in: expires_in,
+                            refresh_token: refresh_token)
+      allow_any_instance_of(OAuth2::Client).
         to receive(:get_token).and_return(access_token)
       stub_omniauth_provider('bitbucket')
 
       get :callback
 
-      expect(session[:bitbucket_access_token]).to eq(token)
-      expect(session[:bitbucket_access_token_secret]).to eq(secret)
+      expect(session[:bitbucket_token]).to eq(token)
+      expect(session[:bitbucket_refresh_token]).to eq(refresh_token)
+      expect(session[:bitbucket_expires_at]).to eq(expires_at)
+      expect(session[:bitbucket_expires_in]).to eq(expires_in)
       expect(controller).to redirect_to(status_import_bitbucket_url)
     end
   end
 
   describe "GET status" do
     before do
-      @repo = OpenStruct.new(slug: 'vim', owner: 'asd')
+      @repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true)
       assign_session_tokens
     end
 
     it "assigns variables" do
       @project = create(:project, import_type: 'bitbucket', creator_id: user.id)
-      client = stub_client(projects: [@repo])
-      allow(client).to receive(:incompatible_projects).and_return([])
+      allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
 
       get :status
 
@@ -57,7 +64,7 @@ describe Import::BitbucketController do
 
     it "does not show already added project" do
       @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim')
-      stub_client(projects: [@repo])
+      allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo])
 
       get :status
 
@@ -70,19 +77,16 @@ describe Import::BitbucketController do
     let(:bitbucket_username) { user.username }
 
     let(:bitbucket_user) do
-      { user: { username: bitbucket_username } }.with_indifferent_access
+      double(username: bitbucket_username)
     end
 
     let(:bitbucket_repo) do
-      { slug: "vim", owner: bitbucket_username }.with_indifferent_access
+      double(slug: "vim", owner: bitbucket_username, name: 'vim')
     end
 
     before do
-      allow(Gitlab::BitbucketImport::KeyAdder).
-        to receive(:new).with(bitbucket_repo, user, access_params).
-        and_return(double(execute: true))
-
-      stub_client(user: bitbucket_user, project: bitbucket_repo)
+      allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo)
+      allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user)
       assign_session_tokens
     end
 
@@ -90,7 +94,7 @@ describe Import::BitbucketController do
       context "when the Bitbucket user and GitLab user's usernames match" do
         it "takes the current user's namespace" do
           expect(Gitlab::BitbucketImport::ProjectCreator).
-            to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+            to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
             and_return(double(execute: true))
 
           post :create, format: :js
@@ -102,7 +106,7 @@ describe Import::BitbucketController do
 
         it "takes the current user's namespace" do
           expect(Gitlab::BitbucketImport::ProjectCreator).
-            to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+            to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
             and_return(double(execute: true))
 
           post :create, format: :js
@@ -114,7 +118,7 @@ describe Import::BitbucketController do
       let(:other_username) { "someone_else" }
 
       before do
-        bitbucket_repo["owner"] = other_username
+        allow(bitbucket_repo).to receive(:owner).and_return(other_username)
       end
 
       context "when a namespace with the Bitbucket user's username already exists" do
@@ -123,7 +127,7 @@ describe Import::BitbucketController do
         context "when the namespace is owned by the GitLab user" do
           it "takes the existing namespace" do
             expect(Gitlab::BitbucketImport::ProjectCreator).
-              to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params).
+              to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params).
               and_return(double(execute: true))
 
             post :create, format: :js
@@ -156,7 +160,7 @@ describe Import::BitbucketController do
 
           it "takes the new namespace" do
             expect(Gitlab::BitbucketImport::ProjectCreator).
-              to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
+              to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params).
               and_return(double(execute: true))
 
             post :create, format: :js
@@ -177,7 +181,7 @@ describe Import::BitbucketController do
 
           it "takes the current user's namespace" do
             expect(Gitlab::BitbucketImport::ProjectCreator).
-              to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+              to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params).
               and_return(double(execute: true))
 
             post :create, format: :js
diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..45534a3a5875abf205c55a853f753bf66024a5b8
--- /dev/null
+++ b/spec/controllers/profiles/personal_access_tokens_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe Profiles::PersonalAccessTokensController do
+  let(:user) { create(:user) }
+
+  describe '#create' do
+    def created_token
+      PersonalAccessToken.order(:created_at).last
+    end
+
+    before { sign_in(user) }
+
+    it "allows creation of a token" do
+      name = FFaker::Product.brand
+
+      post :create, personal_access_token: { name: name }
+
+      expect(created_token).not_to be_nil
+      expect(created_token.name).to eq(name)
+      expect(created_token.expires_at).to be_nil
+      expect(PersonalAccessToken.active).to include(created_token)
+    end
+
+    it "allows creation of a token with an expiry date" do
+      expires_at = 5.days.from_now
+
+      post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at }
+
+      expect(created_token).not_to be_nil
+      expect(created_token.expires_at.to_i).to eq(expires_at.to_i)
+    end
+
+    context "scopes" do
+      it "allows creation of a token with scopes" do
+        post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] }
+
+        expect(created_token).not_to be_nil
+        expect(created_token.scopes).to eq(['api', 'read_user'])
+      end
+
+      it "allows creation of a token with no scopes" do
+        post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] }
+
+        expect(created_token).not_to be_nil
+        expect(created_token.scopes).to eq([])
+      end
+    end
+  end
+end
diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b7bb92907125bebf912db24499ac8a2e5abf8a0f
--- /dev/null
+++ b/spec/controllers/search_controller_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe SearchController do
+  let(:user)    { create(:user) }
+  let(:project) { create(:empty_project, :public) }
+
+  before do
+    sign_in(user)
+  end
+
+  it 'finds issue comments' do
+    project = create(:empty_project, :public)
+    note = create(:note_on_issue, project: project)
+
+    get :show, project_id: project.id, scope: 'notes', search: note.note
+
+    expect(assigns[:search_objects].first).to eq note
+  end
+
+  context 'on restricted projects' do
+    context 'when signed out' do
+      before { sign_out(user) }
+
+      it "doesn't expose comments on issues" do
+        project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+        note = create(:note_on_issue, project: project)
+
+        get :show, project_id: project.id, scope: 'notes', search: note.note
+
+        expect(assigns[:search_objects].count).to eq(0)
+      end
+    end
+
+    it "doesn't expose comments on issues" do
+      project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+      note = create(:note_on_issue, project: project)
+
+      get :show, project_id: project.id, scope: 'notes', search: note.note
+
+      expect(assigns[:search_objects].count).to eq(0)
+    end
+
+    it "doesn't expose comments on merge_requests" do
+      project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+      note = create(:note_on_merge_request, project: project)
+
+      get :show, project_id: project.id, scope: 'notes', search: note.note
+
+      expect(assigns[:search_objects].count).to eq(0)
+    end
+
+    it "doesn't expose comments on snippets" do
+      project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE)
+      note = create(:note_on_project_snippet, project: project)
+
+      get :show, project_id: project.id, scope: 'notes', search: note.note
+
+      expect(assigns[:search_objects].count).to eq(0)
+    end
+  end
+end
diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a7c5283df949076348fc91bf7181778e0467d2e4
--- /dev/null
+++ b/spec/db/production/settings.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require 'rainbow/ext/string'
+
+describe 'seed production settings', lib: true do
+  context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do
+    before do
+      allow(ENV).to receive(:[]).and_call_original
+      allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789')
+    end
+
+    it 'writes the token to the database' do
+      load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb'))
+      expect(ApplicationSetting.current.runners_registration_token).to eq('013456789')
+    end
+  end
+end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 6919002dedcd439a03f34a9a077916b7272071cd..a10ba6297609638f510f4b8924350aabf842c0fd 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -67,7 +67,7 @@ FactoryGirl.define do
     end
 
     trait :on_project_snippet do
-      noteable { create(:snippet, project: project) }
+      noteable { create(:project_snippet, project: project) }
     end
 
     trait :system do
diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb
index da4c72bcb5b32af4b0b41b2ba83b9f2c92ba21ba..811eab7e15bdc298660ec61554b0e667ddb53ddb 100644
--- a/spec/factories/personal_access_tokens.rb
+++ b/spec/factories/personal_access_tokens.rb
@@ -5,5 +5,6 @@ FactoryGirl.define do
     name { FFaker::Product.brand }
     revoked false
     expires_at { 5.days.from_now }
+    scopes ['api']
   end
 end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 1166498ddffb165c6806ea3982f695014dd968da..0d072d6a690f7d74d2262c09338ac5258b1c96d0 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -133,4 +133,17 @@ FactoryGirl.define do
       )
     end
   end
+
+  factory :kubernetes_project, parent: :empty_project do
+    after :create do |project|
+      project.create_kubernetes_service(
+        active: true,
+        properties: {
+          namespace: project.path,
+          api_url: 'https://kubernetes.example.com/api',
+          token: 'a' * 40,
+        }
+      )
+    end
+  end
 end
diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..16064d60ce2c24d67b6714b30426e2d6735351af
--- /dev/null
+++ b/spec/features/admin/admin_active_tab_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+RSpec.describe 'admin active tab' do
+  before do
+    login_as :admin
+  end
+
+  shared_examples 'page has active tab' do |title|
+    it "activates #{title} tab" do
+      expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1)
+      expect(page.find('.layout-nav li.active')).to have_content(title)
+    end
+  end
+
+  shared_examples 'page has active sub tab' do |title|
+    it "activates #{title} sub tab" do
+      expect(page).to have_selector('.sub-nav li.active', count: 1)
+      expect(page.find('.sub-nav li.active')).to have_content(title)
+    end
+  end
+
+  context 'on home page' do
+    before do
+      visit admin_root_path
+    end
+
+    it_behaves_like 'page has active tab', 'Overview'
+  end
+
+  context 'on projects' do
+    before do
+      visit admin_projects_path
+    end
+
+    it_behaves_like 'page has active tab', 'Overview'
+    it_behaves_like 'page has active sub tab', 'Projects'
+  end
+
+  context 'on groups' do
+    before do
+      visit admin_groups_path
+    end
+
+    it_behaves_like 'page has active tab', 'Overview'
+    it_behaves_like 'page has active sub tab', 'Groups'
+  end
+
+  context 'on users' do
+    before do
+      visit admin_users_path
+    end
+
+    it_behaves_like 'page has active tab', 'Overview'
+    it_behaves_like 'page has active sub tab', 'Users'
+  end
+
+  context 'on logs' do
+    before do
+      visit admin_logs_path
+    end
+
+    it_behaves_like 'page has active tab', 'Monitoring'
+    it_behaves_like 'page has active sub tab', 'Logs'
+  end
+
+  context 'on messages' do
+    before do
+      visit admin_broadcast_messages_path
+    end
+
+    it_behaves_like 'page has active tab', 'Messages'
+  end
+
+  context 'on hooks' do
+    before do
+      visit admin_hooks_path
+    end
+
+    it_behaves_like 'page has active tab', 'Hooks'
+  end
+
+  context 'on background jobs' do
+    before do
+      visit admin_background_jobs_path
+    end
+
+    it_behaves_like 'page has active tab', 'Monitoring'
+    it_behaves_like 'page has active sub tab', 'Background Jobs'
+  end
+end
diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8bf6848078520a46419315342f4b84ff2c215f55
--- /dev/null
+++ b/spec/features/admin/admin_deploy_keys_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+RSpec.describe 'admin deploy keys', type: :feature do
+  let!(:deploy_key) { create(:deploy_key, public: true) }
+  let!(:another_deploy_key) { create(:another_deploy_key, public: true) }
+
+  before do
+    login_as(:admin)
+  end
+
+  it 'show all public deploy keys' do
+    visit admin_deploy_keys_path
+
+    expect(page).to have_content(deploy_key.title)
+    expect(page).to have_content(another_deploy_key.title)
+  end
+
+  it 'creates new deploy key' do
+    visit admin_deploy_keys_path
+
+    click_link 'New Deploy Key'
+    fill_in 'deploy_key_title', with: 'laptop'
+    fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop'
+    click_button 'Create'
+
+    expect(current_path).to eq admin_deploy_keys_path
+    expect(page).to have_content('laptop')
+  end
+end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index f6d625fa7f6d172806b0f71310da454024d72db5..0aa01fc499a05e1d06df1061b0675b3b630950ab 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -21,7 +21,7 @@ feature 'Admin Groups', feature: true do
     scenario 'shows the visibility level radio populated with the group visibility_level value' do
       group = create(:group, :private)
 
-      visit edit_admin_group_path(group)
+      visit admin_group_edit_path(group)
 
       expect_selected_visibility(group.visibility_level)
     end
diff --git a/spec/features/admin/admin_manage_applications_spec.rb b/spec/features/admin/admin_manage_applications_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c2c618b5659b480c51edb43ca2accc0511ab1a38
--- /dev/null
+++ b/spec/features/admin/admin_manage_applications_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+RSpec.describe 'admin manage applications', feature: true do
+  before do
+    login_as :admin
+  end
+
+  it do
+    visit admin_applications_path
+
+    click_on 'New Application'
+    expect(page).to have_content('New application')
+
+    fill_in :doorkeeper_application_name, with: 'test'
+    fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com'
+    click_on 'Submit'
+    expect(page).to have_content('Application: test')
+    expect(page).to have_content('Application Id')
+    expect(page).to have_content('Secret')
+
+    click_on 'Edit'
+    expect(page).to have_content('Edit application')
+
+    fill_in :doorkeeper_application_name, with: 'test_changed'
+    click_on 'Submit'
+    expect(page).to have_content('test_changed')
+    expect(page).to have_content('Application Id')
+    expect(page).to have_content('Secret')
+
+    visit admin_applications_path
+    page.within '.oauth-applications' do
+      click_on 'Destroy'
+    end
+    expect(page.find('.oauth-applications')).not_to have_content('test_changed')
+  end
+end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 30ded9202a4975f16a42a1b0e8503dc192d72517..a36bfd574cbe5c7e817e565dfb182355278e9543 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -8,11 +8,11 @@ describe "Admin::Projects", feature: true  do
 
   describe "GET /admin/projects" do
     before do
-      visit admin_namespaces_projects_path
+      visit admin_projects_path
     end
 
     it "is ok" do
-      expect(current_path).to eq(admin_namespaces_projects_path)
+      expect(current_path).to eq(admin_projects_path)
     end
 
     it "has projects list" do
@@ -22,7 +22,7 @@ describe "Admin::Projects", feature: true  do
 
   describe "GET /admin/projects/:id" do
     before do
-      visit admin_namespaces_projects_path
+      visit admin_projects_path
       click_link "#{@project.name}"
     end
 
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index 23a504ff965cbcd7bc158e7f3790942b0e9bac0b..8f561c8f90b4a398cb3c47c589bbf61299559442 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -107,7 +107,7 @@ describe 'Commits' do
         describe 'Cancel build' do
           it 'cancels build' do
             visit ci_status_path(pipeline)
-            click_on 'Cancel'
+            find('a.btn[title="Cancel"]').click
             expect(page).to have_content 'canceled'
           end
         end
diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..608aedd34717088c4ad74cf3a172f4051bbd1a67
--- /dev/null
+++ b/spec/features/groups/members/sorting_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Sorting', feature: true do
+  let(:owner)     { create(:user, name: 'John Doe') }
+  let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
+  let(:group)     { create(:group) }
+
+  background do
+    create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago)
+    create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago)
+
+    login_as(owner)
+  end
+
+  scenario 'sorts alphabetically by default' do
+    visit_members_list(sort: nil)
+
+    expect(first_member).to include(owner.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+  end
+
+  scenario 'sorts by access level ascending' do
+    visit_members_list(sort: :access_level_asc)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(owner.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+  end
+
+  scenario 'sorts by access level descending' do
+    visit_members_list(sort: :access_level_desc)
+
+    expect(first_member).to include(owner.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+  end
+
+  scenario 'sorts by last joined' do
+    visit_members_list(sort: :last_joined)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(owner.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+  end
+
+  scenario 'sorts by oldest joined' do
+    visit_members_list(sort: :oldest_joined)
+
+    expect(first_member).to include(owner.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+  end
+
+  scenario 'sorts by name ascending' do
+    visit_members_list(sort: :name_asc)
+
+    expect(first_member).to include(owner.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+  end
+
+  scenario 'sorts by name descending' do
+    visit_members_list(sort: :name_desc)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(owner.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+  end
+
+  scenario 'sorts by recent sign in' do
+    visit_members_list(sort: :recent_sign_in)
+
+    expect(first_member).to include(owner.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+  end
+
+  scenario 'sorts by oldest sign in' do
+    visit_members_list(sort: :oldest_sign_in)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(owner.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+  end
+
+  def visit_members_list(sort:)
+    visit group_group_members_path(group.to_param, sort: sort)
+  end
+
+  def first_member
+    page.all('ul.content-list > li').first.text
+  end
+
+  def second_member
+    page.all('ul.content-list > li').last.text
+  end
+end
diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index bc2c087c9b9a0f44ebc0a2608c716baaf36e92e5..832757b24d4f6fd71dee14fbf6346ec67e5e61cf 100644
--- a/spec/features/issues/bulk_assignment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
   let!(:issue2)   { create(:issue, project: project, title: "Issue 2") }
   let!(:bug)      { create(:label, project: project, title: 'bug') }
   let!(:feature)  { create(:label, project: project, title: 'feature') }
+  let!(:wontfix)  { create(:label, project: project, title: 'wontfix') }
 
   context 'as an allowed user', js: true do
     before do
@@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do
         expect(find("#issue_#{issue1.id}")).not_to have_content 'feature'
       end
     end
+
+    # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877
+    context 'unmarking common label' do
+      before do
+        issue1.labels << bug
+        issue1.labels << feature
+        issue2.labels << bug
+
+        visit namespace_project_issues_path(project.namespace, project)
+      end
+
+      it 'applies label from filtered results' do
+        check 'check_all_issues'
+
+        page.within('.issues_bulk_update') do
+          click_button 'Labels'
+          wait_for_ajax
+
+          expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active')
+          expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate')
+
+          click_link 'bug'
+          find('.dropdown-input-field', visible: true).set('wontfix')
+          click_link 'wontfix'
+        end
+
+        update_issues
+
+        page.within '.issues-holder' do
+          expect(find("#issue_#{issue1.id}")).not_to have_content 'bug'
+          expect(find("#issue_#{issue1.id}")).to have_content 'feature'
+          expect(find("#issue_#{issue1.id}")).to have_content 'wontfix'
+
+          expect(find("#issue_#{issue2.id}")).not_to have_content 'bug'
+          expect(find("#issue_#{issue2.id}")).not_to have_content 'feature'
+          expect(find("#issue_#{issue2.id}")).to have_content 'wontfix'
+        end
+      end
+    end
   end
 
   context 'as a guest' do
@@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do
 
   def open_labels_dropdown(items = [], unmark = false)
     page.within('.issues_bulk_update') do
-      click_button 'Label'
+      click_button 'Labels'
       wait_for_ajax
       items.map do |item|
         click_link item
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index cd0512a37e66e443b1438ad499b3153412e4fbcb..da64827b377b128daa93e9abe8416628d0de5656 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -89,4 +89,12 @@ feature 'GFM autocomplete', feature: true, js: true do
       end
     end
   end
+
+  it 'doesnt open autocomplete after non-word character' do
+    page.within '.timeline-content-form' do
+      find('#note_note').native.send_keys("@#{user.username[0..2]}!")
+    end
+
+    expect(page).not_to have_selector('.atwho-view')
+  end
 end
diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dc32c8f73733dc7c9b7cb518d0d95864bf3c73cb
--- /dev/null
+++ b/spec/features/merge_requests/closes_issues_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+feature 'Merge Request closing issues message', feature: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:issue_1) { create(:issue, project: project)}
+  let(:issue_2) { create(:issue, project: project)}
+  let(:merge_request) do
+    create(
+      :merge_request,
+      :simple,
+      source_project: project,
+      description: merge_request_description
+    )
+  end
+  let(:merge_request_description) { 'Merge Request Description' }
+
+  before do
+    project.team << [user, :master]
+
+    login_as user
+
+    visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+  end
+
+  context 'not closing or mentioning any issue' do
+    it 'does not display closing issue message' do
+      expect(page).not_to have_css('.mr-widget-footer')
+    end
+  end
+
+  context 'closing issues but not mentioning any other issue' do
+    let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" }
+
+    it 'does not display closing issue message' do
+      expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}")
+    end
+  end
+
+  context 'mentioning issues but not closing them' do
+    let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" }
+
+    it 'does not display closing issue message' do
+      expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.")
+    end
+  end
+
+  context 'closing some issues and mentioning, but not closing, others' do
+    let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" }
+
+    it 'does not display closing issue message' do
+      expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.")
+    end
+  end
+end
diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3dbe26cddb0fda2c9ed2bb7c258d6e17e0dc4635
--- /dev/null
+++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+feature 'Clicking toggle commit message link', feature: true, js: true do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:issue_1) { create(:issue, project: project)}
+  let(:issue_2) { create(:issue, project: project)}
+  let(:merge_request) do
+    create(
+      :merge_request,
+      :simple,
+      source_project: project,
+      description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}"
+    )
+  end
+  let(:textbox) { page.find(:css, '.js-commit-message', visible: false) }
+  let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) }
+  let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) }
+  let(:default_message) do
+    [
+      "Merge branch 'feature' into 'master'",
+      merge_request.title,
+      "Closes #{issue_1.to_reference} and #{issue_2.to_reference}",
+      "See merge request #{merge_request.to_reference}"
+    ].join("\n\n")
+  end
+  let(:message_with_description) do
+    [
+      "Merge branch 'feature' into 'master'",
+      merge_request.title,
+      merge_request.description,
+      "See merge request #{merge_request.to_reference}"
+    ].join("\n\n")
+  end
+
+  before do
+    project.team << [user, :master]
+
+    login_as user
+
+    visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+    expect(textbox).not_to be_visible
+    click_link "Modify commit message"
+    expect(textbox).to be_visible
+  end
+
+  it "toggles commit message between message with description and without description " do
+    expect(textbox.value).to eq(default_message)
+
+    click_link "Include description in commit message"
+
+    expect(textbox.value).to eq(message_with_description)
+
+    click_link "Don't include description in commit message"
+
+    expect(textbox.value).to eq(default_message)
+  end
+
+  it "toggles link between 'Include description' and 'Don't include description'" do
+    expect(include_link).to be_visible
+    expect(do_not_include_link).not_to be_visible
+
+    click_link "Include description in commit message"
+
+    expect(include_link).not_to be_visible
+    expect(do_not_include_link).to be_visible
+
+    click_link "Don't include description in commit message"
+
+    expect(include_link).to be_visible
+    expect(do_not_include_link).not_to be_visible
+  end
+end
diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb
index a78a1c9c8905ca1721c7026df7707a4765961e40..c2545b0c2591e11574ba08e5a5936bb40d82b162 100644
--- a/spec/features/participants_autocomplete_spec.rb
+++ b/spec/features/participants_autocomplete_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 feature 'Member autocomplete', feature: true do
+  include WaitForAjax
+
   let(:project) { create(:project, :public) }
   let(:user) { create(:user) }
   let(:participant) { create(:user) }
@@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do
   end
 
   def open_member_suggestions
-    sleep 1
     page.within('.new-note') do
-      sleep 1
-      find('#note_note').native.send_keys('@')
+      find('#note_note').send_keys('@')
     end
+    wait_for_ajax
   end
 
   def visit_issue(project, issue)
diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb
index a85930c75434b0b629106c2ff8b1b33ddb29a82e..55a01057c83b25b1d7acea05b2033e3d3563f955 100644
--- a/spec/features/profiles/personal_access_tokens_spec.rb
+++ b/spec/features/profiles/personal_access_tokens_spec.rb
@@ -27,28 +27,25 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
 
   describe "token creation" do
     it "allows creation of a token" do
-      visit profile_personal_access_tokens_path
-      fill_in "Name", with: FFaker::Product.brand
-
-      expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
-      expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
-      expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
-      expect(active_personal_access_tokens).to have_text("Never")
-    end
+      name = FFaker::Product.brand
 
-    it "allows creation of a token with an expiry date" do
       visit profile_personal_access_tokens_path
-      fill_in "Name", with: FFaker::Product.brand
+      fill_in "Name", with: name
 
       # Set date to 1st of next month
       find_field("Expires at").trigger('focus')
       find("a[title='Next']").click
       click_on "1"
 
-      expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1)
-      expect(created_personal_access_token).to eq(PersonalAccessToken.last.token)
-      expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name)
+      # Scopes
+      check "api"
+      check "read_user"
+
+      click_on "Create Personal Access Token"
+      expect(active_personal_access_tokens).to have_text(name)
       expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium))
+      expect(active_personal_access_tokens).to have_text('api')
+      expect(active_personal_access_tokens).to have_text('read_user')
     end
 
     context "when creation fails" do
@@ -85,7 +82,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do
         disallow_personal_access_token_saves!
         visit profile_personal_access_tokens_path
 
-        expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count }
+        click_on "Revoke"
         expect(active_personal_access_tokens).to have_text(personal_access_token.name)
         expect(page).to have_content("Could not revoke")
       end
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..32f33a3ca97e2f0c8376ba90f68fd3a7803bda6c
--- /dev/null
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+feature 'User wants to add a Dockerfile file', feature: true do
+  include WaitForAjax
+
+  before do
+    user = create(:user)
+    project = create(:project)
+    project.team << [user, :master]
+    login_as user
+    visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile')
+  end
+
+  scenario 'user can see Dockerfile dropdown' do
+    expect(page).to have_css('.dockerfile-selector')
+  end
+
+  scenario 'user can pick a Dockerfile file from the dropdown', js: true do
+    find('.js-dockerfile-selector').click
+    wait_for_ajax
+    within '.dockerfile-selector' do
+      find('.dropdown-input-field').set('HTTPd')
+      find('.dropdown-content li', text: 'HTTPd').click
+    end
+    wait_for_ajax
+
+    expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd')
+    expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/')
+  end
+end
diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb
index 1921ea6d8aec39c9f0801c6568777ae5fed4a466..dd9622f16a0c4369dbd1d79bf8d56949913822d9 100644
--- a/spec/features/projects/gfm_autocomplete_load_spec.rb
+++ b/spec/features/projects/gfm_autocomplete_load_spec.rb
@@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do
   end
 
   it 'does not load on project#show' do
-    expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('')
+    expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({})
   end
 
   it 'loads on new issue page' do
     visit new_namespace_project_issue_path(project.namespace, project)
 
-    expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('')
+    expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({})
   end
 end
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index bfe59bdb90e75cb869cab6aa86cb9dc81ac5e225..d3165d07d7b9a37d532a6bf97c7d74de7d32098b 100644
Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ
diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d6ebb523f9533aa72292925f662fc7c874970ffa
--- /dev/null
+++ b/spec/features/projects/members/sorting_spec.rb
@@ -0,0 +1,98 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Sorting', feature: true do
+  let(:master)    { create(:user, name: 'John Doe') }
+  let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) }
+  let(:project)   { create(:empty_project) }
+
+  background do
+    create(:project_member, :master, user: master, project: project, created_at: 5.days.ago)
+    create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago)
+
+    login_as(master)
+  end
+
+  scenario 'sorts alphabetically by default' do
+    visit_members_list(sort: nil)
+
+    expect(first_member).to include(master.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+  end
+
+  scenario 'sorts by access level ascending' do
+    visit_members_list(sort: :access_level_asc)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(master.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending')
+  end
+
+  scenario 'sorts by access level descending' do
+    visit_members_list(sort: :access_level_desc)
+
+    expect(first_member).to include(master.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending')
+  end
+
+  scenario 'sorts by last joined' do
+    visit_members_list(sort: :last_joined)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(master.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined')
+  end
+
+  scenario 'sorts by oldest joined' do
+    visit_members_list(sort: :oldest_joined)
+
+    expect(first_member).to include(master.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined')
+  end
+
+  scenario 'sorts by name ascending' do
+    visit_members_list(sort: :name_asc)
+
+    expect(first_member).to include(master.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending')
+  end
+
+  scenario 'sorts by name descending' do
+    visit_members_list(sort: :name_desc)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(master.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending')
+  end
+
+  scenario 'sorts by recent sign in' do
+    visit_members_list(sort: :recent_sign_in)
+
+    expect(first_member).to include(master.name)
+    expect(second_member).to include(developer.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in')
+  end
+
+  scenario 'sorts by oldest sign in' do
+    visit_members_list(sort: :oldest_sign_in)
+
+    expect(first_member).to include(developer.name)
+    expect(second_member).to include(master.name)
+    expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in')
+  end
+
+  def visit_members_list(sort:)
+    visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort)
+  end
+
+  def first_member
+    page.all('ul.content-list > li').first.text
+  end
+
+  def second_member
+    page.all('ul.content-list > li').last.text
+  end
+end
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index 3350a3aeefcc404013729c254b559c2bd7f1f82e..0a77eaa123cc77c5aa6c5ece7fda711358da37c6 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -38,6 +38,91 @@ describe "Pipelines", feature: true, js: true do
       expect(page).to have_css('#js-tab-pipeline.active')
     end
 
+    describe 'pipeline graph' do
+      context 'when pipeline has running builds' do
+        it 'shows a running icon and a cancel action for the running build' do
+          page.within('a[data-title="deploy - running"]') do
+            expect(page).to have_selector('.ci-status-icon-running')
+            expect(page).to have_content('deploy')
+          end
+
+          page.within('a[data-title="deploy - running"] + .ci-action-icon-container') do
+            expect(page).to have_selector('.ci-action-icon-container .fa-ban')
+          end
+        end
+
+        it 'should be possible to cancel the running build' do
+          find('a[data-title="deploy - running"] + .ci-action-icon-container').trigger('click')
+
+          expect(page).not_to have_content('Cancel running')
+        end
+      end
+
+      context 'when pipeline has successful builds' do
+        it 'shows the success icon and a retry action for the successfull build' do
+          page.within('a[data-title="build - passed"]') do
+            expect(page).to have_selector('.ci-status-icon-success')
+            expect(page).to have_content('build')
+          end
+
+          page.within('a[data-title="build - passed"] + .ci-action-icon-container') do
+            expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+          end
+        end
+
+        it 'should be possible to retry the success build' do
+          find('a[data-title="build - passed"] + .ci-action-icon-container').trigger('click')
+
+          expect(page).not_to have_content('Retry build')
+        end
+      end
+
+      context 'when pipeline has failed builds' do
+        it 'shows the failed icon and a retry action for the failed build' do
+          page.within('a[data-title="test - failed"]') do
+            expect(page).to have_selector('.ci-status-icon-failed')
+            expect(page).to have_content('test')
+          end
+
+          page.within('a[data-title="test - failed"] + .ci-action-icon-container') do
+            expect(page).to have_selector('.ci-action-icon-container .fa-refresh')
+          end
+        end
+
+        it 'should be possible to retry the failed build' do
+          find('a[data-title="test - failed"] + .ci-action-icon-container').trigger('click')
+
+          expect(page).not_to have_content('Retry build')
+        end
+      end
+
+      context 'when pipeline has manual builds' do
+        it 'shows the skipped icon and a play action for the manual build' do
+          page.within('a[data-title="manual build - manual play action"]') do
+            expect(page).to have_selector('.ci-status-icon-skipped')
+            expect(page).to have_content('manual')
+          end
+
+          page.within('a[data-title="manual build - manual play action"] + .ci-action-icon-container') do
+            expect(page).to have_selector('.ci-action-icon-container .fa-play')
+          end
+        end
+
+        it 'should be possible to play the manual build' do
+          find('a[data-title="manual build - manual play action"] + .ci-action-icon-container').trigger('click')
+
+          expect(page).not_to have_content('Play build')
+        end
+      end
+
+      context 'when pipeline has external build' do
+        it 'shows the success icon and the generic comit status build' do
+          expect(page).to have_selector('.ci-status-icon-success')
+          expect(page).to have_content('jenkins')
+        end
+      end
+    end
+
     context 'page tabs' do
       it 'shows Pipeline and Builds tabs with link' do
         expect(page).to have_link('Pipeline')
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 16541f51d98cb9fc6e54b9ba6189db1929325efb..320ed13a01dd3d277de9d3537ad91ad582529bce 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -2,8 +2,8 @@ require 'spec_helper'
 
 feature 'Projects > Slack service > Setup events', feature: true do
   let(:user) { create(:user) }
-  let(:service) { SlackService.new }
-  let(:project) { create(:project, slack_service: service) }
+  let(:service) { SlackNotificationService.new }
+  let(:project) { create(:project, slack_notification_service: service) }
 
   background do
     service.fields
diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb
index fe8cd7b7602380afaecdc79f1ec53123d9584134..e180ca53eb51e8f6c9e438a3a243456e0320573e 100644
--- a/spec/features/security/admin_access_spec.rb
+++ b/spec/features/security/admin_access_spec.rb
@@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true  do
   include AccessMatchers
 
   describe "GET /admin/projects" do
-    subject { admin_namespaces_projects_path }
+    subject { admin_projects_path }
 
     it { is_expected.to be_allowed_for :admin }
     it { is_expected.to be_denied_for :user }
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 7f69e888f3249242c8ef22c8aa1d944dd7c5c364..97737d7ddc730a81d3263a6251a87dba5f3b8e70 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -10,24 +10,24 @@ describe IssuesFinder do
   let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') }
   let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') }
   let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) }
-  let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
-  let!(:label_link) { create(:label_link, label: label, target: issue2) }
-
-  before do
-    project1.team << [user, :master]
-    project2.team << [user, :developer]
-    project2.team << [user2, :developer]
-
-    issue1
-    issue2
-    issue3
-  end
 
   describe '#execute' do
+    let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') }
+    let!(:label_link) { create(:label_link, label: label, target: issue2) }
     let(:search_user) { user }
     let(:params) { {} }
     let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute }
 
+    before do
+      project1.team << [user, :master]
+      project2.team << [user, :developer]
+      project2.team << [user2, :developer]
+
+      issue1
+      issue2
+      issue3
+    end
+
     context 'scope: all' do
       let(:scope) { 'all' }
 
@@ -193,6 +193,15 @@ describe IssuesFinder do
           expect(issues).to contain_exactly(issue2, issue3)
         end
       end
+
+      it 'finds issues user can access due to group' do
+        group = create(:group)
+        project = create(:empty_project, group: group)
+        issue = create(:issue, project: project)
+        group.add_user(user, :owner)
+
+        expect(issues).to include(issue)
+      end
     end
 
     context 'personal scope' do
@@ -210,5 +219,43 @@ describe IssuesFinder do
         end
       end
     end
+
+    context 'when project restricts issues' do
+      let(:scope) { nil }
+
+      it "doesn't return team-only issues to non team members" do
+        project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+        issue = create(:issue, project: project)
+
+        expect(issues).not_to include(issue)
+      end
+
+      it "doesn't return issues if feature disabled" do
+        [project1, project2].each do |project|
+          project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED)
+        end
+
+        expect(issues.count).to eq 0
+      end
+    end
+  end
+
+  describe '.not_restricted_by_confidentiality' do
+    let(:authorized_user) { create(:user) }
+    let(:project) { create(:empty_project, namespace: authorized_user.namespace) }
+    let!(:public_issue) { create(:issue, project: project) }
+    let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
+
+    it 'returns non confidential issues for nil user' do
+      expect(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue)
+    end
+
+    it 'returns non confidential issues for user not authorized for the issues projects' do
+      expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue)
+    end
+
+    it 'returns all issues for user authorized for the issues projects' do
+      expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue)
+    end
   end
 end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 7c6860372cc2c1041e61b2f78049a90ac6e3c11b..4d21254323c78d104a5dbced40dd20f5c141e083 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -2,59 +2,203 @@ require 'spec_helper'
 
 describe NotesFinder do
   let(:user) { create :user }
-  let(:project) { create :project }
-  let(:note1) { create :note_on_commit, project: project }
-  let(:note2) { create :note_on_commit, project: project }
-  let(:commit) { note1.noteable }
+  let(:project) { create(:empty_project) }
 
   before do
     project.team << [user, :master]
   end
 
   describe '#execute' do
-    let(:params)  { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
+    it 'finds notes on snippets when project is public and user isnt a member'
 
-    before do
-      note1
-      note2
+    it 'finds notes on merge requests' do
+      create(:note_on_merge_request, project: project)
+
+      notes = described_class.new(project, user).execute
+
+      expect(notes.count).to eq(1)
     end
 
-    it 'finds all notes' do
-      notes = NotesFinder.new.execute(project, user, params)
-      expect(notes.size).to eq(2)
+    it 'finds notes on snippets' do
+      create(:note_on_project_snippet, project: project)
+
+      notes = described_class.new(project, user).execute
+
+      expect(notes.count).to eq(1)
     end
 
-    it 'raises an exception for an invalid target_type' do
-      params.merge!(target_type: 'invalid')
-      expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type')
+    it "excludes notes on commits the author can't download" do
+      project = create(:project, :private)
+      note = create(:note_on_commit, project: project)
+      params = { target_type: 'commit', target_id: note.noteable.id }
+
+      notes = described_class.new(project, create(:user), params).execute
+
+      expect(notes.count).to eq(0)
     end
 
-    it 'filters out old notes' do
-      note2.update_attribute(:updated_at, 2.hours.ago)
-      notes = NotesFinder.new.execute(project, user, params)
-      expect(notes).to eq([note1])
+    it 'succeeds when no notes found' do
+      notes = described_class.new(project, create(:user)).execute
+
+      expect(notes.count).to eq(0)
     end
 
-    context 'confidential issue notes' do
-      let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
-      let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+    context 'on restricted projects' do
+      let(:project) do
+        create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE,
+                                        snippets_access_level: ProjectFeature::PRIVATE,
+                                        merge_requests_access_level: ProjectFeature::PRIVATE)
+      end
+
+      it 'publicly excludes notes on merge requests' do
+        create(:note_on_merge_request, project: project)
+
+        notes = described_class.new(project, create(:user)).execute
+
+        expect(notes.count).to eq(0)
+      end
+
+      it 'publicly excludes notes on issues' do
+        create(:note_on_issue, project: project)
+
+        notes = described_class.new(project, create(:user)).execute
+
+        expect(notes.count).to eq(0)
+      end
+
+      it 'publicly excludes notes on snippets' do
+        create(:note_on_project_snippet, project: project)
+
+        notes = described_class.new(project, create(:user)).execute
+
+        expect(notes.count).to eq(0)
+      end
+    end
+
+    context 'for target' do
+      let(:project) { create(:project) }
+      let(:note1) { create :note_on_commit, project: project }
+      let(:note2) { create :note_on_commit, project: project }
+      let(:commit) { note1.noteable }
+      let(:params)  { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } }
+
+      before do
+        note1
+        note2
+      end
+
+      it 'finds all notes' do
+        notes = described_class.new(project, user, params).execute
+        expect(notes.size).to eq(2)
+      end
+
+      it 'finds notes on merge requests' do
+        note = create(:note_on_merge_request, project: project)
+        params = { target_type: 'merge_request', target_id: note.noteable.id }
+
+        notes = described_class.new(project, user, params).execute
+
+        expect(notes).to include(note)
+      end
+
+      it 'finds notes on snippets' do
+        note = create(:note_on_project_snippet, project: project)
+        params = { target_type: 'snippet', target_id: note.noteable.id }
 
-      let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+        notes = described_class.new(project, user, params).execute
 
-      it 'returns notes if user can see the issue' do
-        expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note])
+        expect(notes.count).to eq(1)
       end
 
-      it 'raises an error if user can not see the issue' do
+      it 'raises an exception for an invalid target_type' do
+        params.merge!(target_type: 'invalid')
+        expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
+      end
+
+      it 'filters out old notes' do
+        note2.update_attribute(:updated_at, 2.hours.ago)
+        notes = described_class.new(project, user, params).execute
+        expect(notes).to eq([note1])
+      end
+
+      context 'confidential issue notes' do
+        let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+        let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) }
+
+        let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } }
+
+        it 'returns notes if user can see the issue' do
+          expect(described_class.new(project, user, params).execute).to eq([confidential_note])
+        end
+
+        it 'raises an error if user can not see the issue' do
+          user = create(:user)
+          expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+        end
+
+        it 'raises an error for project members with guest role' do
+          user = create(:user)
+          project.team << [user, :guest]
+
+          expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+        end
+      end
+    end
+  end
+
+  describe '.search' do
+    let(:project) { create(:empty_project, :public) }
+    let(:note) { create(:note_on_issue, note: 'WoW', project: project) }
+
+    it 'returns notes with matching content' do
+      expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note])
+    end
+
+    it 'returns notes with matching content regardless of the casing' do
+      expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note])
+    end
+
+    it 'returns commit notes user can access' do
+      note = create(:note_on_commit, project: project)
+
+      expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note])
+    end
+
+    context "confidential issues" do
+      let(:user) { create(:user) }
+      let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
+      let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
+
+      it "returns notes with matching content if user can see the issue" do
+        expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note])
+      end
+
+      it "does not return notes with matching content if user can not see the issue" do
         user = create(:user)
-        expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+        expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty
       end
 
-      it 'raises an error for project members with guest role' do
+      it "does not return notes with matching content for project members with guest role" do
         user = create(:user)
         project.team << [user, :guest]
+        expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty
+      end
+
+      it "does not return notes with matching content for unauthenticated users" do
+        expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty
+      end
+    end
+
+    context 'inlines SQL filters on subqueries for performance' do
+      let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql }
+      let(:number_of_noteable_types) { 4 }
+
+      specify 'project_id check' do
+        expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2)
+      end
 
-        expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound)
+      specify 'search filter' do
+        expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types
       end
     end
   end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index 15863d444f8d5a5f6f6aa2d2758d71b298017e30..92053e5a7c64dcd4a5da7df935ec65c06802a566 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe ApplicationHelper do
+  include UploadHelpers
+
   describe 'current_controller?' do
     it 'returns true when controller matches argument' do
       stub_controller_name('foo')
@@ -52,10 +54,8 @@ describe ApplicationHelper do
   end
 
   describe 'project_icon' do
-    let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
-
     it 'returns an url for the avatar' do
-      project = create(:project, avatar: File.open(avatar_file_path))
+      project = create(:project, avatar: File.open(uploaded_image_temp_path))
 
       avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif"
       expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s).
@@ -74,10 +74,8 @@ describe ApplicationHelper do
   end
 
   describe 'avatar_icon' do
-    let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
-
     it 'returns an url for the avatar' do
-      user = create(:user, avatar: File.open(avatar_file_path))
+      user = create(:user, avatar: File.open(uploaded_image_temp_path))
 
       expect(helper.avatar_icon(user.email).to_s).
         to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -88,7 +86,7 @@ describe ApplicationHelper do
       # Must be stubbed after the stub above, and separately
       stub_config_setting(url: Settings.send(:build_gitlab_url))
 
-      user = create(:user, avatar: File.open(avatar_file_path))
+      user = create(:user, avatar: File.open(uploaded_image_temp_path))
 
       expect(helper.avatar_icon(user.email).to_s).
         to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif")
@@ -102,7 +100,7 @@ describe ApplicationHelper do
 
     describe 'using a User' do
       it 'returns an URL for the avatar' do
-        user = create(:user, avatar: File.open(avatar_file_path))
+        user = create(:user, avatar: File.open(uploaded_image_temp_path))
 
         expect(helper.avatar_icon(user).to_s).
           to match("/uploads/user/avatar/#{user.id}/banana_sample.gif")
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 233d00534e507446288b4c577f43e5b6be49c1ba..c8b0d86425fa706731191fc9a05e72f1d06ff502 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -6,7 +6,7 @@ describe GroupsHelper do
 
     it 'returns an url for the avatar' do
       group = create(:group)
-      group.avatar = File.open(avatar_file_path)
+      group.avatar = fixture_file_upload(avatar_file_path)
       group.save!
       expect(group_icon(group.path).to_s).
         to match("/uploads/group/avatar/#{group.id}/banana_sample.gif")
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index a3171353bfb5817e71a03b1e943abe31f8c6da47..49e562495655be3ed9eb968a63fa712beca6ec64 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -1,42 +1,44 @@
-/* eslint-disable */
+/*= require lib/utils/text_utility */
 /*= require abuse_reports */
 
-/*= require jquery */
-
 ((global) => {
-  const FIXTURE = 'abuse_reports.html';
-  const MAX_MESSAGE_LENGTH = 500;
+  describe('Abuse Reports', () => {
+    const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw';
+    const MAX_MESSAGE_LENGTH = 500;
+
+    let messages;
 
-  function assertMaxLength($message) {
-    expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
-  }
+    const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH);
+    const findMessage = searchText => messages.filter(
+      (index, element) => element.innerText.indexOf(searchText) > -1,
+    ).first();
 
-  describe('Abuse Reports', function() {
     fixture.preload(FIXTURE);
 
-    beforeEach(function() {
+    beforeEach(function () {
       fixture.load(FIXTURE);
-      new global.AbuseReports();
+      this.abuseReports = new global.AbuseReports();
+      messages = $('.abuse-reports .message');
     });
 
-    it('should truncate long messages', function() {
-      const $longMessage = $('#long');
+
+    it('should truncate long messages', () => {
+      const $longMessage = findMessage('LONG MESSAGE');
       expect($longMessage.data('original-message')).toEqual(jasmine.anything());
       assertMaxLength($longMessage);
     });
 
-    it('should not truncate short messages', function() {
-      const $shortMessage = $('#short');
+    it('should not truncate short messages', () => {
+      const $shortMessage = findMessage('SHORT MESSAGE');
       expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything());
     });
 
-    it('should allow clicking a truncated message to expand and collapse the full message', function() {
-      const $longMessage = $('#long');
+    it('should allow clicking a truncated message to expand and collapse the full message', () => {
+      const $longMessage = findMessage('LONG MESSAGE');
       $longMessage.click();
       expect($longMessage.data('original-message').length).toEqual($longMessage.text().length);
       $longMessage.click();
       assertMaxLength($longMessage);
     });
   });
-
 })(window.gl);
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
index 8640cd440850be2d4d23095b4b0cdf174515a65a..192da4ee8d9acc8419fb6be799822fbf0cccbebf 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */
+
 /*= require js.cookie.js */
 /*= require jquery.endless-scroll.js */
 /*= require pager */
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 7b2e3db6218a4a12079475526ffaa5cfd0d0ebc3..89201c8cb8b71c1de7ef5220910257405cbaa8a2 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-undef, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */
+/* global AwardsHandler */
 
 /*= require awards_handler */
 /*= require jquery */
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index efb1203eb2f74069a5d03e4adad24d1fff134203..0f61000bc37a28ac02cb4fa9865af0e1b85721ff 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,4 +1,4 @@
-/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, no-undef, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */
 
 /*= require behaviors/quick_submit */
 
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index b84dfc8197beb764d80ee799c20570466f1b9cd0..b3a1afa28a564906fc2e6d06be0bb66a6b5b7231 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -1,4 +1,11 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */
+/* global Vue */
+/* global BoardService */
+/* global boardsMockInterceptor */
+/* global Cookies */
+/* global listObj */
+/* global listObjDuplicate */
+
 //= require jquery
 //= require jquery_ujs
 //= require js.cookie
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index 90cb89265451cd45f35a6ef0e88c77ace7e20074..c8a61a0a9b5dbcb531b50743373361fab7c89d26 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle */
+/* global BoardService */
+/* global ListIssue */
+
 //= require jquery
 //= require jquery_ujs
 //= require js.cookie
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index dfbcbe3a7c1db031e9ce1fda89d700ac050167e1..7d942ec3d658e1b2a8d5800fb035e2f09de5a0cf 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -1,4 +1,10 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle */
+/* global Vue */
+/* global boardsMockInterceptor */
+/* global BoardService */
+/* global List */
+/* global listObj */
+
 //= require jquery
 //= require jquery_ujs
 //= require js.cookie
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index fcb3d8f17d8c6326ce8e216b0b2db4d478f36956..8d3e2237fda5a6ddb7c5cd4e3a5300461d4e9394 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, no-unused-vars, quote-props */
+
 const listObj = {
   id: 1,
   position: 0,
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
index 93f73fa0e9a669a5a69525f954888bd7a2b3abdd..aadf6f518a86a5a2d80dbe6d035d0df225b9a286 100644
--- a/spec/javascripts/dashboard_spec.js.es6
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable no-new, padded-blocks */
+
 /*= require sidebar */
 /*= require jquery */
 /*= require js.cookie */
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
index 9fdbab3a9e983d1d47e25de61e00e723c7a5ed7b..8ece24555c5f86e90b8ab87b5299f59ce0e9beb0 100644
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -1,5 +1,5 @@
-/* eslint-disable */
 //= require lib/utils/datetime_utility
+
 (() => {
   describe('Date time utils', () => {
     describe('get day name', () => {
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
index 9b2845af608835168b08f339011546690d17b6fd..18805d26ac0229342853c0ebe40313735a3406ed 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -1,8 +1,11 @@
-/* eslint-disable */
+/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */
+/* global CommentsStore */
+
 //= require vue
 //= require diff_notes/models/discussion
 //= require diff_notes/models/note
 //= require diff_notes/stores/comments
+
 (() => {
   function createDiscussion(noteId = 1, resolved = true) {
     CommentsStore.create('a', noteId, true, resolved, 'test');
diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..3b71c255b309b8134079e293227966a0cd5b7f6b
--- /dev/null
+++ b/spec/javascripts/extensions/object_spec.js.es6
@@ -0,0 +1,25 @@
+/*= require extensions/object */
+
+describe('Object extensions', () => {
+  describe('assign', () => {
+    it('merges source object into target object', () => {
+      const targetObj = {};
+      const sourceObj = {
+        foo: 'bar',
+      };
+      Object.assign(targetObj, sourceObj);
+      expect(targetObj.foo).toBe('bar');
+    });
+
+    it('merges object with the same properties', () => {
+      const targetObj = {
+        foo: 'bar',
+      };
+      const sourceObj = {
+        foo: 'baz',
+      };
+      Object.assign(targetObj, sourceObj);
+      expect(targetObj.foo).toBe('baz');
+    });
+  });
+});
diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml
deleted file mode 100644
index 2ec302abcb7dc9e58bd4f0b03e993c1582663e34..0000000000000000000000000000000000000000
--- a/spec/javascripts/fixtures/abuse_reports.html.haml
+++ /dev/null
@@ -1,16 +0,0 @@
-.abuse-reports
-  .message#long
-    Cat ipsum dolor sit amet, hide head under blanket so no one can see.
-    Gate keepers of hell eat and than sleep on your face but hunt by meowing
-    loudly at 5am next to human slave food dispenser cats go for world
-    domination or chase laser, yet poop on grasses chirp at birds. Cat is love,
-    cat is life chase after silly colored fish toys around the house climb a
-    tree, wait for a fireman jump to fireman then scratch his face fall asleep
-    on the washing machine lies down always hungry so caticus cuteicus. Sit on
-    human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to
-    pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under
-    blanket so no one can see throwup on your pillow.
-  .message#short
-    Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your
-    beauty sleep 18 hours - checked, be fabulous for the rest of the day -
-    checked! for shake treat bag.
diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb
new file mode 100644
index 0000000000000000000000000000000000000000..de673f94d72c4509126ddc3e18e79d4be9a46a67
--- /dev/null
+++ b/spec/javascripts/fixtures/abuse_reports.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do
+  include JavaScriptFixturesHelpers
+
+  let(:admin) { create(:admin) }
+  let!(:abuse_report) { create(:abuse_report) }
+  let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') }
+  let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) }
+
+  render_views
+
+  before(:all) do
+    clean_frontend_fixtures('abuse_reports/')
+  end
+
+  before(:each) do
+    sign_in(admin)
+  end
+
+  it 'abuse_reports/abuse_reports_list.html.raw' do |example|
+    get :index
+
+    expect(response).to be_success
+    store_frontend_fixture(response, example.description)
+  end
+end
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index 8ba238018cdd4775ba88424f354d0bc73a6c67a8..bfaf90e2aeec592f12ea966a7cb5f4b395b8e39a 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -1,4 +1,6 @@
-/* eslint-disable */
+/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */
+/* global Turbolinks */
+
 /*= require jquery */
 /*= require gl_dropdown */
 /*= require turbolinks */
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
index 0713e30e485692b48e9ceb5fe9f098c47daf85ce..5018e87ad6c8205845276f90636659896e4c6a45 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */
+
 //= require jquery
 //= require gl_field_errors
 
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index a406e6cc36a200ebca50ee5a8d4ff71c24bca29d..bc5cbeb6a4081f71181788208a6f781626cd203e 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,4 +1,8 @@
-/* eslint-disable quotes, no-undef, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
+/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */
+/* global d3 */
+/* global ContributorsGraph */
+/* global ContributorsMasterGraph */
+
 //= require graphs/stat_graph_contributors_graph
 
 describe("ContributorsGraph", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 96f39abe13ec930f5c1a12c55dde5f3c6aabca99..751f3d175e2560c44bee70bc20e1b1efa61b57c8 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, no-undef, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces, max-len */
+/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */
+/* global ContributorsStatGraphUtil */
+
 //= require graphs/stat_graph_contributors_util
 
 describe("ContributorsStatGraphUtil", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index f78573b992bdd1c25e1f25dcb1465cd24443cfff..0da124632ae751fd0a7fdfb1aafe9ca492a89287 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable quotes, padded-blocks, no-undef, semi */
+/* eslint-disable quotes, padded-blocks, semi */
+/* global StatGraph */
+
 //= require graphs/stat_graph
 
 describe("StatGraph", function () {
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 14af6644de1a0ecb0492c0ed474f914c59a9af60..faab5ae00c2fad5f30fe213ae022b2926b14b290 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-undef, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */
+/* global Issue */
 
 /*= require lib/utils/text_utility */
 /*= require issue */
@@ -70,7 +71,7 @@
         $('input[type=checkbox]').attr('checked', true).trigger('change');
         expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
       });
-      
+
       it('submits an ajax request on tasklist:changed', function() {
         spyOn(jQuery, 'ajax').and.callFake(function(req) {
           expect(req.type).toBe('PATCH');
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 49687048eb5ab12f101eb493090c7074cf157b6e..0c48d04776fb03b68f7ac9d830320a9ae9ef9d27 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -1,4 +1,7 @@
-/* eslint-disable */
+/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */
+/* global IssuableContext */
+/* global LabelsSelect */
+
 //= require lib/utils/type_utility
 //= require jquery
 //= require bootstrap
diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
new file mode 100644
index 0000000000000000000000000000000000000000..3645dd70c55876c4c2d65ca9d09e0578dfe66cc2
--- /dev/null
+++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6
@@ -0,0 +1,43 @@
+//= require lib/utils/custom_event_polyfill
+
+describe('Custom Event Polyfill', () => {
+  it('should be defined', () => {
+    expect(window.CustomEvent).toBeDefined();
+  });
+
+  it('should create a `CustomEvent` instance', () => {
+    const e = new window.CustomEvent('foo');
+
+    expect(e.type).toEqual('foo');
+    expect(e.bubbles).toBe(false);
+    expect(e.cancelable).toBe(false);
+    expect(e.detail).toBeFalsy();
+  });
+
+  it('should create a `CustomEvent` instance with a `details` object', () => {
+    const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } });
+
+    expect(e.type).toEqual('bar');
+    expect(e.bubbles).toBe(false);
+    expect(e.cancelable).toBe(false);
+    expect(e.detail.foo).toEqual('bar');
+  });
+
+  it('should create a `CustomEvent` instance with a `bubbles` boolean', () => {
+    const e = new window.CustomEvent('bar', { bubbles: true });
+
+    expect(e.type).toEqual('bar');
+    expect(e.bubbles).toBe(true);
+    expect(e.cancelable).toBe(false);
+    expect(e.detail).toBeFalsy();
+  });
+
+  it('should create a `CustomEvent` instance with a `cancelable` boolean', () => {
+    const e = new window.CustomEvent('bar', { cancelable: true });
+
+    expect(e.type).toEqual('bar');
+    expect(e.bubbles).toBe(false);
+    expect(e.cancelable).toBe(true);
+    expect(e.detail).toBeFalsy();
+  });
+});
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index b8b174a2e53a6ebeb1dc8c5b07120cf404b1d8fa..decdf583410c333aa4074074940700e6e7a7153c 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-undef, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */
+/* global LineHighlighter */
 
 /*= require line_highlighter */
 
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index cbe2634d3a43f6b9b9cf557274939b7610ffe347..4cf1693af1b25fabe005d419568111fc8abc7360 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-return-assign, no-undef, padded-blocks */
+/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */
+/* global MergeRequest */
 
 /*= require merge_request */
 
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 8828970d984d22bd1edddb3dab8eaaf4f61d9f06..a6cb9e4774454d515f0ceaefc5ef49cb2984859d 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, no-undef, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */
+/* global NewBranchForm */
 
 /*= require jquery-ui/autocomplete */
 /*= require new_branch_form */
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 2db182d702b6a56962703f0f93efd546e0921f90..d3bfa7730fa29029f8532a30e08a346677a4cfb0 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-undef, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */
+/* global Notes */
+
 /*= require notes */
 /*= require autosize */
 /*= require gl_form */
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 65de1756201f6eaaf56ba426172f8b2ff18ad0f7..bb802a4b5e3930e4371396de583b3f04efe51977 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-undef, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */
+
+/* global Project */
 
 /*= require bootstrap */
 /*= require select2 */
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 0a9bc546144c3dacbc4a3472aa0b2ab3693711a2..a083dbf033a217cba784f95d114e6da4a93729ee 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-undef, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */
+/* global Sidebar */
 
 /*= require right_sidebar */
 /*= require jquery */
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index e37816b0a8cfc62b8b1bad30d6a54e7a71efbe80..7bc898aed5daaae3f2db4a0992cf85ec7a75c1d8 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,4 +1,5 @@
-/* eslint-disable space-before-function-paren, no-return-assign, no-undef, no-var, quotes, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */
+/* global ShortcutsIssuable */
 
 /*= require shortcuts_issuable */
 
diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6
index df395296791a5c908ce638f4149e5a3861bfbd4c..6a70dd856a711e656a5a54bf17fa57b34f183ce9 100644
--- a/spec/javascripts/subbable_resource_spec.js.es6
+++ b/spec/javascripts/subbable_resource_spec.js.es6
@@ -1,4 +1,5 @@
-/* eslint-disable */
+/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */
+
 //= vue
 //= vue-resource
 //= require jquery
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 944df6d23f7bc4c3b683f97cc21df24c909fcb9e..a8874ab12d3e1fada4730b141bd500c097ee8f91 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */
+/* global MockU2FDevice */
+/* global U2FAuthenticate */
 
 /*= require u2f/authenticate */
 /*= require u2f/util */
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 0c73c5772bd09470d7c7776e4ef49932b29d56ac..189592ea87a89d528c76043f1bd29a4a4d87344d 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,4 +1,6 @@
-/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */
+/* global MockU2FDevice */
+/* global U2FRegister */
 
 /*= require u2f/register */
 /*= require u2f/util */
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index b9acaaa5a0da306c6f57c6746b34e39dc64c1268..d80ce5a7f7e3a546cb6213027923d13db5fd5424 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,4 +1,7 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-undef, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
+/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */
+/* global Dropzone */
+/* global Mousetrap */
+/* global ZenMode */
 
 /*= require zen_mode */
 
diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3fe2c7f5d5d5ef5244873b358fda3d12b8a420c2
--- /dev/null
+++ b/spec/lib/banzai/filter/math_filter_spec.rb
@@ -0,0 +1,120 @@
+require 'spec_helper'
+
+describe Banzai::Filter::MathFilter, lib: true do
+  include FilterSpecHelper
+
+  it 'leaves regular inline code unchanged' do
+    input = "<code>2+2</code>"
+    doc = filter(input)
+
+    expect(doc.to_s).to eq input
+  end
+
+  it 'removes surrounding dollar signs and adds class code, math and js-render-math' do
+    doc = filter("$<code>2+2</code>$")
+
+    expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>'
+  end
+
+  it 'only removes surrounding dollar signs' do
+    doc = filter("test $<code>2+2</code>$ test")
+    before = doc.xpath('descendant-or-self::text()[1]').first
+    after = doc.xpath('descendant-or-self::text()[3]').first
+
+    expect(before.to_s).to eq 'test '
+    expect(after.to_s).to eq ' test'
+  end
+
+  it 'only removes surrounding single dollar sign' do
+    doc = filter("test $$<code>2+2</code>$$ test")
+    before = doc.xpath('descendant-or-self::text()[1]').first
+    after = doc.xpath('descendant-or-self::text()[3]').first
+
+    expect(before.to_s).to eq 'test $'
+    expect(after.to_s).to eq '$ test'
+  end
+
+  it 'adds data-math-style inline attribute to inline math' do
+    doc = filter('$<code>2+2</code>$')
+    code = doc.xpath('descendant-or-self::code').first
+
+    expect(code['data-math-style']).to eq 'inline'
+  end
+
+  it 'adds class code and math to inline math' do
+    doc = filter('$<code>2+2</code>$')
+    code = doc.xpath('descendant-or-self::code').first
+
+    expect(code[:class]).to include("code")
+    expect(code[:class]).to include("math")
+  end
+
+  it 'adds js-render-math class to inline math' do
+    doc = filter('$<code>2+2</code>$')
+    code = doc.xpath('descendant-or-self::code').first
+
+    expect(code[:class]).to include("js-render-math")
+  end
+
+  # Cases with faulty syntax. Should be a no-op
+
+  it 'ignores cases with missing dolar sign at the end' do
+    input = "test $<code>2+2</code> test"
+    doc = filter(input)
+
+    expect(doc.to_s).to eq input
+  end
+
+  it 'ignores cases with missing dolar sign at the beginning' do
+    input = "test <code>2+2</code>$ test"
+    doc = filter(input)
+
+    expect(doc.to_s).to eq input
+  end
+
+  it 'ignores dollar signs if it is not adjacent' do
+    input = '<p>We check strictly $<code>2+2</code> and  <code>2+2</code>$ </p>'
+    doc = filter(input)
+
+    expect(doc.to_s).to eq input
+  end
+
+  # Display math
+
+  it 'adds data-math-style display attribute to display math' do
+    doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+    pre = doc.xpath('descendant-or-self::pre').first
+
+    expect(pre['data-math-style']).to eq 'display'
+  end
+
+  it 'adds js-render-math class to display math' do
+    doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>')
+    pre = doc.xpath('descendant-or-self::pre').first
+
+    expect(pre[:class]).to include("js-render-math")
+  end
+
+  it 'ignores code blocks that are not math' do
+    input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>'
+    doc = filter(input)
+
+    expect(doc.to_s).to eq input
+  end
+
+  it 'requires the pre to contain both code and math' do
+    input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>'
+    doc = filter(input)
+
+    expect(doc.to_s).to eq input
+  end
+
+  it 'dollar signs around to display math' do
+    doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$')
+    before = doc.xpath('descendant-or-self::text()[1]').first
+    after = doc.xpath('descendant-or-self::text()[3]').first
+
+    expect(before.to_s).to eq '$'
+    expect(after.to_s).to eq '$'
+  end
+end
diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..015a7f80e03441d932696ff7f530c5e37f698d96
--- /dev/null
+++ b/spec/lib/bitbucket/collection_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+# Emulates paginator. It returns 2 pages with results
+class TestPaginator
+  def initialize
+    @current_page = 0
+  end
+
+  def items
+    @current_page += 1
+
+    raise StopIteration if @current_page > 2
+
+    ["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"]
+  end
+end
+
+describe Bitbucket::Collection do
+  it "iterates paginator" do
+    collection = described_class.new(TestPaginator.new)
+
+    expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"])
+  end
+end
diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..14faeb231a980cdc53130d71d1e5c1ba0842eba8
--- /dev/null
+++ b/spec/lib/bitbucket/connection_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Bitbucket::Connection do
+  before do
+    allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: ''))
+  end
+
+  describe '#get' do
+    it 'calls OAuth2::AccessToken::get' do
+      expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true))
+
+      connection = described_class.new({})
+
+      connection.get('/users')
+    end
+  end
+
+  describe '#expired?' do
+    it 'calls connection.expired?' do
+      expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true)
+
+      expect(described_class.new({}).expired?).to be_truthy
+    end
+  end
+
+  describe '#refresh!' do
+    it 'calls connection.refresh!' do
+      response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil)
+
+      expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response)
+
+      described_class.new({}).refresh!
+    end
+  end
+end
diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..04d5a0470b1ca568861c2ddda714855c1d716498
--- /dev/null
+++ b/spec/lib/bitbucket/page_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Bitbucket::Page do
+  let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } }
+
+  before do
+    # Autoloading hack
+    Bitbucket::Representation::User.new({})
+  end
+
+  describe '#items' do
+    it 'returns collection of needed objects' do
+      page = described_class.new(response, :user)
+
+      expect(page.items.first).to be_a(Bitbucket::Representation::User)
+      expect(page.items.count).to eq(1)
+    end
+  end
+
+  describe '#attrs' do
+    it 'returns attributes' do
+      page = described_class.new(response, :user)
+
+      expect(page.attrs.keys).to include(:pagelen, :next)
+    end
+  end
+
+  describe '#next?' do
+    it 'returns true' do
+      page = described_class.new(response, :user)
+
+      expect(page.next?).to be_truthy
+    end
+
+    it 'returns false' do
+      response['next'] = nil
+      page = described_class.new(response, :user)
+
+      expect(page.next?).to be_falsey
+    end
+  end
+
+  describe '#next' do
+    it 'returns next attribute' do
+      page = described_class.new(response, :user)
+
+      expect(page.next).to eq('')
+    end
+  end
+end
diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2c972da682e99d5d9511062a0fcca89818b477f4
--- /dev/null
+++ b/spec/lib/bitbucket/paginator_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Bitbucket::Paginator do
+  let(:last_page) { double(:page, next?: false, items: ['item_2']) }
+  let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) }
+
+  describe 'items' do
+    it 'return items and raises StopIteration in the end' do
+      paginator = described_class.new(nil, nil, nil)
+
+      allow(paginator).to receive(:fetch_next_page).and_return(first_page)
+      expect(paginator.items).to match(['item_1'])
+
+      allow(paginator).to receive(:fetch_next_page).and_return(last_page)
+      expect(paginator.items).to match(['item_2'])
+
+      allow(paginator).to receive(:fetch_next_page).and_return(nil)
+      expect{ paginator.items }.to raise_error(StopIteration)
+    end
+  end
+end
diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fec243a9f9603f622a5439788234a6bbc04d7a1f
--- /dev/null
+++ b/spec/lib/bitbucket/representation/comment_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::Comment do
+  describe '#author' do
+    it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') }
+    it { expect(described_class.new({}).author).to be_nil }
+  end
+
+  describe '#note' do
+    it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') }
+    it { expect(described_class.new({}).note).to be_nil }
+  end
+
+  describe '#created_at' do
+    it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
+  end
+
+  describe '#updated_at' do
+    it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) }
+    it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) }
+  end
+end
diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..20f47224aa8e44ab04f4c2cf65b6148586d040a9
--- /dev/null
+++ b/spec/lib/bitbucket/representation/issue_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::Issue do
+  describe '#iid' do
+    it { expect(described_class.new('id' => 1).iid).to eq(1) }
+  end
+
+  describe '#kind' do
+    it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') }
+  end
+
+  describe '#milestone' do
+    it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') }
+    it { expect(described_class.new({}).milestone).to be_nil }
+  end
+
+  describe '#author' do
+    it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+    it { expect(described_class.new({}).author).to be_nil }
+  end
+
+  describe '#description' do
+    it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') }
+    it { expect(described_class.new({}).description).to be_nil }
+  end
+
+  describe '#state' do
+    it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') }
+    it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') }
+    it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') }
+    it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') }
+    it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') }
+    it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') }
+  end
+
+  describe '#title' do
+    it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
+  end
+
+  describe '#created_at' do
+    it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) }
+  end
+
+  describe '#updated_at' do
+    it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) }
+  end
+end
diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..673dcf22ce869892bdbcae24dd2c344e2d0b5893
--- /dev/null
+++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::PullRequestComment do
+  describe '#iid' do
+    it { expect(described_class.new('id' => 1).iid).to eq(1) }
+  end
+
+  describe '#file_path' do
+    it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') }
+  end
+
+  describe '#old_pos' do
+    it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) }
+  end
+
+  describe '#new_pos' do
+    it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) }
+  end
+
+  describe '#parent_id' do
+    it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) }
+    it { expect(described_class.new({}).parent_id).to be_nil }
+  end
+
+  describe '#inline?' do
+    it { expect(described_class.new('inline' => {}).inline?).to be_truthy }
+    it { expect(described_class.new({}).inline?).to be_falsey }
+  end
+
+  describe '#has_parent?' do
+    it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy }
+    it { expect(described_class.new({}).has_parent?).to be_falsey }
+  end
+end
diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..30453528be4c2d491920c3945e1afe119d156304
--- /dev/null
+++ b/spec/lib/bitbucket/representation/pull_request_spec.rb
@@ -0,0 +1,47 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::PullRequest do
+  describe '#iid' do
+    it { expect(described_class.new('id' => 1).iid).to eq(1) }
+  end
+
+  describe '#author' do
+    it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') }
+    it { expect(described_class.new({}).author).to be_nil }
+  end
+
+  describe '#description' do
+    it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') }
+    it { expect(described_class.new({}).description).to be_nil }
+  end
+
+  describe '#state' do
+    it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') }
+    it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') }
+    it { expect(described_class.new({}).state).to eq('opened') }
+  end
+
+  describe '#title' do
+    it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') }
+  end
+
+  describe '#source_branch_name' do
+    it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') }
+    it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil }
+  end
+
+  describe '#source_branch_sha' do
+    it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') }
+    it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil }
+  end
+
+  describe '#target_branch_name' do
+    it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') }
+    it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil }
+  end
+
+  describe '#target_branch_sha' do
+    it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') }
+    it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil }
+  end
+end
diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f79ff4edb7bd88d7d2268945b867d2397041c182
--- /dev/null
+++ b/spec/lib/bitbucket/representation/user_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe Bitbucket::Representation::User do
+  describe '#username' do
+    it 'returns correct value' do
+      user = described_class.new('username' => 'Ben')
+
+      expect(user.username).to eq('Ben')
+    end
+  end
+end
diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb
index 4aba783dc334a1732bc2d9f01e9dbbc43155988b..f3843ca64ff6fb5c9008b7ace6efeb9e3d142055 100644
--- a/spec/lib/gitlab/asciidoc_spec.rb
+++ b/spec/lib/gitlab/asciidoc_spec.rb
@@ -11,7 +11,7 @@ module Gitlab
       it "converts the input using Asciidoctor and default options" do
         expected_asciidoc_opts = {
             safe: :secure,
-            backend: :html5,
+            backend: :gitlab_html5,
             attributes: described_class::DEFAULT_ADOC_ATTRS
         }
 
@@ -27,7 +27,7 @@ module Gitlab
         it "merges the options with default ones" do
           expected_asciidoc_opts = {
               safe: :safe,
-              backend: :html5,
+              backend: :gitlab_html5,
               attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo']
           }
 
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index c9d64e99f88f2d8756d9765c0241dd3e02b2af43..f251c0dd25a1d8cac3080d26e096a52b37627b6e 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -47,54 +47,76 @@ describe Gitlab::Auth, lib: true do
       project.create_drone_ci_service(active: true)
       project.drone_ci_service.update(token: 'token')
 
-      ip = 'ip'
-
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token')
-      expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
+      expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token')
+      expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities))
     end
 
     it 'recognizes master passwords' do
       user = create(:user, password: 'password')
-      ip = 'ip'
 
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
-      expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
+      expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+      expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities))
     end
 
     it 'recognizes user lfs tokens' do
       user = create(:user)
-      ip = 'ip'
       token = Gitlab::LfsToken.new(user).token
 
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
-      expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
+      expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username)
+      expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
     end
 
     it 'recognizes deploy key lfs tokens' do
       key = create(:deploy_key)
-      ip = 'ip'
       token = Gitlab::LfsToken.new(key).token
 
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
-      expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
+      expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}")
+      expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
     end
 
-    it 'recognizes OAuth tokens' do
-      user = create(:user)
-      application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
-      token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
-      ip = 'ip'
+    context "while using OAuth tokens as passwords" do
+      it 'succeeds for OAuth tokens with the `api` scope' do
+        user = create(:user)
+        application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+        token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
+
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2')
+        expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+      end
 
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2')
-      expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities))
+      it 'fails for OAuth tokens with other scopes' do
+        user = create(:user)
+        application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+        token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user")
+
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2')
+        expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+      end
+    end
+
+    context "while using personal access tokens as passwords" do
+      it 'succeeds for personal access tokens with the `api` scope' do
+        user = create(:user)
+        personal_access_token = create(:personal_access_token, user: user, scopes: ['api'])
+
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email)
+        expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities))
+      end
+
+      it 'fails for personal access tokens with other scopes' do
+        user = create(:user)
+        personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+
+        expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email)
+        expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil))
+      end
     end
 
     it 'returns double nil for invalid credentials' do
       login = 'foo'
-      ip = 'ip'
 
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
-      expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
+      expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login)
+      expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new)
     end
   end
 
diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb
index 38eebb2a1769b2a25656e77b3c4239c08e9e25ca..70f03021d361f1cc965cf6b4eda6ad93fcde2b56 100644
--- a/spec/lib/gitlab/badge/build/status_spec.rb
+++ b/spec/lib/gitlab/badge/build/status_spec.rb
@@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do
         new_build.success!
       end
 
-      it 'reports the compound status' do
-        expect(badge.status).to eq 'failed'
+      it 'does not take outdated pipeline into account' do
+        expect(badge.status).to eq 'success'
       end
     end
   end
diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb
deleted file mode 100644
index 7543c29bcc449f5f94551b4cfc90f497415cfa5d..0000000000000000000000000000000000000000
--- a/spec/lib/gitlab/bitbucket_import/client_spec.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::BitbucketImport::Client, lib: true do
-  include ImportSpecHelper
-
-  let(:token) { '123456' }
-  let(:secret) { 'secret' }
-  let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) }
-
-  before do
-    stub_omniauth_provider('bitbucket')
-  end
-
-  it 'all OAuth client options are symbols' do
-    client.consumer.options.keys.each do |key|
-      expect(key).to be_kind_of(Symbol)
-    end
-  end
-
-  context 'issues' do
-    let(:per_page) { 50 }
-    let(:count) { 95 }
-    let(:sample_issues) do
-      issues = []
-
-      count.times do |i|
-        issues << { local_id: i }
-      end
-
-      issues
-    end
-    let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } }
-    let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } }
-    let(:project_id) { 'namespace/repo' }
-
-    it 'retrieves issues over a number of pages' do
-      stub_request(:get,
-                   "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0").
-        to_return(status: 200,
-                  body: first_sample_data.to_json,
-                  headers: {})
-
-      stub_request(:get,
-                   "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50").
-        to_return(status: 200,
-                  body: second_sample_data.to_json,
-                  headers: {})
-
-      issues = client.issues(project_id)
-      expect(issues.count).to eq(95)
-    end
-  end
-
-  context 'project import' do
-    it 'calls .from_project with no errors' do
-      project = create(:empty_project)
-      project.import_url = "ssh://git@bitbucket.org/test/test.git"
-      project.create_or_update_import_data(credentials:
-                                             { user: "git",
-                                               password: nil,
-                                               bb_session: { bitbucket_access_token: "test",
-                                                             bitbucket_access_token_secret: "test" } })
-
-      expect { described_class.from_project(project) }.not_to raise_error
-    end
-  end
-end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index aa00f32becbec1861e12a5cc6db056add55a804b..53f3c73ade487da734ea8dafda8c6668a2bb593b 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -18,15 +18,21 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
       "closed"  # undocumented status
     ]
   end
+
   let(:sample_issues_statuses) do
     issues = []
 
     statuses.map.with_index do |status, index|
       issues << {
-        local_id: index,
-        status: status,
+        id: index,
+        state: status,
         title: "Issue #{index}",
-        content: "Some content to issue #{index}"
+        kind: 'bug',
+        content: {
+            raw: "Some content to issue #{index}",
+            markup: "markdown",
+            html: "Some content to issue #{index}"
+        }
       }
     end
 
@@ -34,14 +40,16 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
   end
 
   let(:project_identifier) { 'namespace/repo' }
+
   let(:data) do
     {
       'bb_session' => {
-        'bitbucket_access_token' => "123456",
-        'bitbucket_access_token_secret' => "secret"
+        'bitbucket_token' => "123456",
+        'bitbucket_refresh_token' => "secret"
       }
     }
   end
+
   let(:project) do
     create(
       :project,
@@ -49,11 +57,13 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
       import_data: ProjectImportData.new(credentials: data)
     )
   end
+
   let(:importer) { Gitlab::BitbucketImport::Importer.new(project) }
+
   let(:issues_statuses_sample_data) do
     {
       count: sample_issues_statuses.count,
-      issues: sample_issues_statuses
+      values: sample_issues_statuses
     }
   end
 
@@ -61,26 +71,46 @@ describe Gitlab::BitbucketImport::Importer, lib: true do
     before do
       stub_request(
         :get,
-        "https://bitbucket.org/api/1.0/repositories/#{project_identifier}"
-      ).to_return(status: 200, body: { has_issues: true }.to_json)
+        "https://api.bitbucket.org/2.0/repositories/#{project_identifier}"
+      ).to_return(status: 200,
+                  headers: { "Content-Type" => "application/json" },
+                  body: { has_issues: true, full_name: project_identifier }.to_json)
 
       stub_request(
         :get,
-        "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0"
-      ).to_return(status: 200, body: issues_statuses_sample_data.to_json)
+        "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on"
+      ).to_return(status: 200,
+                  headers: { "Content-Type" => "application/json" },
+                  body: issues_statuses_sample_data.to_json)
+
+      stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on").
+         with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }).
+         to_return(status: 200,
+                   body: "",
+                   headers: {})
 
       sample_issues_statuses.each_with_index do |issue, index|
         stub_request(
           :get,
-          "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments"
+          "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on"
         ).to_return(
           status: 200,
-          body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json
+          headers: { "Content-Type" => "application/json" },
+          body: { author_info: { username: "username" }, utc_created_on: index }.to_json
         )
       end
+
+      stub_request(
+        :get,
+        "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL"
+      ).to_return(status: 200,
+                  headers: { "Content-Type" => "application/json" },
+                  body: {}.to_json)
     end
 
     it 'map statuses to open or closed' do
+      # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this
+      Bitbucket::Representation::Issue.new({})
       importer.execute
 
       expect(project.issues.where(state: "closed").size).to eq(5)
diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
index e1c60e07b4d656f9b54dd7df45eac9aa69c4d191..b6d052a4612b00aca993f02fdf52a97b04150b64 100644
--- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb
@@ -2,14 +2,18 @@ require 'spec_helper'
 
 describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
   let(:user) { create(:user) }
+
   let(:repo) do
-    {
-      name: 'Vim',
-      slug: 'vim',
-      is_private: true,
-      owner: "asd"
-    }.with_indifferent_access
+    double(name: 'Vim',
+           slug: 'vim',
+           description: 'Test repo',
+           is_private: true,
+           owner: "asd",
+           full_name: 'Vim repo',
+           visibility_level: Gitlab::VisibilityLevel::PRIVATE,
+           clone_url: 'ssh://git@bitbucket.org/asd/vim.git')
   end
+
   let(:namespace){ create(:group, owner: user) }
   let(:token) { "asdasd12345" }
   let(:secret) { "sekrettt" }
@@ -22,7 +26,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do
   it 'creates project' do
     allow_any_instance_of(Project).to receive(:add_import_job)
 
-    project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params)
+    project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params)
     project = project_creator.execute
 
     expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git")
diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f6288011494be13af46e00feeb1c02a379c5220b
--- /dev/null
+++ b/spec/lib/gitlab/checks/force_push_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::Checks::ChangeAccess, lib: true do
+  let(:project) { create(:project) }
+
+  context "exit code checking" do
+    it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do
+      allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0])
+
+      expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error
+    end
+
+    it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do
+      allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
+
+      expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
index d619e401897ebda7f8e7c12b637ea6e64aac2616..f4703dc704f565ca1ca3c09db00d010bbce58328 100644
--- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
+++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb
@@ -29,7 +29,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
       context 'description with ignored elements' do
         let(:text) do
           "Hi. This references #1, but not `#2`\n" +
-          '<pre>and not !1</pre>'
+            '<pre>and not !1</pre>'
         end
 
         it { is_expected.to include issue_first.to_reference(new_project) }
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..444639acbaab5c40e6ad3e24749a78f665535f56
--- /dev/null
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe Gitlab::Git::RevList, lib: true do
+  let(:project) { create(:project) }
+
+  context "validations" do
+    described_class::ALLOWED_VARIABLES.each do |var|
+      context var do
+        it "accepts values starting with the project repo path" do
+          env = { var => "#{project.repository.path_to_repo}/objects" }
+          rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
+
+          expect(rev_list).to be_valid
+        end
+
+        it "rejects values starting not with the project repo path" do
+          env = { var => "/some/other/path" }
+          rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
+
+          expect(rev_list).not_to be_valid
+        end
+
+        it "rejects values containing the project repo path but not starting with it" do
+          env = { var => "/some/other/path/#{project.repository.path_to_repo}" }
+          rev_list = described_class.new('oldrev', 'newrev', project: project, env: env)
+
+          expect(rev_list).not_to be_valid
+        end
+      end
+    end
+  end
+
+  context "#execute" do
+    let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } }
+    let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) }
+
+    it "calls out to `popen` without environment variables if the record is invalid" do
+      allow(rev_list).to receive(:valid?).and_return(false)
+
+      expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args)
+
+      rev_list.execute
+    end
+
+    it "calls out to `popen` with environment variables if the record is valid" do
+      allow(rev_list).to receive(:valid?).and_return(true)
+
+      expect(Open3).to receive(:popen3).with(hash_including(env), any_args)
+
+      rev_list.execute
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 8e1a28f272346c1c2ed3275c4eba32ad4a8c8ab8..9b49d6837c39010552fbc1d0bd9b1d112102bc35 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -136,7 +136,8 @@ project:
 - assembla_service
 - asana_service
 - gemnasium_service
-- slack_service
+- slack_notification_service
+- mattermost_notification_service
 - buildkite_service
 - bamboo_service
 - teamcity_service
@@ -147,6 +148,7 @@ project:
 - bugzilla_service
 - gitlab_issue_tracker_service
 - external_wiki_service
+- kubernetes_service
 - forked_project_link
 - forked_from_project
 - forked_project_links
diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
index 5ae178414cc240f529a55e96e557b4c64956f61d..08a42fd27a255bf1577adc61f75b5695734ad3ff 100644
--- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb
@@ -1,12 +1,14 @@
 require 'spec_helper'
 
 describe Gitlab::ImportExport::AvatarRestorer, lib: true do
+  include UploadHelpers
+
   let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') }
   let(:project) { create(:empty_project) }
 
   before do
     allow_any_instance_of(described_class).to receive(:avatar_export_file)
-                                                .and_return(Rails.root + "spec/fixtures/dk.png")
+                                                .and_return(uploaded_image_temp_path)
   end
 
   after do
diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ab1ab22795c90b4b69b918181d0cf9b21403d183
--- /dev/null
+++ b/spec/lib/gitlab/middleware/multipart_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+require 'tempfile'
+
+describe Gitlab::Middleware::Multipart do
+  let(:app) { double(:app) }
+  let(:middleware) { described_class.new(app) }
+
+  it 'opens top-level files' do
+    Tempfile.open('top-level') do |tempfile|
+      env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+      expect(app).to receive(:call) do |env|
+        file = Rack::Request.new(env).params['file']
+        expect(file).to be_a(File)
+        expect(file.path).to eq(tempfile.path)
+      end
+
+      middleware.call(env)
+    end
+  end
+
+  it 'rejects headers signed with the wrong secret' do
+    env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse')
+
+    expect { middleware.call(env) }.to raise_error(JWT::VerificationError)
+  end
+
+  it 'rejects headers signed with the wrong issuer' do
+    env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc')
+
+    expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError)
+  end
+
+  it 'opens files one level deep' do
+    Tempfile.open('one-level') do |tempfile|
+      in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } }
+      env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+      expect(app).to receive(:call) do |env|
+        file = Rack::Request.new(env).params['user']['avatar']
+        expect(file).to be_a(File)
+        expect(file.path).to eq(tempfile.path)
+      end
+
+      middleware.call(env)
+    end
+  end
+
+  it 'opens files two levels deep' do
+    Tempfile.open('two-levels') do |tempfile|
+      in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } }
+      env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse')
+
+      expect(app).to receive(:call) do |env|
+        file = Rack::Request.new(env).params['project']['milestone']['themesong']
+        expect(file).to be_a(File)
+        expect(file.path).to eq(tempfile.path)
+      end
+
+      middleware.call(env)
+    end
+  end
+
+  def post_env(rewritten_fields, params, secret, issuer)
+    token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256')
+    Rack::MockRequest.env_for(
+      '/',
+      method: 'post',
+      params: params,
+      described_class::RACK_ENV_KEY => token
+    )
+  end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 3cd9863ec6ae4a3092ffa2298d7e6ab0391f39b3..14ee386dba6c26fbb9d75f7651f01febf6bd560d 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -149,4 +149,33 @@ describe Gitlab::ProjectSearchResults, lib: true do
       expect(results.issues_count).to eq 3
     end
   end
+
+  describe 'notes search' do
+    it 'lists notes' do
+      project = create(:empty_project, :public)
+      note = create(:note, project: project)
+
+      results = described_class.new(user, project, note.note)
+
+      expect(results.objects('notes')).to include note
+    end
+
+    it "doesn't list issue notes when access is restricted" do
+      project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE)
+      note = create(:note_on_issue, project: project)
+
+      results = described_class.new(user, project, note.note)
+
+      expect(results.objects('notes')).not_to include note
+    end
+
+    it "doesn't list merge_request notes when access is restricted" do
+      project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE)
+      note = create(:note_on_merge_request, project: project)
+
+      results = described_class.new(user, project, note.note)
+
+      expect(results.objects('notes')).not_to include note
+    end
+  end
 end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index c51b10bdc69b5870673b06cd998938dc6149ea6a..c78cd30157e5e8ed3264bf351a9965520c1d905d 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do
   describe 'file path regex' do
     it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) }
   end
+
+  describe 'environment slug regex' do
+    def be_matched
+      match(Gitlab::Regex.environment_slug_regex)
+    end
+
+    it { expect('foo').to be_matched }
+    it { expect('foo-1').to be_matched }
+
+    it { expect('FOO').not_to be_matched }
+    it { expect('foo/1').not_to be_matched }
+    it { expect('foo.1').not_to be_matched }
+    it { expect('foo*1').not_to be_matched }
+    it { expect('9foo').not_to be_matched }
+    it { expect('foo-').not_to be_matched }
+  end
 end
diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb
index 0cdbab8754417a4a5d4494718186372f7d42d801..849edb094761ed49a7cc1f7cf660a2c5fd864007 100644
--- a/spec/lib/gitlab/sql/union_spec.rb
+++ b/spec/lib/gitlab/sql/union_spec.rb
@@ -1,16 +1,26 @@
 require 'spec_helper'
 
 describe Gitlab::SQL::Union, lib: true do
+  let(:relation_1) { User.where(email: 'alice@example.com').select(:id) }
+  let(:relation_2) { User.where(email: 'bob@example.com').select(:id) }
+
+  def to_sql(relation)
+    relation.reorder(nil).to_sql
+  end
+
   describe '#to_sql' do
     it 'returns a String joining relations together using a UNION' do
-      rel1  = User.where(email: 'alice@example.com')
-      rel2  = User.where(email: 'bob@example.com')
-      union = described_class.new([rel1, rel2])
+      union = described_class.new([relation_1, relation_2])
+
+      expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
+    end
 
-      sql1 = rel1.reorder(nil).to_sql
-      sql2 = rel2.reorder(nil).to_sql
+    it 'skips Model.none segements' do
+      empty_relation = User.none
+      union = described_class.new([empty_relation, relation_1, relation_2])
 
-      expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}")
+      expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error
+      expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}")
     end
   end
 end
diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3c2eddbd22108f4cc3051fe1774feab4609b107b
--- /dev/null
+++ b/spec/lib/mattermost/session_spec.rb
@@ -0,0 +1,99 @@
+require 'spec_helper'
+
+describe Mattermost::Session, type: :request do
+  let(:user) { create(:user) }
+
+  let(:gitlab_url) { "http://gitlab.com" }
+  let(:mattermost_url) { "http://mattermost.com" }
+
+  subject { described_class.new(user) }
+
+  # Needed for doorkeeper to function
+  it { is_expected.to respond_to(:current_resource_owner) }
+  it { is_expected.to respond_to(:request) }
+  it { is_expected.to respond_to(:authorization) }
+  it { is_expected.to respond_to(:strategy) }
+
+  before do
+    described_class.base_uri(mattermost_url)
+  end
+
+  describe '#with session' do
+    let(:location) { 'http://location.tld' }
+    let!(:stub) do
+      WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login").
+        to_return(headers: { 'location' => location }, status: 307)
+    end
+
+    context 'without oauth uri' do
+      it 'makes a request to the oauth uri' do
+        expect { subject.with_session }.to raise_error(Mattermost::NoSessionError)
+      end
+    end
+
+    context 'with oauth_uri' do
+      let!(:doorkeeper) do
+        Doorkeeper::Application.create(
+          name: "GitLab Mattermost",
+          redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete",
+          scopes: "")
+      end
+
+      context 'without token_uri' do
+        it 'can not create a session' do
+          expect do
+            subject.with_session
+          end.to raise_error(Mattermost::NoSessionError)
+        end
+      end
+
+      context 'with token_uri' do
+        let(:state) { "state" }
+        let(:params) do
+          { response_type: "code",
+            client_id: doorkeeper.uid,
+            redirect_uri: "#{mattermost_url}/signup/gitlab/complete",
+            state: state }
+        end
+        let(:location) do
+          "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}"
+        end
+
+        before do
+          WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete").
+            with(query: hash_including({ 'state' => state })).
+            to_return do |request|
+              post "/oauth/token",
+                client_id: doorkeeper.uid,
+                client_secret: doorkeeper.secret,
+                redirect_uri: params[:redirect_uri],
+                grant_type: 'authorization_code',
+                code: request.uri.query_values['code']
+
+              if response.status == 200
+                { headers: { 'token' => 'thisworksnow' }, status: 202 }
+              end
+            end
+
+          WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout").
+            to_return(headers: { Authorization: 'token thisworksnow' }, status: 200)
+        end
+
+        it 'can setup a session' do
+          subject.with_session do |session|
+          end
+
+          expect(subject.token).not_to be_nil
+        end
+
+        it 'returns the value of the block' do
+          result = subject.with_session do |session|
+            "value"
+          end
+
+          expect(result).to eq("value")
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 7f39aff7639ec9bdd30df93b8e858f161588d344..6f1c2ae0fd84da0a9545b9193fc80cdd5131f2ba 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -87,6 +87,26 @@ describe Ci::Build, models: true do
     end
   end
 
+  describe '#persisted_environment' do
+    before do
+      @environment = create(:environment, project: project, name: "foo-#{project.default_branch}")
+    end
+
+    subject { build.persisted_environment }
+
+    context 'referenced literally' do
+      let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") }
+
+      it { is_expected.to eq(@environment) }
+    end
+
+    context 'referenced with a variable' do
+      let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") }
+
+      it { is_expected.to eq(@environment) }
+    end
+  end
+
   describe '#trace' do
     it { expect(build.trace).to be_nil }
 
@@ -254,6 +274,24 @@ describe Ci::Build, models: true do
     end
   end
 
+  describe '#ref_slug' do
+    {
+      'master'    => 'master',
+      '1-foo'     => '1-foo',
+      'fix/1-foo' => 'fix-1-foo',
+      'fix-1-foo' => 'fix-1-foo',
+      'a' * 63    => 'a' * 63,
+      'a' * 64    => 'a' * 63,
+      'FOO'       => 'foo',
+    }.each do |ref, slug|
+      it "transforms #{ref} to #{slug}" do
+        build.ref = ref
+
+        expect(build.ref_slug).to eq(slug)
+      end
+    end
+  end
+
   describe '#variables' do
     let(:container_registry_enabled) { false }
     let(:predefined_variables) do
@@ -265,6 +303,7 @@ describe Ci::Build, models: true do
         { key: 'CI_BUILD_REF', value: build.sha, public: true },
         { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
         { key: 'CI_BUILD_REF_NAME', value: 'master', public: true },
+        { key: 'CI_BUILD_REF_SLUG', value: 'master', public: true },
         { key: 'CI_BUILD_NAME', value: 'test', public: true },
         { key: 'CI_BUILD_STAGE', value: 'test', public: true },
         { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
@@ -309,6 +348,22 @@ describe Ci::Build, models: true do
       it { user_variables.each { |v| is_expected.to include(v) } }
     end
 
+    context 'when build has an environment' do
+      before do
+        build.update(environment: 'production')
+        create(:environment, project: build.project, name: 'production', slug: 'prod-slug')
+      end
+
+      let(:environment_variables) do
+        [
+          { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true },
+          { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug',  public: true }
+        ]
+      end
+
+      it { environment_variables.each { |v| is_expected.to include(v) } }
+    end
+
     context 'when build started manually' do
       before do
         build.update_attributes(when: :manual)
@@ -451,6 +506,17 @@ describe Ci::Build, models: true do
       it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) }
     end
 
+    context 'when build is for a deployment' do
+      let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } }
+
+      before do
+        build.environment = 'production'
+        allow(project).to receive(:deployment_variables).and_return([deployment_variable])
+      end
+
+      it { is_expected.to include(deployment_variable) }
+    end
+
     context 'returns variables in valid order' do
       before do
         allow(build).to receive(:predefined_variables) { ['predefined'] }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index e78ae14b7377497795d8f01aeb6def1cec2424d4..52dd41065e9d840107a4f2fa8531860804c6dcf5 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -381,6 +381,65 @@ describe Ci::Pipeline, models: true do
     end
   end
 
+  shared_context 'with some outdated pipelines' do
+    before do
+      create_pipeline(:canceled, 'ref', 'A')
+      create_pipeline(:success, 'ref', 'A')
+      create_pipeline(:failed, 'ref', 'B')
+      create_pipeline(:skipped, 'feature', 'C')
+    end
+
+    def create_pipeline(status, ref, sha)
+      create(:ci_empty_pipeline, status: status, ref: ref, sha: sha)
+    end
+  end
+
+  describe '.latest' do
+    include_context 'with some outdated pipelines'
+
+    context 'when no ref is specified' do
+      let(:pipelines) { described_class.latest.all }
+
+      it 'returns the latest pipeline for the same ref and different sha' do
+        expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C')
+        expect(pipelines.map(&:status)).
+          to contain_exactly('success', 'failed', 'skipped')
+      end
+    end
+
+    context 'when ref is specified' do
+      let(:pipelines) { described_class.latest('ref').all }
+
+      it 'returns the latest pipeline for ref and different sha' do
+        expect(pipelines.map(&:sha)).to contain_exactly('A', 'B')
+        expect(pipelines.map(&:status)).
+          to contain_exactly('success', 'failed')
+      end
+    end
+  end
+
+  describe '.latest_status' do
+    include_context 'with some outdated pipelines'
+
+    context 'when no ref is specified' do
+      let(:latest_status) { described_class.latest_status }
+
+      it 'returns the latest status for the same ref and different sha' do
+        expect(latest_status).to eq(described_class.latest.status)
+        expect(latest_status).to eq('failed')
+      end
+    end
+
+    context 'when ref is specified' do
+      let(:latest_status) { described_class.latest_status('ref') }
+
+      it 'returns the latest status for ref and different sha' do
+        expect(latest_status).to eq(described_class.latest_status('ref'))
+        expect(latest_status).to eq('failed')
+      end
+    end
+  end
+
   describe '#status' do
     let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') }
 
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 0935fc0561cbfe22d3f39b91d11acc46fd00ea88..74b50d2908dcf0c9ad94b26f67f0de87892ffae1 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -213,23 +213,19 @@ eos
   end
 
   describe '#status' do
-    context 'without arguments for compound status' do
-      shared_examples 'giving the status from pipeline' do
-        it do
-          expect(commit.status).to eq(Ci::Pipeline.status)
-        end
-      end
-
-      context 'with pipelines' do
-        let!(:pipeline) do
-          create(:ci_empty_pipeline, project: project, sha: commit.sha)
+    context 'without ref argument' do
+      before do
+        %w[success failed created pending].each do |status|
+          create(:ci_empty_pipeline,
+                 project: project,
+                 sha: commit.sha,
+                 status: status)
         end
-
-        it_behaves_like 'giving the status from pipeline'
       end
 
-      context 'without pipelines' do
-        it_behaves_like 'giving the status from pipeline'
+      it 'gives compound status from latest pipelines' do
+        expect(commit.status).to eq(Ci::Pipeline.latest_status)
+        expect(commit.status).to eq('pending')
       end
     end
 
@@ -255,8 +251,9 @@ eos
         expect(commit.status('fix')).to eq(pipeline_from_fix.status)
       end
 
-      it 'gives compound status if ref is nil' do
-        expect(commit.status(nil)).to eq(commit.status)
+      it 'gives compound status from latest pipelines if ref is nil' do
+        expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status)
+        expect(commit.status(nil)).to eq('failed')
       end
     end
   end
diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb
index eb64f3d0c83291796004dcf70468e546bd291bed..4b0bfa43abf0c27b7d617c34dab146d011927c1d 100644
--- a/spec/models/concerns/token_authenticatable_spec.rb
+++ b/spec/models/concerns/token_authenticatable_spec.rb
@@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do
     it { expect(described_class).to be_private_method_defined(:write_new_token) }
     it { expect(described_class).to respond_to("find_by_#{token_field}") }
     it { is_expected.to respond_to("ensure_#{token_field}") }
+    it { is_expected.to respond_to("set_#{token_field}") }
     it { is_expected.to respond_to("reset_#{token_field}!") }
   end
 end
@@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do
     end
   end
 
+  describe 'setting new token' do
+    subject { described_class.new.send("set_#{token_field}", '0123456789') }
+
+    it { is_expected.to eq '0123456789' }
+  end
+
   describe 'multiple token fields' do
     before do
       described_class.send(:add_authentication_token_field, :yet_another_token)
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index c8170164898640e2a49bc42729580745f1168bc8..97cbb093ed264e9cd7b86a5879b72deb82aa3cc8 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 describe Environment, models: true do
-  let(:environment) { create(:environment) }
+  subject(:environment) { create(:environment) }
 
   it { is_expected.to belong_to(:project) }
   it { is_expected.to have_many(:deployments) }
@@ -15,15 +15,11 @@ describe Environment, models: true do
   it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) }
   it { is_expected.to validate_length_of(:name).is_at_most(255) }
 
-  it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
-
-  # To circumvent a not null violation of the name column:
-  # https://github.com/thoughtbot/shoulda-matchers/issues/336
-  it 'validates uniqueness of :external_url' do
-    create(:environment)
+  it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) }
+  it { is_expected.to validate_length_of(:slug).is_at_most(24) }
 
-    is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id)
-  end
+  it { is_expected.to validate_length_of(:external_url).is_at_most(255) }
+  it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) }
 
   describe '#nullify_external_url' do
     it 'replaces a blank url with nil' do
@@ -199,4 +195,38 @@ describe Environment, models: true do
       expect(environment.actions_for('review/master')).to contain_exactly(review_action)
     end
   end
+
+  describe '#slug' do
+    it "is automatically generated" do
+      expect(environment.slug).not_to be_nil
+    end
+
+    it "is not regenerated if name changes" do
+      original_slug = environment.slug
+      environment.update_attributes!(name: environment.name.reverse)
+
+      expect(environment.slug).to eq(original_slug)
+    end
+  end
+
+  describe '#generate_slug' do
+    SUFFIX = "-[a-z0-9]{6}"
+    {
+      "staging-12345678901234567" => "staging-123456789" + SUFFIX,
+      "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX,
+      "staging-1234567890123456"  => "staging-1234567890123456",
+      "production"                => "production",
+      "PRODUCTION"                => "production" + SUFFIX,
+      "review/1-foo"              => "review-1-foo" + SUFFIX,
+      "1-foo"                     => "env-1-foo" + SUFFIX,
+      "1/foo"                     => "env-1-foo" + SUFFIX,
+      "foo-"                      => "foo" + SUFFIX,
+    }.each do |name, matcher|
+      it "returns a slug matching #{matcher}, given #{name}" do
+        slug = described_class.new(name: name).generate_slug
+
+        expect(slug).to match(/\A#{matcher}\z/)
+      end
+    end
+  end
 end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 850b1a3cf1ecf0937fd4dee8529bf10ce46e67ff..7d5ecfbaa64a35f19379dc4c7735eb94ac306cb1 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -50,9 +50,8 @@ describe Group, models: true do
 
   describe 'validations' do
     it { is_expected.to validate_presence_of :name }
-    it { is_expected.to validate_uniqueness_of(:name) }
+    it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
     it { is_expected.to validate_presence_of :path }
-    it { is_expected.to validate_uniqueness_of(:path) }
     it { is_expected.not_to validate_presence_of :owner }
   end
 
@@ -273,7 +272,7 @@ describe Group, models: true do
   end
 
   describe 'nested group' do
-    subject { create(:group, :nested) }
+    subject { build(:group, :nested) }
 
     it { is_expected.to be_valid }
     it { expect(subject.parent).to be_kind_of(Group) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 24e216329a9da98f11454aa29fbd9c0b71742d19..bb56e44db296c68cc6cf37ad980e538eacbaacc8 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -22,26 +22,6 @@ describe Issue, models: true do
     it { is_expected.to have_db_index(:deleted_at) }
   end
 
-  describe '.visible_to_user' do
-    let(:user) { create(:user) }
-    let(:authorized_user) { create(:user) }
-    let(:project) { create(:project, namespace: authorized_user.namespace) }
-    let!(:public_issue) { create(:issue, project: project) }
-    let!(:confidential_issue) { create(:issue, project: project, confidential: true) }
-
-    it 'returns non confidential issues for nil user' do
-      expect(Issue.visible_to_user(nil).count).to be(1)
-    end
-
-    it 'returns non confidential issues for user not authorized for the issues projects' do
-      expect(Issue.visible_to_user(user).count).to be(1)
-    end
-
-    it 'returns all issues for user authorized for the issues projects' do
-      expect(Issue.visible_to_user(authorized_user).count).to be(2)
-    end
-  end
-
   describe '#to_reference' do
     let(:project) { build(:empty_project, name: 'sample-project') }
     let(:issue) { build(:issue, iid: 1, project: project) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 8b730be91fdfa5e249a299fe4198c8dc40c5c044..5da00a8636a03cd14b631195a2b364e2c74d98de 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -205,7 +205,7 @@ describe MergeRequest, models: true do
     end
   end
 
-  describe "#mr_and_commit_notes" do
+  describe "#related_notes" do
     let!(:merge_request) { create(:merge_request) }
 
     before do
@@ -217,7 +217,7 @@ describe MergeRequest, models: true do
 
     it "includes notes for commits" do
       expect(merge_request.commits).not_to be_empty
-      expect(merge_request.mr_and_commit_notes.count).to eq(2)
+      expect(merge_request.related_notes.count).to eq(2)
     end
 
     it "includes notes for commits from target project as well" do
@@ -225,7 +225,7 @@ describe MergeRequest, models: true do
                               project: merge_request.target_project)
 
       expect(merge_request.commits).not_to be_empty
-      expect(merge_request.mr_and_commit_notes.count).to eq(3)
+      expect(merge_request.related_notes.count).to eq(3)
     end
   end
 
@@ -252,7 +252,7 @@ describe MergeRequest, models: true do
     end
   end
 
-  describe 'detection of issues to be closed' do
+  describe '#closes_issues' do
     let(:issue0) { create :issue, project: subject.project }
     let(:issue1) { create :issue, project: subject.project }
 
@@ -280,14 +280,19 @@ describe MergeRequest, models: true do
 
       expect(subject.closes_issues).to be_empty
     end
+  end
+
+  describe '#issues_mentioned_but_not_closing' do
+    it 'detects issues mentioned in description but not closed' do
+      mentioned_issue = create(:issue, project: subject.project)
+
+      subject.project.team << [subject.author, :developer]
+      subject.description = "Is related to #{mentioned_issue.to_reference}"
 
-    it 'detects issues mentioned in the description' do
-      issue2 = create(:issue, project: subject.project)
-      subject.description = "Closes #{issue2.to_reference}"
       allow(subject.project).to receive(:default_branch).
         and_return(subject.target_branch)
 
-      expect(subject.closes_issues).to include(issue2)
+      expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue])
     end
   end
 
@@ -410,11 +415,17 @@ describe MergeRequest, models: true do
         .to match("Remove all technical debt\n\n")
     end
 
-    it 'includes its description in the body' do
-      request = build(:merge_request, description: 'By removing all code')
+    it 'includes its closed issues in the body' do
+      issue = create(:issue, project: subject.project)
 
-      expect(request.merge_commit_message)
-        .to match("By removing all code\n\n")
+      subject.project.team << [subject.author, :developer]
+      subject.description = "This issue Closes #{issue.to_reference}"
+
+      allow(subject.project).to receive(:default_branch).
+        and_return(subject.target_branch)
+
+      expect(subject.merge_commit_message)
+        .to match("Closes #{issue.to_reference}")
     end
 
     it 'includes its reference in the body' do
@@ -429,6 +440,20 @@ describe MergeRequest, models: true do
 
       expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
     end
+
+    it 'includes its description in the body' do
+      request = build(:merge_request, description: 'By removing all code')
+
+      expect(request.merge_commit_message(include_description: true))
+        .to match("By removing all code\n\n")
+    end
+
+    it 'does not includes its description in the body' do
+      request = build(:merge_request, description: 'By removing all code')
+
+      expect(request.merge_commit_message)
+        .not_to match("By removing all code\n\n")
+    end
   end
 
   describe "#reset_merge_when_build_succeeds" do
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 0cc2efae5f9b86617716fa685746e3a0470b8e63..064f29d2d66a8a6dac340e23a4a1215c0b1b5fa2 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -24,8 +24,9 @@ describe Milestone, models: true do
     it { is_expected.to have_many(:issues) }
   end
 
-  let(:milestone) { create(:milestone) }
-  let(:issue) { create(:issue) }
+  let(:project) { create(:project, :public) }
+  let(:milestone) { create(:milestone, project: project) }
+  let(:issue) { create(:issue, project: project) }
   let(:user) { create(:user) }
 
   describe "#title" do
@@ -110,8 +111,8 @@ describe Milestone, models: true do
 
   describe :items_count do
     before do
-      milestone.issues << create(:issue)
-      milestone.issues << create(:closed_issue)
+      milestone.issues << create(:issue, project: project)
+      milestone.issues << create(:closed_issue, project: project)
       milestone.merge_requests << create(:merge_request)
     end
 
@@ -126,7 +127,7 @@ describe Milestone, models: true do
 
   describe '#total_items_count' do
     before do
-      create :closed_issue, milestone: milestone
+      create :closed_issue, milestone: milestone, project: project
       create :merge_request, milestone: milestone
     end
 
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 7f82e85563bcfe02fb2ce342ac3204aefa9b1cd6..9fd06bb6b23030fa73b269dd516e5db0291e8f86 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -6,20 +6,16 @@ describe Namespace, models: true do
   it { is_expected.to have_many :projects }
 
   it { is_expected.to validate_presence_of(:name) }
-  it { is_expected.to validate_uniqueness_of(:name) }
+  it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) }
   it { is_expected.to validate_length_of(:name).is_at_most(255) }
 
   it { is_expected.to validate_length_of(:description).is_at_most(255) }
 
   it { is_expected.to validate_presence_of(:path) }
-  it { is_expected.to validate_uniqueness_of(:path) }
   it { is_expected.to validate_length_of(:path).is_at_most(255) }
 
   it { is_expected.to validate_presence_of(:owner) }
 
-  describe "Mass assignment" do
-  end
-
   describe "Respond to" do
     it { is_expected.to respond_to(:human_name) }
     it { is_expected.to respond_to(:to_param) }
@@ -132,4 +128,26 @@ describe Namespace, models: true do
     it { expect(group.full_path).to eq(group.path) }
     it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") }
   end
+
+  describe '#full_name' do
+    let(:group) { create(:group) }
+    let(:nested_group) { create(:group, parent: group) }
+
+    it { expect(group.full_name).to eq(group.name) }
+    it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") }
+  end
+
+  describe '#parents' do
+    let(:group) { create(:group) }
+    let(:nested_group) { create(:group, parent: group) }
+    let(:deep_nested_group) { create(:group, parent: nested_group) }
+    let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) }
+
+    it 'returns the correct parents' do
+      expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group])
+      expect(deep_nested_group.parents).to eq([group, nested_group])
+      expect(nested_group.parents).to eq([group])
+      expect(group.parents).to eq([])
+    end
+  end
 end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 17a15b12dcb09421deb0684d2bf80a93a499ae46..310fecd8a5cc5f05a190730814ca89ce1e6f9b4c 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -162,44 +162,6 @@ describe Note, models: true do
     end
   end
 
-  describe '.search' do
-    let(:note) { create(:note_on_issue, note: 'WoW') }
-
-    it 'returns notes with matching content' do
-      expect(described_class.search(note.note)).to eq([note])
-    end
-
-    it 'returns notes with matching content regardless of the casing' do
-      expect(described_class.search('WOW')).to eq([note])
-    end
-
-    context "confidential issues" do
-      let(:user) { create(:user) }
-      let(:project) { create(:project) }
-      let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) }
-      let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) }
-
-      it "returns notes with matching content if user can see the issue" do
-        expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note])
-      end
-
-      it "does not return notes with matching content if user can not see the issue" do
-        user = create(:user)
-        expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
-      end
-
-      it "does not return notes with matching content for project members with guest role" do
-        user = create(:user)
-        project.team << [user, :guest]
-        expect(described_class.search(confidential_note.note, as_user: user)).to be_empty
-      end
-
-      it "does not return notes with matching content for unauthenticated users" do
-        expect(described_class.search(confidential_note.note)).to be_empty
-      end
-    end
-  end
-
   describe "editable?" do
     it "returns true" do
       note = build(:note)
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
similarity index 94%
rename from spec/models/project_services/slack_service/build_message_spec.rb
rename to spec/models/project_services/chat_message/build_message_spec.rb
index 452f4e2782c80568f71bffc76f8deeaa6438b0ed..b71d153f814b49f30919e6d7afec9311925a35e8 100644
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ b/spec/models/project_services/chat_message/build_message_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
-describe SlackService::BuildMessage do
-  subject { SlackService::BuildMessage.new(args) }
+describe ChatMessage::BuildMessage do
+  subject { described_class.new(args) }
 
   let(:args) do
     {
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb
similarity index 93%
rename from spec/models/project_services/slack_service/issue_message_spec.rb
rename to spec/models/project_services/chat_message/issue_message_spec.rb
index 98c36ec088dde79355b9a16943f194e68abbc285..ebe0ead4408f8d7e16d2e33849ab26cf40f55ed3 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/chat_message/issue_message_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
-describe SlackService::IssueMessage, models: true do
-  subject { SlackService::IssueMessage.new(args) }
+describe ChatMessage::IssueMessage, models: true do
+  subject { described_class.new(args) }
 
   let(:args) do
     {
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb
similarity index 92%
rename from spec/models/project_services/slack_service/merge_message_spec.rb
rename to spec/models/project_services/chat_message/merge_message_spec.rb
index c5c052d9af1b0e342bf0493f096f9ab7be7ad404..07c414c6ca468dd6b242729f5115283d724c307b 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/chat_message/merge_message_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
-describe SlackService::MergeMessage, models: true do
-  subject { SlackService::MergeMessage.new(args) }
+describe ChatMessage::MergeMessage, models: true do
+  subject { described_class.new(args) }
 
   let(:args) do
     {
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb
similarity index 92%
rename from spec/models/project_services/slack_service/note_message_spec.rb
rename to spec/models/project_services/chat_message/note_message_spec.rb
index 97f818125d33155cf1aea8f27098a97fa636e9a0..31936da40a221a8475933ed795764169fc8d6644 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/chat_message/note_message_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe SlackService::NoteMessage, models: true do
+describe ChatMessage::NoteMessage, models: true do
   let(:color) { '#345' }
 
   before do
@@ -36,7 +36,7 @@ describe SlackService::NoteMessage, models: true do
     end
 
     it 'returns a message regarding notes on commits' do
-      message = SlackService::NoteMessage.new(@args)
+      message = described_class.new(@args)
       expect(message.pretext).to eq("test.user <url|commented on " \
       "commit 5f163b2b> in <somewhere.com|project_name>: " \
       "*Added a commit message*")
@@ -62,7 +62,7 @@ describe SlackService::NoteMessage, models: true do
     end
 
     it 'returns a message regarding notes on a merge request' do
-      message = SlackService::NoteMessage.new(@args)
+      message = described_class.new(@args)
       expect(message.pretext).to eq("test.user <url|commented on " \
       "merge request !30> in <somewhere.com|project_name>: " \
       "*merge request title*")
@@ -88,7 +88,7 @@ describe SlackService::NoteMessage, models: true do
     end
 
     it 'returns a message regarding notes on an issue' do
-      message = SlackService::NoteMessage.new(@args)
+      message = described_class.new(@args)
       expect(message.pretext).to eq(
         "test.user <url|commented on " \
         "issue #20> in <somewhere.com|project_name>: " \
@@ -114,7 +114,7 @@ describe SlackService::NoteMessage, models: true do
     end
 
     it 'returns a message regarding notes on a project snippet' do
-      message = SlackService::NoteMessage.new(@args)
+      message = described_class.new(@args)
       expect(message.pretext).to eq("test.user <url|commented on " \
       "snippet #5> in <somewhere.com|project_name>: " \
       "*snippet title*")
diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb
similarity index 58%
rename from spec/models/project_services/slack_service/pipeline_message_spec.rb
rename to spec/models/project_services/chat_message/pipeline_message_spec.rb
index 363138a9454ca5cec4e65a036ade72c74d01163c..eca71db07b671baf1d46b5ed76512bbf54729493 100644
--- a/spec/models/project_services/slack_service/pipeline_message_spec.rb
+++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb
@@ -1,7 +1,8 @@
 require 'spec_helper'
 
-describe SlackService::PipelineMessage do
-  subject { SlackService::PipelineMessage.new(args) }
+describe ChatMessage::PipelineMessage do
+  subject { described_class.new(args) }
+  let(:user) { { name: 'hacker' } }
 
   let(:args) do
     {
@@ -15,7 +16,7 @@ describe SlackService::PipelineMessage do
       },
       project: { path_with_namespace: 'project_name',
                  web_url: 'example.gitlab.com' },
-      user: { name: 'hacker' }
+      user: user
     }
   end
 
@@ -28,9 +29,7 @@ describe SlackService::PipelineMessage do
     let(:message) { build_message('passed') }
 
     it 'returns a message with information about succeeded build' do
-      expect(subject.pretext).to be_empty
-      expect(subject.fallback).to eq(message)
-      expect(subject.attachments).to eq([text: message, color: color])
+      verify_message
     end
   end
 
@@ -40,16 +39,29 @@ describe SlackService::PipelineMessage do
     let(:duration) { 10 }
 
     it 'returns a message with information about failed build' do
-      expect(subject.pretext).to be_empty
-      expect(subject.fallback).to eq(message)
-      expect(subject.attachments).to eq([text: message, color: color])
+      verify_message
     end
+
+    context 'when triggered by API therefore lacking user' do
+      let(:user) { nil }
+      let(:message) { build_message(status, 'API') }
+
+      it 'returns a message stating it is by API' do
+        verify_message
+      end
+    end
+  end
+
+  def verify_message
+    expect(subject.pretext).to be_empty
+    expect(subject.fallback).to eq(message)
+    expect(subject.attachments).to eq([text: message, color: color])
   end
 
-  def build_message(status_text = status)
+  def build_message(status_text = status, name = user[:name])
     "<example.gitlab.com|project_name>:" \
     " Pipeline <example.gitlab.com/pipelines/123|#123>" \
     " of <example.gitlab.com/commits/develop|develop> branch" \
-    " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+    " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}"
   end
 end
diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb
similarity index 95%
rename from spec/models/project_services/slack_service/push_message_spec.rb
rename to spec/models/project_services/chat_message/push_message_spec.rb
index 17cd05e24f1291e4765fc70ae839f11e0da82c6f..b781c4505db7b29e526142203bedaa1fb118712f 100644
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ b/spec/models/project_services/chat_message/push_message_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
-describe SlackService::PushMessage, models: true do
-  subject { SlackService::PushMessage.new(args) }
+describe ChatMessage::PushMessage, models: true do
+  subject { described_class.new(args) }
 
   let(:args) do
     {
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
similarity index 97%
rename from spec/models/project_services/slack_service/wiki_page_message_spec.rb
rename to spec/models/project_services/chat_message/wiki_page_message_spec.rb
index 093911598b009e29a2a606c2231f0968e5cd82e1..94c04dc08654bb46196a820c91ce1fdfa2fa38d6 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-describe SlackService::WikiPageMessage, models: true do
+describe ChatMessage::WikiPageMessage, models: true do
   subject { described_class.new(args) }
 
   let(:args) do
diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c98e7ee14fdf76cd2bd7b694f52a07164ffc4bf3
--- /dev/null
+++ b/spec/models/project_services/chat_notification_service_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe ChatNotificationService, models: true do
+  describe "Associations" do
+    before do
+      allow(subject).to receive(:activated?).and_return(true)
+    end
+
+    it { is_expected.to validate_presence_of :webhook }
+  end
+end
diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3603602e41d6e87be7274c4598ac9af7f202222a
--- /dev/null
+++ b/spec/models/project_services/kubernetes_service_spec.rb
@@ -0,0 +1,159 @@
+require 'spec_helper'
+
+describe KubernetesService, models: true do
+  let(:project) { create(:empty_project) }
+
+  describe "Associations" do
+    it { is_expected.to belong_to :project }
+  end
+
+  describe 'Validations' do
+    context 'when service is active' do
+      before { subject.active = true }
+      it { is_expected.to validate_presence_of(:namespace) }
+      it { is_expected.to validate_presence_of(:api_url) }
+      it { is_expected.to validate_presence_of(:token) }
+
+      context 'namespace format' do
+        before do
+          subject.project = project
+          subject.api_url = "http://example.com"
+          subject.token = "test"
+        end
+
+        {
+          'foo'  => true,
+          '1foo' => true,
+          'foo1' => true,
+          'foo-bar' => true,
+          '-foo' => false,
+          'foo-' => false,
+          'a' * 63 => true,
+          'a' * 64 => false,
+          'a.b' => false,
+          'a*b' => false,
+        }.each do |namespace, validity|
+          it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do
+            subject.namespace = namespace
+
+            expect(subject.valid?).to eq(validity)
+          end
+        end
+      end
+    end
+
+    context 'when service is inactive' do
+      before { subject.active = false }
+      it { is_expected.not_to validate_presence_of(:namespace) }
+      it { is_expected.not_to validate_presence_of(:api_url) }
+      it { is_expected.not_to validate_presence_of(:token) }
+    end
+  end
+
+  describe '#initialize_properties' do
+    context 'with a project' do
+      it 'defaults to the project name' do
+        expect(described_class.new(project: project).namespace).to eq(project.name)
+      end
+    end
+
+    context 'without a project' do
+      it 'leaves the namespace unset' do
+        expect(described_class.new.namespace).to be_nil
+      end
+    end
+  end
+
+  describe '#test' do
+    let(:project) { create(:kubernetes_project) }
+    let(:service) { project.kubernetes_service }
+    let(:discovery_url) { service.api_url + '/api/v1' }
+
+    # JSON response body from Kubernetes GET /api/v1 request
+    let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json }
+
+    context 'with path prefix in api_url' do
+      let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' }
+
+      before do
+        service.api_url = 'https://kubernetes.example.com/prefix/'
+      end
+
+      it 'tests with the prefix' do
+        WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
+
+        expect(service.test[:success]).to be_truthy
+        expect(WebMock).to have_requested(:get, discovery_url).once
+      end
+    end
+
+    context 'with custom CA certificate' do
+      let(:certificate) { "CA PEM DATA" }
+      before do
+        service.update_attributes!(ca_pem: certificate)
+      end
+
+      it 'is added to the certificate store' do
+        cert = double("certificate")
+
+        expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert)
+        expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert)
+        WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
+
+        expect(service.test[:success]).to be_truthy
+        expect(WebMock).to have_requested(:get, discovery_url).once
+      end
+    end
+
+    context 'success' do
+      it 'reads the discovery endpoint' do
+        WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response)
+
+        expect(service.test[:success]).to be_truthy
+        expect(WebMock).to have_requested(:get, discovery_url).once
+      end
+    end
+
+    context 'failure' do
+      it 'fails to read the discovery endpoint' do
+        WebMock.stub_request(:get, discovery_url).to_return(status: 404)
+
+        expect(service.test[:success]).to be_falsy
+        expect(WebMock).to have_requested(:get, discovery_url).once
+      end
+    end
+  end
+
+  describe '#predefined_variables' do
+    before do
+      subject.api_url = 'https://kube.domain.com'
+      subject.token = 'token'
+      subject.namespace = 'my-project'
+      subject.ca_pem = 'CA PEM DATA'
+    end
+
+    it 'sets KUBE_URL' do
+      expect(subject.predefined_variables).to include(
+        { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true }
+      )
+    end
+
+    it 'sets KUBE_TOKEN' do
+      expect(subject.predefined_variables).to include(
+        { key: 'KUBE_TOKEN', value: 'token', public: false }
+      )
+    end
+
+    it 'sets KUBE_NAMESPACE' do
+      expect(subject.predefined_variables).to include(
+        { key: 'KUBE_NAMESPACE', value: 'my-project', public: true }
+      )
+    end
+
+    it 'sets KUBE_CA_PEM' do
+      expect(subject.predefined_variables).to include(
+        { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true }
+      )
+    end
+  end
+end
diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c01e64b4c8e9ec3ddefb8401440936d9dcd3c041
--- /dev/null
+++ b/spec/models/project_services/mattermost_notification_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe MattermostNotificationService, models: true do
+  it_behaves_like "slack or mattermost"
+end
diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..59ddddf745451a0cb049140aa09535011e9ac5a5
--- /dev/null
+++ b/spec/models/project_services/slack_notification_service_spec.rb
@@ -0,0 +1,5 @@
+require 'spec_helper'
+
+describe SlackNotificationService, models: true do
+  it_behaves_like "slack or mattermost"
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 21ff238841ea237d6a0f9db719e9a3b262b726ea..ed6b2c6a22b7e9f4767b845f2dd1360a5daef5d9 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -22,7 +22,8 @@ describe Project, models: true do
     it { is_expected.to have_many(:protected_branches).dependent(:destroy) }
     it { is_expected.to have_many(:chat_services) }
     it { is_expected.to have_one(:forked_project_link).dependent(:destroy) }
-    it { is_expected.to have_one(:slack_service).dependent(:destroy) }
+    it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) }
+    it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) }
     it { is_expected.to have_one(:pushover_service).dependent(:destroy) }
     it { is_expected.to have_one(:asana_service).dependent(:destroy) }
     it { is_expected.to have_many(:boards).dependent(:destroy) }
@@ -1696,6 +1697,26 @@ describe Project, models: true do
     end
   end
 
+  describe '#deployment_variables' do
+    context 'when project has no deployment service' do
+      let(:project) { create(:empty_project) }
+
+      it 'returns an empty array' do
+        expect(project.deployment_variables).to eq []
+      end
+    end
+
+    context 'when project has a deployment service' do
+      let(:project) { create(:kubernetes_project) }
+
+      it 'returns variables from this service' do
+        expect(project.deployment_variables).to include(
+          { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false }
+        )
+      end
+    end
+  end
+
   def enable_lfs
     allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
   end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a20ac303a5385c82d5d6df692e551da94a365365
--- /dev/null
+++ b/spec/policies/group_policy_spec.rb
@@ -0,0 +1,108 @@
+require 'spec_helper'
+
+describe GroupPolicy, models: true do
+  let(:guest) { create(:user) }
+  let(:reporter) { create(:user) }
+  let(:developer) { create(:user) }
+  let(:master) { create(:user) }
+  let(:owner) { create(:user) }
+  let(:admin) { create(:admin) }
+  let(:group) { create(:group) }
+
+  let(:master_permissions) do
+    [
+      :create_projects,
+      :admin_milestones,
+      :admin_label
+    ]
+  end
+
+  let(:owner_permissions) do
+    [
+      :admin_group,
+      :admin_namespace,
+      :admin_group_member,
+      :change_visibility_level
+    ]
+  end
+
+  before do
+    group.add_guest(guest)
+    group.add_reporter(reporter)
+    group.add_developer(developer)
+    group.add_master(master)
+    group.add_owner(owner)
+  end
+
+  subject { described_class.abilities(current_user, group).to_set }
+
+  context 'with no user' do
+    let(:current_user) { nil }
+
+    it do
+      is_expected.to include(:read_group)
+      is_expected.not_to include(*master_permissions)
+      is_expected.not_to include(*owner_permissions)
+    end
+  end
+
+  context 'guests' do
+    let(:current_user) { guest }
+
+    it do
+      is_expected.to include(:read_group)
+      is_expected.not_to include(*master_permissions)
+      is_expected.not_to include(*owner_permissions)
+    end
+  end
+
+  context 'reporter' do
+    let(:current_user) { reporter }
+
+    it do
+      is_expected.to include(:read_group)
+      is_expected.not_to include(*master_permissions)
+      is_expected.not_to include(*owner_permissions)
+    end
+  end
+
+  context 'developer' do
+    let(:current_user) { developer }
+
+    it do
+      is_expected.to include(:read_group)
+      is_expected.not_to include(*master_permissions)
+      is_expected.not_to include(*owner_permissions)
+    end
+  end
+
+  context 'master' do
+    let(:current_user) { master }
+
+    it do
+      is_expected.to include(:read_group)
+      is_expected.to include(*master_permissions)
+      is_expected.not_to include(*owner_permissions)
+    end
+  end
+
+  context 'owner' do
+    let(:current_user) { owner }
+
+    it do
+      is_expected.to include(:read_group)
+      is_expected.to include(*master_permissions)
+      is_expected.to include(*owner_permissions)
+    end
+  end
+
+  context 'admin' do
+    let(:current_user) { admin }
+
+    it do
+      is_expected.to include(:read_group)
+      is_expected.to include(*master_permissions)
+      is_expected.to include(*owner_permissions)
+    end
+  end
+end
diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb
index 5262a623761545d84291e4672de894640a6957b6..bd9ecaf26856c99ed80de5920f426786efbddd80 100644
--- a/spec/requests/api/doorkeeper_access_spec.rb
+++ b/spec/requests/api/doorkeeper_access_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true  do
 
   let!(:user) { create(:user) }
   let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) }
-  let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id }
+  let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" }
 
   describe "when unauthenticated" do
     it "returns authentication success" do
diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb
index 126496c43a5bbf5656859447372d6c274de20b54..b9d535bc314c7b33fb52399102d58078245135e7 100644
--- a/spec/requests/api/environments_spec.rb
+++ b/spec/requests/api/environments_spec.rb
@@ -46,6 +46,7 @@ describe API::Environments, api: true  do
 
         expect(response).to have_http_status(201)
         expect(json_response['name']).to eq('mepmep')
+        expect(json_response['slug']).to eq('mepmep')
         expect(json_response['external']).to be nil
       end
 
@@ -60,6 +61,13 @@ describe API::Environments, api: true  do
 
         expect(response).to have_http_status(400)
       end
+
+      it 'returns a 400 if slug is specified' do
+        post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo"
+
+        expect(response).to have_http_status(400)
+        expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+      end
     end
 
     context 'a non member' do
@@ -86,6 +94,15 @@ describe API::Environments, api: true  do
       expect(json_response['external_url']).to eq(url)
     end
 
+    it "won't allow slug to be changed" do
+      slug = environment.slug
+      api_url = api("/projects/#{project.id}/environments/#{environment.id}", user)
+      put api_url, slug: slug + "-foo"
+
+      expect(response).to have_http_status(400)
+      expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed")
+    end
+
     it "won't update the external_url if only the name is passed" do
       url = environment.external_url
       put api("/projects/#{project.id}/environments/#{environment.id}", user),
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index a75ba824e85412bef0b39726108de4e5e3a62ade..cdeb965b413140d059f4ad3126df97f826dc8d60 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -2,13 +2,13 @@ require 'spec_helper'
 
 describe API::Groups, api: true  do
   include ApiHelpers
+  include UploadHelpers
 
   let(:user1) { create(:user, can_create_group: false) }
   let(:user2) { create(:user) }
   let(:user3) { create(:user) }
   let(:admin) { create(:admin) }
-  let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') }
-  let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) }
+  let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
   let!(:group2) { create(:group, :private) }
   let!(:project1) { create(:project, namespace: group1) }
   let!(:project2) { create(:project, namespace: group2) }
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 4035fd97af59a52ff0eafccb8bfaf25d12774912..c3d7ac3eef8407ce4a1dd34124bf555bb51181f7 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -1,6 +1,7 @@
 require 'spec_helper'
 
 describe API::Helpers, api: true do
+  include API::APIGuard::HelperMethods
   include API::Helpers
   include SentryHelper
 
@@ -15,24 +16,24 @@ describe API::Helpers, api: true do
   def set_env(user_or_token, identifier)
     clear_env
     clear_param
-    env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+    env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
     env[API::Helpers::SUDO_HEADER] = identifier.to_s
   end
 
   def set_param(user_or_token, identifier)
     clear_env
     clear_param
-    params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
+    params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token
     params[API::Helpers::SUDO_PARAM] = identifier.to_s
   end
 
   def clear_env
-    env.delete(API::Helpers::PRIVATE_TOKEN_HEADER)
+    env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER)
     env.delete(API::Helpers::SUDO_HEADER)
   end
 
   def clear_param
-    params.delete(API::Helpers::PRIVATE_TOKEN_PARAM)
+    params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM)
     params.delete(API::Helpers::SUDO_PARAM)
   end
 
@@ -94,22 +95,28 @@ describe API::Helpers, api: true do
 
     describe "when authenticating using a user's private token" do
       it "returns nil for an invalid token" do
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
         allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+
         expect(current_user).to be_nil
       end
 
       it "returns nil for a user without access" do
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
         allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+
         expect(current_user).to be_nil
       end
 
       it "leaves user as is when sudo not specified" do
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token
+
         expect(current_user).to eq(user)
+
         clear_env
-        params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token
+
+        params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token
+
         expect(current_user).to eq(user)
       end
     end
@@ -117,37 +124,51 @@ describe API::Helpers, api: true do
     describe "when authenticating using a user's personal access tokens" do
       let(:personal_access_token) { create(:personal_access_token, user: user) }
 
+      before do
+        allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false }
+      end
+
       it "returns nil for an invalid token" do
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token'
-        allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token'
+
         expect(current_user).to be_nil
       end
 
       it "returns nil for a user without access" do
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
         allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false)
+
+        expect(current_user).to be_nil
+      end
+
+      it "returns nil for a token without the appropriate scope" do
+        personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user'])
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+        allow_access_with_scope('write_user')
+
         expect(current_user).to be_nil
       end
 
       it "leaves user as is when sudo not specified" do
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
         expect(current_user).to eq(user)
         clear_env
-        params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+        params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token
+
         expect(current_user).to eq(user)
       end
 
       it 'does not allow revoked tokens' do
         personal_access_token.revoke!
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
-        allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
         expect(current_user).to be_nil
       end
 
       it 'does not allow expired tokens' do
         personal_access_token.update_attributes!(expires_at: 1.day.ago)
-        env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token
-        allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false }
+        env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token
+
         expect(current_user).to be_nil
       end
     end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index c5d67a90abc194813a123721399e7a26d5867977..8304c4080646389b89031030777884b9d2a46824 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -167,7 +167,7 @@ describe API::Projects, api: true  do
         expect(json_response).to satisfy do |response|
           response.one? do |entry|
             entry.has_key?('permissions') &&
-            entry['name'] == project.name &&
+              entry['name'] == project.name &&
               entry['owner']['username'] == user.username
           end
         end
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index f1728d61def1be85ce85a015b154e094b79a8274..d71bb08c218771274c09e3d23c2723d8183d5849 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -230,7 +230,7 @@ describe 'Git HTTP requests', lib: true do
               context "when an oauth token is provided" do
                 before do
                   application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
-                  @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+                  @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api")
                 end
 
                 it "downloads get status 200" do
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 69eeb45ed71589744275663176dae332d374d29b..661b671301e8e3f13bd42526b61a8f59d2e88304 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do
   end
 
   it "to #show" do
-    expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab')
+    expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce')
+    expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce')
   end
 end
 
@@ -119,3 +120,14 @@ describe Admin::HealthCheckController, "routing" do
     expect(get("/admin/health_check")).to route_to('admin/health_check#show')
   end
 end
+
+describe Admin::GroupsController, "routing" do
+  it "to #index" do
+    expect(get("/admin/groups")).to route_to('admin/groups#index')
+  end
+
+  it "to #show" do
+    expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab')
+    expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup')
+  end
+end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index b6e7da841b1c7cf1ee18f2a5f8c4757dccc1df5d..77549db29277b5c15a40290f9fd43c8892287d1e 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -80,10 +80,6 @@ describe 'project routing' do
       expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq')
     end
 
-    it 'to #autocomplete_sources' do
-      expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq')
-    end
-
     describe 'to #show' do
       context 'regular name' do
         it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') }
@@ -117,6 +113,21 @@ describe 'project routing' do
     end
   end
 
+  # emojis_namespace_project_autocomplete_sources_path         GET /:project_id/autocomplete_sources/emojis(.:format)         projects/autocomplete_sources#emojis
+  # members_namespace_project_autocomplete_sources_path        GET /:project_id/autocomplete_sources/members(.:format)        projects/autocomplete_sources#members
+  # issues_namespace_project_autocomplete_sources_path         GET /:project_id/autocomplete_sources/issues(.:format)         projects/autocomplete_sources#issues
+  # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests
+  # labels_namespace_project_autocomplete_sources_path         GET /:project_id/autocomplete_sources/labels(.:format)         projects/autocomplete_sources#labels
+  # milestones_namespace_project_autocomplete_sources_path     GET /:project_id/autocomplete_sources/milestones(.:format)     projects/autocomplete_sources#milestones
+  # commands_namespace_project_autocomplete_sources_path       GET /:project_id/autocomplete_sources/commands(.:format)       projects/autocomplete_sources#commands
+  describe Projects::AutocompleteSourcesController, 'routing' do
+    [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action|
+      it "to ##{action}" do
+        expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq')
+      end
+    end
+  end
+
   #  pages_project_wikis GET    /:project_id/wikis/pages(.:format)       projects/wikis#pages
   # history_project_wiki GET    /:project_id/wikis/:id/history(.:format) projects/wikis#history
   #        project_wikis POST   /:project_id/wikis(.:format)             projects/wikis#create
diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb
index 6b33fe66a63767944b02cb0f1ecb2a84567806ef..86e703a6448081a5803cde0362accf553976f815 100644
--- a/spec/serializers/analytics_build_entity_spec.rb
+++ b/spec/serializers/analytics_build_entity_spec.rb
@@ -13,6 +13,14 @@ describe AnalyticsBuildEntity do
 
     subject { entity.as_json }
 
+    before do
+      Timecop.freeze
+    end
+
+    after do
+      Timecop.return
+    end
+
     it 'contains the URL' do
       expect(subject).to include(:url)
     end
diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..87f093ee8ce9183a0e0434017bda56fdbe1d6559
--- /dev/null
+++ b/spec/services/access_token_validation_service_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe AccessTokenValidationService, services: true do
+  describe ".include_any_scope?" do
+    it "returns true if the required scope is present in the token's scopes" do
+      token = double("token", scopes: [:api, :read_user])
+
+      expect(described_class.new(token).include_any_scope?([:api])).to be(true)
+    end
+
+    it "returns true if more than one of the required scopes is present in the token's scopes" do
+      token = double("token", scopes: [:api, :read_user, :other_scope])
+
+      expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true)
+    end
+
+    it "returns true if the list of required scopes is an exact match for the token's scopes" do
+      token = double("token", scopes: [:api, :read_user, :other_scope])
+
+      expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
+    end
+
+    it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do
+      token = double("token", scopes: [:api, :read_user])
+
+      expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true)
+    end
+
+    it 'returns true if the list of required scopes is blank' do
+      token = double("token", scopes: [])
+
+      expect(described_class.new(token).include_any_scope?([])).to be(true)
+    end
+
+    it "returns false if there are no scopes in common between the required scopes and the token scopes" do
+      token = double("token", scopes: [:api, :read_user])
+
+      expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false)
+    end
+  end
+end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 4aadd009f3ecf0316e6ae28b9de734f317a29e69..ceaca96e25b377a1ace0e89bef1de526261ea9d1 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do
         expect(result.manual_actions).not_to be_empty
       end
     end
+
+    context 'with environment' do
+      before do
+        config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' })
+        stub_ci_pipeline_yaml_file(config)
+      end
+
+      it 'creates the environment' do
+        result = execute(ref: 'refs/heads/master',
+                         before: '00000000',
+                         after: project.commit.id,
+                         commits: [{ message: 'some msg' }])
+
+        expect(result).to be_persisted
+        expect(Environment.find_by(name: "review/master")).not_to be_nil
+      end
+    end
   end
 end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 500d224ff98434fa39671e17b419ab13aec52e1c..eafbea46905a9b6cf04bb886da7e6e9eb056ea9d 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do
       let(:mentionable) { issue }
       include_examples 'updating mentions', Issues::UpdateService
     end
+
+    include_examples 'issuable update service' do
+      let(:open_issuable) { issue }
+      let(:closed_issuable) { create(:closed_issue, project: project) }
+    end
   end
 end
diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb
index 790ef765f3a1b641faacbf46c9a431a19cf1b78a..88c786947d38cba6d4607b845b393d42e7c2c8f5 100644
--- a/spec/services/merge_requests/update_service_spec.rb
+++ b/spec/services/merge_requests/update_service_spec.rb
@@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do
         expect(issue_ids).to be_empty
       end
     end
+
+    include_examples 'issuable update service' do
+      let(:open_issuable) { merge_request }
+      let(:closed_issuable) { create(:closed_merge_request, source_project: project) }
+    end
   end
 end
diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a3336755773790acd8315dfffa58aef1de318cb7
--- /dev/null
+++ b/spec/support/services/issuable_update_service_shared_examples.rb
@@ -0,0 +1,17 @@
+shared_examples 'issuable update service' do
+  context 'changing state' do
+    before { expect(project).to receive(:execute_hooks).once }
+
+    context 'to reopened' do
+      it 'executes hooks only once' do
+        described_class.new(project, user, state_event: 'reopen').execute(closed_issuable)
+      end
+    end
+
+    context 'to closed' do
+      it 'executes hooks only once' do
+        described_class.new(project, user, state_event: 'close').execute(open_issuable)
+      end
+    end
+  end
+end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/support/slack_mattermost_shared_examples.rb
similarity index 76%
rename from spec/models/project_services/slack_service_spec.rb
rename to spec/support/slack_mattermost_shared_examples.rb
index c07a70a806965975c637e5a877b7225811c95a71..56d4965f74df71a6cf46510e0cf40ee2900e95f0 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/support/slack_mattermost_shared_examples.rb
@@ -1,7 +1,7 @@
-require 'spec_helper'
+Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f }
 
-describe SlackService, models: true do
-  let(:slack) { SlackService.new }
+RSpec.shared_examples 'slack or mattermost' do
+  let(:chat_service) { described_class.new }
   let(:webhook_url) { 'https://example.gitlab.com/' }
 
   describe "Associations" do
@@ -24,7 +24,7 @@ describe SlackService, models: true do
     end
   end
 
-  describe "Execute" do
+  describe "#execute" do
     let(:user)    { create(:user) }
     let(:project) { create(:project) }
     let(:username) { 'slack_username' }
@@ -35,7 +35,7 @@ describe SlackService, models: true do
     end
 
     before do
-      allow(slack).to receive_messages(
+      allow(chat_service).to receive_messages(
         project: project,
         project_id: project.id,
         service_hook: true,
@@ -77,54 +77,55 @@ describe SlackService, models: true do
       @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
     end
 
-    it "calls Slack API for push events" do
-      slack.execute(push_sample_data)
+    it "calls Slack/Mattermost API for push events" do
+      chat_service.execute(push_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "calls Slack API for issue events" do
-      slack.execute(@issues_sample_data)
+    it "calls Slack/Mattermost API for issue events" do
+      chat_service.execute(@issues_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "calls Slack API for merge requests events" do
-      slack.execute(@merge_sample_data)
+    it "calls Slack/Mattermost API for merge requests events" do
+      chat_service.execute(@merge_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
-    it "calls Slack API for wiki page events" do
-      slack.execute(@wiki_page_sample_data)
+    it "calls Slack/Mattermost API for wiki page events" do
+      chat_service.execute(@wiki_page_sample_data)
 
       expect(WebMock).to have_requested(:post, webhook_url).once
     end
 
     it 'uses the username as an option for slack when configured' do
-      allow(slack).to receive(:username).and_return(username)
+      allow(chat_service).to receive(:username).and_return(username)
+
       expect(Slack::Notifier).to receive(:new).
-       with(webhook_url, username: username).
+       with(webhook_url, username: username, channel: chat_service.default_channel).
        and_return(
          double(:slack_service).as_null_object
        )
 
-      slack.execute(push_sample_data)
+      chat_service.execute(push_sample_data)
     end
 
     it 'uses the channel as an option when it is configured' do
-      allow(slack).to receive(:channel).and_return(channel)
+      allow(chat_service).to receive(:channel).and_return(channel)
       expect(Slack::Notifier).to receive(:new).
         with(webhook_url, channel: channel).
         and_return(
           double(:slack_service).as_null_object
         )
-      slack.execute(push_sample_data)
+      chat_service.execute(push_sample_data)
     end
 
     context "event channels" do
       it "uses the right channel for push event" do
-        slack.update_attributes(push_channel: "random")
+        chat_service.update_attributes(push_channel: "random")
 
         expect(Slack::Notifier).to receive(:new).
          with(webhook_url, channel: "random").
@@ -132,11 +133,11 @@ describe SlackService, models: true do
            double(:slack_service).as_null_object
          )
 
-        slack.execute(push_sample_data)
+        chat_service.execute(push_sample_data)
       end
 
       it "uses the right channel for merge request event" do
-        slack.update_attributes(merge_request_channel: "random")
+        chat_service.update_attributes(merge_request_channel: "random")
 
         expect(Slack::Notifier).to receive(:new).
          with(webhook_url, channel: "random").
@@ -144,11 +145,11 @@ describe SlackService, models: true do
            double(:slack_service).as_null_object
          )
 
-        slack.execute(@merge_sample_data)
+        chat_service.execute(@merge_sample_data)
       end
 
       it "uses the right channel for issue event" do
-        slack.update_attributes(issue_channel: "random")
+        chat_service.update_attributes(issue_channel: "random")
 
         expect(Slack::Notifier).to receive(:new).
          with(webhook_url, channel: "random").
@@ -156,11 +157,11 @@ describe SlackService, models: true do
            double(:slack_service).as_null_object
          )
 
-        slack.execute(@issues_sample_data)
+        chat_service.execute(@issues_sample_data)
       end
 
       it "uses the right channel for wiki event" do
-        slack.update_attributes(wiki_page_channel: "random")
+        chat_service.update_attributes(wiki_page_channel: "random")
 
         expect(Slack::Notifier).to receive(:new).
          with(webhook_url, channel: "random").
@@ -168,7 +169,7 @@ describe SlackService, models: true do
            double(:slack_service).as_null_object
          )
 
-        slack.execute(@wiki_page_sample_data)
+        chat_service.execute(@wiki_page_sample_data)
       end
 
       context "note event" do
@@ -177,7 +178,7 @@ describe SlackService, models: true do
         end
 
         it "uses the right channel" do
-          slack.update_attributes(note_channel: "random")
+          chat_service.update_attributes(note_channel: "random")
 
           note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
 
@@ -187,7 +188,7 @@ describe SlackService, models: true do
              double(:slack_service).as_null_object
            )
 
-          slack.execute(note_data)
+          chat_service.execute(note_data)
         end
       end
     end
@@ -198,7 +199,7 @@ describe SlackService, models: true do
     let(:project) { create(:project, creator_id: user.id) }
 
     before do
-      allow(slack).to receive_messages(
+      allow(chat_service).to receive_messages(
         project: project,
         project_id: project.id,
         service_hook: true,
@@ -216,9 +217,9 @@ describe SlackService, models: true do
                                 note: 'a comment on a commit')
       end
 
-      it "calls Slack API for commit comment events" do
+      it "calls Slack/Mattermost API for commit comment events" do
         data = Gitlab::DataBuilder::Note.build(commit_note, user)
-        slack.execute(data)
+        chat_service.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
       end
@@ -232,7 +233,7 @@ describe SlackService, models: true do
 
       it "calls Slack API for merge request comment events" do
         data = Gitlab::DataBuilder::Note.build(merge_request_note, user)
-        slack.execute(data)
+        chat_service.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
       end
@@ -245,7 +246,7 @@ describe SlackService, models: true do
 
       it "calls Slack API for issue comment events" do
         data = Gitlab::DataBuilder::Note.build(issue_note, user)
-        slack.execute(data)
+        chat_service.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
       end
@@ -259,7 +260,7 @@ describe SlackService, models: true do
 
       it "calls Slack API for snippet comment events" do
         data = Gitlab::DataBuilder::Note.build(snippet_note, user)
-        slack.execute(data)
+        chat_service.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
       end
@@ -277,21 +278,21 @@ describe SlackService, models: true do
     end
 
     before do
-      allow(slack).to receive_messages(
+      allow(chat_service).to receive_messages(
         project: project,
         service_hook: true,
         webhook: webhook_url
       )
     end
 
-    shared_examples 'call Slack API' do
+    shared_examples 'call Slack/Mattermost API' do
       before do
         WebMock.stub_request(:post, webhook_url)
       end
 
-      it 'calls Slack API for pipeline events' do
+      it 'calls Slack/Mattermost API for pipeline events' do
         data = Gitlab::DataBuilder::Pipeline.build(pipeline)
-        slack.execute(data)
+        chat_service.execute(data)
 
         expect(WebMock).to have_requested(:post, webhook_url).once
       end
@@ -300,16 +301,16 @@ describe SlackService, models: true do
     context 'with failed pipeline' do
       let(:status) { 'failed' }
 
-      it_behaves_like 'call Slack API'
+      it_behaves_like 'call Slack/Mattermost API'
     end
 
     context 'with succeeded pipeline' do
       let(:status) { 'success' }
 
       context 'with default to notify_only_broken_pipelines' do
-        it 'does not call Slack API for pipeline events' do
+        it 'does not call Slack/Mattermost API for pipeline events' do
           data = Gitlab::DataBuilder::Pipeline.build(pipeline)
-          result = slack.execute(data)
+          result = chat_service.execute(data)
 
           expect(result).to be_falsy
         end
@@ -317,10 +318,10 @@ describe SlackService, models: true do
 
       context 'with setting notify_only_broken_pipelines to false' do
         before do
-          slack.notify_only_broken_pipelines = false
+          chat_service.notify_only_broken_pipelines = false
         end
 
-        it_behaves_like 'call Slack API'
+        it_behaves_like 'call Slack/Mattermost API'
       end
     end
   end
diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5eead80c935d7121d1f3a8dd2df2190679d69624
--- /dev/null
+++ b/spec/support/upload_helpers.rb
@@ -0,0 +1,16 @@
+require 'fileutils'
+
+module UploadHelpers
+  extend self
+
+  def uploaded_image_temp_path
+    basename = 'banana_sample.gif'
+    orig_path = File.join(Rails.root, 'spec', 'fixtures', basename)
+    tmp_path = File.join(Rails.root, 'tmp', 'tests', basename)
+    # Because we use 'move_to_store' on all uploaders, we create a new
+    # tempfile on each call: the file we return here will be renamed in most
+    # cases.
+    FileUtils.copy(orig_path, tmp_path)
+    tmp_path
+  end
+end
diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6098be5cd45508848298b28c4016fd0ccb74b8aa
--- /dev/null
+++ b/spec/uploaders/attachment_uploader_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe AttachmentUploader do
+  let(:issue) { build(:issue) }
+  subject { described_class.new(issue) }
+
+  describe '#move_to_cache' do
+    it 'is true' do
+      expect(subject.move_to_cache).to eq(true)
+    end
+  end
+
+  describe '#move_to_store' do
+    it 'is true' do
+      expect(subject.move_to_store).to eq(true)
+    end
+  end
+end
diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1f0e8732587460ba58c229ab131dcd3f8b05d143
--- /dev/null
+++ b/spec/uploaders/avatar_uploader_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe AvatarUploader do
+  let(:user) { build(:user) }
+  subject { described_class.new(user) }
+
+  describe '#move_to_cache' do
+    it 'is true' do
+      expect(subject.move_to_cache).to eq(true)
+    end
+  end
+
+  describe '#move_to_store' do
+    it 'is true' do
+      expect(subject.move_to_store).to eq(true)
+    end
+  end
+end
diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb
index e8300abed5dc201ecf5cc4e01e2afe0d3a661a9d..bc86adfef6828c6927f1a7e8c8bbb664c5cbab14 100644
--- a/spec/uploaders/file_uploader_spec.rb
+++ b/spec/uploaders/file_uploader_spec.rb
@@ -42,4 +42,16 @@ describe FileUploader do
       expect(@uploader.image_or_video?).to be false
     end
   end
+
+  describe '#move_to_cache' do
+    it 'is true' do
+      expect(@uploader.move_to_cache).to eq(true)
+    end
+  end
+
+  describe '#move_to_store' do
+    it 'is true' do
+      expect(@uploader.move_to_store).to eq(true)
+    end
+  end
 end
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.eot b/vendor/assets/fonts/KaTeX_AMS-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..784276a3cbfb3d3b93c974594ffaf70514ab0eb7
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.ttf b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..6f1e0be2028f8c9ccb9ca055f9091fd8495c2975
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff b/vendor/assets/fonts/KaTeX_AMS-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..4dded4733b3fd85fdb1c00352e30d8efa3f1a426
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..ea81079c4e2ec7160db9fe41ffa1ab70098f3d45
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_AMS-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot
new file mode 100644
index 0000000000000000000000000000000000000000..1a0db0c568e4697d56785a687649e98e9bacda7b
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..b94907dad11e545adaf8fb1aaf54c032b7de1796
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff
new file mode 100644
index 0000000000000000000000000000000000000000..799fa8122ca184fff98b8f0424ae6949f8668984
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..73bb54228786739b75b05af392000dd214486580
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Bold.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..6cc83d0922c961a8e94dc0460bb583f14b6a041e
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..cf51e2021e4d84b7fa8d19820387742138b5aedb
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..f5e5c623577a08e9c85da33e2bd383e32fe18c28
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..dd76d3488d5b1722e8f929349a978d8cd953c4c2
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Caligraphic-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot
new file mode 100644
index 0000000000000000000000000000000000000000..1960b106656db1de5df7206db47cb445c8e906dc
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..7b0790f1ae89994c5e7a54b219c013f80166cc18
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff
new file mode 100644
index 0000000000000000000000000000000000000000..dc325713291c91c9fef5967e0d52d0366704715e
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..fdc429227adb718aabf201bed2075bfbaff70a75
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Bold.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..e4e73796aea73b5a7bf372f36a27d6703679a7ca
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..063bc0263eba6fff6ab62d728b5ef666b134f5e1
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..c4b18d863f31c568adb0b8de64ff2368cad1b656
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..4318d938e26c1dd40212772f4c9984b7fbce6adf
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Fraktur-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.eot b/vendor/assets/fonts/KaTeX_Main-Bold.eot
new file mode 100644
index 0000000000000000000000000000000000000000..80fbd022363e7ee84f211c9f814ffa479d588815
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.ttf b/vendor/assets/fonts/KaTeX_Main-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..8e10722afae5f5f0d9ac4dc5245d445db9151f2c
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff b/vendor/assets/fonts/KaTeX_Main-Bold.woff
new file mode 100644
index 0000000000000000000000000000000000000000..43b361a6005e845717c8de2fbc8980830acfceb2
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Bold.woff2 b/vendor/assets/fonts/KaTeX_Main-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..af57a96c1488802f4a5a3d59cac03ad04860ea84
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Bold.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.eot b/vendor/assets/fonts/KaTeX_Main-Italic.eot
new file mode 100644
index 0000000000000000000000000000000000000000..fc770166b5e86b6a9c18440231ec09449c89b634
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.ttf b/vendor/assets/fonts/KaTeX_Main-Italic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..d124495d7b68caa27fb15435bf0004bfe058e5a6
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff b/vendor/assets/fonts/KaTeX_Main-Italic.woff
new file mode 100644
index 0000000000000000000000000000000000000000..e623236bc44485389e5a1d77937699967818bbca
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Italic.woff2 b/vendor/assets/fonts/KaTeX_Main-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..944e9740bdf61ca4f437f48f44133253c7caf052
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Italic.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.eot b/vendor/assets/fonts/KaTeX_Main-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..dc60c090c7a31a74ffa16acea85ddb4e715ccc5f
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.ttf b/vendor/assets/fonts/KaTeX_Main-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..da5797ffcce7d6bcf6155a5744398fc92ed15cc4
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff b/vendor/assets/fonts/KaTeX_Main-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..37db672e821041b2232c741848d4a37052fda363
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Main-Regular.woff2 b/vendor/assets/fonts/KaTeX_Main-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..4882042489387f004bbfd83fa1aaa68d3655cc48
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Main-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot
new file mode 100644
index 0000000000000000000000000000000000000000..52c8b8c6b4089c75de5f6f82635154be2188afcb
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..a8b527c7ef6b56119ef869c96947eb0f3228c20f
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff
new file mode 100644
index 0000000000000000000000000000000000000000..8940e0b58013e7002cc8af7f77c2ad78460f9411
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..15cf56d3408b3d1afa9ccae05dae5de94e91cdce
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-BoldItalic.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.eot b/vendor/assets/fonts/KaTeX_Math-Italic.eot
new file mode 100644
index 0000000000000000000000000000000000000000..64c8992c477ed0007844d3cfd3f0f962eb91ac89
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.ttf b/vendor/assets/fonts/KaTeX_Math-Italic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..06f39d3a29906ba9c26675cb57188dd25a57609d
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff b/vendor/assets/fonts/KaTeX_Math-Italic.woff
new file mode 100644
index 0000000000000000000000000000000000000000..cf3b4b79e5b940e9e7a48b79022e1ed82fcf2786
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Italic.woff2 b/vendor/assets/fonts/KaTeX_Math-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..5f8c4bfa455259e3e0a1c53c8fa399fe2a4ead59
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Italic.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.eot b/vendor/assets/fonts/KaTeX_Math-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..5521e6a564de3076d4b790a9db8eb302333a2916
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.ttf b/vendor/assets/fonts/KaTeX_Math-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..731270823704787f06e136c49e588614fa39ab68
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff b/vendor/assets/fonts/KaTeX_Math-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..0e2ebdf18af76d3f8af92b55f7c76a60e30a9c08
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Math-Regular.woff2 b/vendor/assets/fonts/KaTeX_Math-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..ebe3d028a34a343fe4d31706cf3d2644d646cfad
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Math-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot
new file mode 100644
index 0000000000000000000000000000000000000000..1660e76a2b6ea179340472720b3cbd15acf1ce9f
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.eot differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..dbeb7b92ab57a2687b8bcd80350520205127b6bc
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff
new file mode 100644
index 0000000000000000000000000000000000000000..8f144a8bb312fa160afff853133665554eee9065
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..329e85557fa22c84800d38d8e0d28e30bbd6543f
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Bold.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot
new file mode 100644
index 0000000000000000000000000000000000000000..289ae3ff8b793b05b342aa29ff2d7c0447196368
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.eot differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..b3a2f38f22466e837cc0a1e8bfe166ab86787496
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff
new file mode 100644
index 0000000000000000000000000000000000000000..bddf7ea6579bc13bd4455a14c5f380e69cbd6b50
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..5fa767bddd69dde2a3a8a009667c5555161058ef
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Italic.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..1b38b98a180c93c0dee50f491c520265029a98e9
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..e4712f847754250be2596ca801da591b0f558416
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..33be368048f7e28c8b87fe4631339a801a09b871
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..4fcb2e29a05c2a1c1894982b7d63d916a755faac
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_SansSerif-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.eot b/vendor/assets/fonts/KaTeX_Script-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..7870d7f319b6efba6bba9502db33fb4b73129403
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.ttf b/vendor/assets/fonts/KaTeX_Script-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..da4d11308aeecb950b6a4f669ef1d4b05746f887
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff b/vendor/assets/fonts/KaTeX_Script-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..d6ae79f998a341c1b0fc5a9292ca8403f734d23d
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Script-Regular.woff2 b/vendor/assets/fonts/KaTeX_Script-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..1b43deb45a854d69d134865608f7ddf128e654c3
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Script-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.eot b/vendor/assets/fonts/KaTeX_Size1-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..29950f95ff6a625cfe0435b01d5fbe6b6270c938
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.ttf b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..194466a655d38d8c0c112b39e7b04c5182670c6d
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff b/vendor/assets/fonts/KaTeX_Size1-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..237f271edd1ca85a9cb78d12c142f22deec6788e
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..39b6f8f746c39c8c2faa9188de664475443d6b91
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size1-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.eot b/vendor/assets/fonts/KaTeX_Size2-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..b8b0536f96767e3e4235d7795f5bb5bb6b7d03f6
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.ttf b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..b41b66a638ff64d009950af0a9463561ba552290
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff b/vendor/assets/fonts/KaTeX_Size2-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..4a3055854ede4eaf25de2364da0fea0a05858cfc
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..3facec1ab899c8b984146e54051efafb948eb08c
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size2-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.eot b/vendor/assets/fonts/KaTeX_Size3-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..576b864fae6e163967a7fbe5f6da41c8076d6210
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.ttf b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..790ddbbc55fdd3e4bf42f567e4f5f3b7aee5c86a
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff b/vendor/assets/fonts/KaTeX_Size3-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..3a6d062e660d3a9f71b0230e601dcaff83638430
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..2cffafe5018f6e8a7988d6e277d2cf2177968aca
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size3-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.eot b/vendor/assets/fonts/KaTeX_Size4-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..c2b045fc3dbebff9ecede9a4a1fcf9080633fb15
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.ttf b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..ce660aa7ff91c74b859c521bded76a3fe2150b5b
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff b/vendor/assets/fonts/KaTeX_Size4-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..7826c6c97a108759a64d1fd402428ad73c795898
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..c92189812d98fdf4feb8eafcc81481778f039f65
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Size4-Regular.woff2 differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot
new file mode 100644
index 0000000000000000000000000000000000000000..4c178f484a8d7369a4142755c30ae4a0e12e8d5d
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.eot differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf
new file mode 100644
index 0000000000000000000000000000000000000000..b0427ad0a56ca0eb58c035ebadbc7bc3ebe7fae0
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.ttf differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff
new file mode 100644
index 0000000000000000000000000000000000000000..78e990488a9b61630e28263b73157205c0aeda96
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff differ
diff --git a/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2
new file mode 100644
index 0000000000000000000000000000000000000000..618de99d480f9a5dfeabce556f3a94bd31de1a2c
Binary files /dev/null and b/vendor/assets/fonts/KaTeX_Typewriter-Regular.woff2 differ
diff --git a/vendor/assets/javascripts/katex.js b/vendor/assets/javascripts/katex.js
new file mode 100644
index 0000000000000000000000000000000000000000..beb31ca6a7ea943bd6420686d9bc646988b9c57a
--- /dev/null
+++ b/vendor/assets/javascripts/katex.js
@@ -0,0 +1,8683 @@
+/*
+ The MIT License (MIT)
+
+ Copyright (c) 2015 Khan Academy
+
+ This software also uses portions of the underscore.js project, which is
+ MIT licensed with the following copyright:
+
+ Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
+ Reporters & Editors
+
+ 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.
+ */
+
+/*
+ Here is how to build a version of KaTeX that works with gitlab.
+
+ The problem is that the standard procedure for changing font location doesn't work for the empty string.
+
+ 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
+ 2. make (requires node)
+ 3. sed -i 's,fonts/,,' build/katex.css
+ 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
+*/
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.katex = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This is the main entry point for KaTeX. Here, we expose functions for
+ * rendering expressions either to DOM nodes or to markup strings.
+ *
+ * We also expose the ParseError class to check if errors thrown from KaTeX are
+ * errors in the expression, or errors in javascript handling.
+ */
+
+var ParseError = require("./src/ParseError");
+var Settings = require("./src/Settings");
+
+var buildTree = require("./src/buildTree");
+var parseTree = require("./src/parseTree");
+var utils = require("./src/utils");
+
+/**
+ * Parse and build an expression, and place that expression in the DOM node
+ * given.
+ */
+var render = function(expression, baseNode, options) {
+    utils.clearNode(baseNode);
+
+    var settings = new Settings(options);
+
+    var tree = parseTree(expression, settings);
+    var node = buildTree(tree, expression, settings).toNode();
+
+    baseNode.appendChild(node);
+};
+
+// KaTeX's styles don't work properly in quirks mode. Print out an error, and
+// disable rendering.
+if (typeof document !== "undefined") {
+    if (document.compatMode !== "CSS1Compat") {
+        typeof console !== "undefined" && console.warn(
+            "Warning: KaTeX doesn't work in quirks mode. Make sure your " +
+                "website has a suitable doctype.");
+
+        render = function() {
+            throw new ParseError("KaTeX doesn't work in quirks mode.");
+        };
+    }
+}
+
+/**
+ * Parse and build an expression, and return the markup for that.
+ */
+var renderToString = function(expression, options) {
+    var settings = new Settings(options);
+
+    var tree = parseTree(expression, settings);
+    return buildTree(tree, expression, settings).toMarkup();
+};
+
+/**
+ * Parse an expression and return the parse tree.
+ */
+var generateParseTree = function(expression, options) {
+    var settings = new Settings(options);
+    return parseTree(expression, settings);
+};
+
+module.exports = {
+    render: render,
+    renderToString: renderToString,
+    /**
+     * NOTE: This method is not currently recommended for public use.
+     * The internal tree representation is unstable and is very likely
+     * to change. Use at your own risk.
+     */
+    __parse: generateParseTree,
+    ParseError: ParseError,
+};
+
+},{"./src/ParseError":6,"./src/Settings":8,"./src/buildTree":13,"./src/parseTree":22,"./src/utils":25}],2:[function(require,module,exports){
+/** @flow */
+
+"use strict";
+
+function getRelocatable(re) {
+  // In the future, this could use a WeakMap instead of an expando.
+  if (!re.__matchAtRelocatable) {
+    // Disjunctions are the lowest-precedence operator, so we can make any
+    // pattern match the empty string by appending `|()` to it:
+    // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-patterns
+    var source = re.source + "|()";
+
+    // We always make the new regex global.
+    var flags = "g" + (re.ignoreCase ? "i" : "") + (re.multiline ? "m" : "") + (re.unicode ? "u" : "")
+    // sticky (/.../y) doesn't make sense in conjunction with our relocation
+    // logic, so we ignore it here.
+    ;
+
+    re.__matchAtRelocatable = new RegExp(source, flags);
+  }
+  return re.__matchAtRelocatable;
+}
+
+function matchAt(re, str, pos) {
+  if (re.global || re.sticky) {
+    throw new Error("matchAt(...): Only non-global regexes are supported");
+  }
+  var reloc = getRelocatable(re);
+  reloc.lastIndex = pos;
+  var match = reloc.exec(str);
+  // Last capturing group is our sentinel that indicates whether the regex
+  // matched at the given location.
+  if (match[match.length - 1] == null) {
+    // Original regex matched.
+    match.length = match.length - 1;
+    return match;
+  } else {
+    return null;
+  }
+}
+
+module.exports = matchAt;
+},{}],3:[function(require,module,exports){
+/**
+ * The Lexer class handles tokenizing the input in various ways. Since our
+ * parser expects us to be able to backtrack, the lexer allows lexing from any
+ * given starting point.
+ *
+ * Its main exposed function is the `lex` function, which takes a position to
+ * lex from and a type of token to lex. It defers to the appropriate `_innerLex`
+ * function.
+ *
+ * The various `_innerLex` functions perform the actual lexing of different
+ * kinds.
+ */
+
+var matchAt = require("match-at");
+
+var ParseError = require("./ParseError");
+
+// The main lexer class
+function Lexer(input) {
+    this.input = input;
+    this.pos = 0;
+}
+
+/**
+ * The resulting token returned from `lex`.
+ *
+ * It consists of the token text plus some position information.
+ * The position information is essentially a range in an input string,
+ * but instead of referencing the bare input string, we refer to the lexer.
+ * That way it is possible to attach extra metadata to the input string,
+ * like for example a file name or similar.
+ *
+ * The position information (all three parameters) is optional,
+ * so it is OK to construct synthetic tokens if appropriate.
+ * Not providing available position information may lead to
+ * degraded error reporting, though.
+ *
+ * @param {string}  text   the text of this token
+ * @param {number=} start  the start offset, zero-based inclusive
+ * @param {number=} end    the end offset, zero-based exclusive
+ * @param {Lexer=}  lexer  the lexer which in turn holds the input string
+ */
+function Token(text, start, end, lexer) {
+    this.text = text;
+    this.start = start;
+    this.end = end;
+    this.lexer = lexer;
+}
+
+/**
+ * Given a pair of tokens (this and endToken), compute a “Token” encompassing
+ * the whole input range enclosed by these two.
+ *
+ * @param {Token}  endToken  last token of the range, inclusive
+ * @param {string} text      the text of the newly constructed token
+ */
+Token.prototype.range = function(endToken, text) {
+    if (endToken.lexer !== this.lexer) {
+        return new Token(text); // sorry, no position information available
+    }
+    return new Token(text, this.start, endToken.end, this.lexer);
+};
+
+/* The following tokenRegex
+ * - matches typical whitespace (but not NBSP etc.) using its first group
+ * - does not match any control character \x00-\x1f except whitespace
+ * - does not match a bare backslash
+ * - matches any ASCII character except those just mentioned
+ * - does not match the BMP private use area \uE000-\uF8FF
+ * - does not match bare surrogate code units
+ * - matches any BMP character except for those just described
+ * - matches any valid Unicode surrogate pair
+ * - matches a backslash followed by one or more letters
+ * - matches a backslash followed by any BMP character, including newline
+ * Just because the Lexer matches something doesn't mean it's valid input:
+ * If there is no matching function or symbol definition, the Parser will
+ * still reject the input.
+ */
+var tokenRegex = new RegExp(
+    "([ \r\n\t]+)|" +                                 // whitespace
+    "([!-\\[\\]-\u2027\u202A-\uD7FF\uF900-\uFFFF]" +  // single codepoint
+    "|[\uD800-\uDBFF][\uDC00-\uDFFF]" +               // surrogate pair
+    "|\\\\(?:[a-zA-Z]+|[^\uD800-\uDFFF])" +           // function name
+    ")"
+);
+
+/**
+ * This function lexes a single token.
+ */
+Lexer.prototype.lex = function() {
+    var input = this.input;
+    var pos = this.pos;
+    if (pos === input.length) {
+        return new Token("EOF", pos, pos, this);
+    }
+    var match = matchAt(tokenRegex, input, pos);
+    if (match === null) {
+        throw new ParseError(
+            "Unexpected character: '" + input[pos] + "'",
+            new Token(input[pos], pos, pos + 1, this));
+    }
+    var text = match[2] || " ";
+    var start = this.pos;
+    this.pos += match[0].length;
+    var end = this.pos;
+    return new Token(text, start, end, this);
+};
+
+module.exports = Lexer;
+
+},{"./ParseError":6,"match-at":2}],4:[function(require,module,exports){
+/**
+ * This file contains the “gullet” where macros are expanded
+ * until only non-macro tokens remain.
+ */
+
+var Lexer = require("./Lexer");
+
+function MacroExpander(input, macros) {
+    this.lexer = new Lexer(input);
+    this.macros = macros;
+    this.stack = []; // contains tokens in REVERSE order
+    this.discardedWhiteSpace = [];
+}
+
+/**
+ * Recursively expand first token, then return first non-expandable token.
+ */
+MacroExpander.prototype.nextToken = function() {
+    for (;;) {
+        if (this.stack.length === 0) {
+            this.stack.push(this.lexer.lex());
+        }
+        var topToken = this.stack.pop();
+        var name = topToken.text;
+        if (!(name.charAt(0) === "\\" && this.macros.hasOwnProperty(name))) {
+            return topToken;
+        }
+        var expansion = this.macros[name];
+        if (typeof expansion === "string") {
+            var bodyLexer = new Lexer(expansion);
+            expansion = [];
+            var tok = bodyLexer.lex();
+            while (tok.text !== "EOF") {
+                expansion.push(tok);
+                tok = bodyLexer.lex();
+            }
+            expansion.reverse(); // to fit in with stack using push and pop
+            this.macros[name] = expansion;
+        }
+        this.stack = this.stack.concat(expansion);
+    }
+};
+
+MacroExpander.prototype.get = function(ignoreSpace) {
+    this.discardedWhiteSpace = [];
+    var token = this.nextToken();
+    if (ignoreSpace) {
+        while (token.text === " ") {
+            this.discardedWhiteSpace.push(token);
+            token = this.nextToken();
+        }
+    }
+    return token;
+};
+
+/**
+ * Undo the effect of the preceding call to the get method.
+ * A call to this method MUST be immediately preceded and immediately followed
+ * by a call to get.  Only used during mode switching, i.e. after one token
+ * was got in the old mode but should get got again in a new mode
+ * with possibly different whitespace handling.
+ */
+MacroExpander.prototype.unget = function(token) {
+    this.stack.push(token);
+    while (this.discardedWhiteSpace.length !== 0) {
+        this.stack.push(this.discardedWhiteSpace.pop());
+    }
+};
+
+module.exports = MacroExpander;
+
+},{"./Lexer":3}],5:[function(require,module,exports){
+/**
+ * This file contains information about the options that the Parser carries
+ * around with it while parsing. Data is held in an `Options` object, and when
+ * recursing, a new `Options` object can be created with the `.with*` and
+ * `.reset` functions.
+ */
+
+/**
+ * This is the main options class. It contains the style, size, color, and font
+ * of the current parse level. It also contains the style and size of the parent
+ * parse level, so size changes can be handled efficiently.
+ *
+ * Each of the `.with*` and `.reset` functions passes its current style and size
+ * as the parentStyle and parentSize of the new options class, so parent
+ * handling is taken care of automatically.
+ */
+function Options(data) {
+    this.style = data.style;
+    this.color = data.color;
+    this.size = data.size;
+    this.phantom = data.phantom;
+    this.font = data.font;
+
+    if (data.parentStyle === undefined) {
+        this.parentStyle = data.style;
+    } else {
+        this.parentStyle = data.parentStyle;
+    }
+
+    if (data.parentSize === undefined) {
+        this.parentSize = data.size;
+    } else {
+        this.parentSize = data.parentSize;
+    }
+}
+
+/**
+ * Returns a new options object with the same properties as "this".  Properties
+ * from "extension" will be copied to the new options object.
+ */
+Options.prototype.extend = function(extension) {
+    var data = {
+        style: this.style,
+        size: this.size,
+        color: this.color,
+        parentStyle: this.style,
+        parentSize: this.size,
+        phantom: this.phantom,
+        font: this.font,
+    };
+
+    for (var key in extension) {
+        if (extension.hasOwnProperty(key)) {
+            data[key] = extension[key];
+        }
+    }
+
+    return new Options(data);
+};
+
+/**
+ * Create a new options object with the given style.
+ */
+Options.prototype.withStyle = function(style) {
+    return this.extend({
+        style: style,
+    });
+};
+
+/**
+ * Create a new options object with the given size.
+ */
+Options.prototype.withSize = function(size) {
+    return this.extend({
+        size: size,
+    });
+};
+
+/**
+ * Create a new options object with the given color.
+ */
+Options.prototype.withColor = function(color) {
+    return this.extend({
+        color: color,
+    });
+};
+
+/**
+ * Create a new options object with "phantom" set to true.
+ */
+Options.prototype.withPhantom = function() {
+    return this.extend({
+        phantom: true,
+    });
+};
+
+/**
+ * Create a new options objects with the give font.
+ */
+Options.prototype.withFont = function(font) {
+    return this.extend({
+        font: font,
+    });
+};
+
+/**
+ * Create a new options object with the same style, size, and color. This is
+ * used so that parent style and size changes are handled correctly.
+ */
+Options.prototype.reset = function() {
+    return this.extend({});
+};
+
+/**
+ * A map of color names to CSS colors.
+ * TODO(emily): Remove this when we have real macros
+ */
+var colorMap = {
+    "katex-blue": "#6495ed",
+    "katex-orange": "#ffa500",
+    "katex-pink": "#ff00af",
+    "katex-red": "#df0030",
+    "katex-green": "#28ae7b",
+    "katex-gray": "gray",
+    "katex-purple": "#9d38bd",
+    "katex-blueA": "#ccfaff",
+    "katex-blueB": "#80f6ff",
+    "katex-blueC": "#63d9ea",
+    "katex-blueD": "#11accd",
+    "katex-blueE": "#0c7f99",
+    "katex-tealA": "#94fff5",
+    "katex-tealB": "#26edd5",
+    "katex-tealC": "#01d1c1",
+    "katex-tealD": "#01a995",
+    "katex-tealE": "#208170",
+    "katex-greenA": "#b6ffb0",
+    "katex-greenB": "#8af281",
+    "katex-greenC": "#74cf70",
+    "katex-greenD": "#1fab54",
+    "katex-greenE": "#0d923f",
+    "katex-goldA": "#ffd0a9",
+    "katex-goldB": "#ffbb71",
+    "katex-goldC": "#ff9c39",
+    "katex-goldD": "#e07d10",
+    "katex-goldE": "#a75a05",
+    "katex-redA": "#fca9a9",
+    "katex-redB": "#ff8482",
+    "katex-redC": "#f9685d",
+    "katex-redD": "#e84d39",
+    "katex-redE": "#bc2612",
+    "katex-maroonA": "#ffbde0",
+    "katex-maroonB": "#ff92c6",
+    "katex-maroonC": "#ed5fa6",
+    "katex-maroonD": "#ca337c",
+    "katex-maroonE": "#9e034e",
+    "katex-purpleA": "#ddd7ff",
+    "katex-purpleB": "#c6b9fc",
+    "katex-purpleC": "#aa87ff",
+    "katex-purpleD": "#7854ab",
+    "katex-purpleE": "#543b78",
+    "katex-mintA": "#f5f9e8",
+    "katex-mintB": "#edf2df",
+    "katex-mintC": "#e0e5cc",
+    "katex-grayA": "#f6f7f7",
+    "katex-grayB": "#f0f1f2",
+    "katex-grayC": "#e3e5e6",
+    "katex-grayD": "#d6d8da",
+    "katex-grayE": "#babec2",
+    "katex-grayF": "#888d93",
+    "katex-grayG": "#626569",
+    "katex-grayH": "#3b3e40",
+    "katex-grayI": "#21242c",
+    "katex-kaBlue": "#314453",
+    "katex-kaGreen": "#71B307",
+};
+
+/**
+ * Gets the CSS color of the current options object, accounting for the
+ * `colorMap`.
+ */
+Options.prototype.getColor = function() {
+    if (this.phantom) {
+        return "transparent";
+    } else {
+        return colorMap[this.color] || this.color;
+    }
+};
+
+module.exports = Options;
+
+},{}],6:[function(require,module,exports){
+/**
+ * This is the ParseError class, which is the main error thrown by KaTeX
+ * functions when something has gone wrong. This is used to distinguish internal
+ * errors from errors in the expression that the user provided.
+ *
+ * If possible, a caller should provide a Token or ParseNode with information
+ * about where in the source string the problem occurred.
+ *
+ * @param {string} message  The error message
+ * @param {(Token|ParseNode)=} token  An object providing position information
+ */
+function ParseError(message, token) {
+    var error = "KaTeX parse error: " + message;
+    var start;
+    var end;
+
+    if (token && token.lexer && token.start <= token.end) {
+        // If we have the input and a position, make the error a bit fancier
+
+        // Get the input
+        var input = token.lexer.input;
+
+        // Prepend some information
+        start = token.start;
+        end = token.end;
+        if (start === input.length) {
+            error += " at end of input: ";
+        } else {
+            error += " at position " + (start + 1) + ": ";
+        }
+
+        // Underline token in question using combining underscores
+        var underlined = input.slice(start, end).replace(/[^]/g, "$&\u0332");
+
+        // Extract some context from the input and add it to the error
+        var left;
+        if (start > 15) {
+            left = "…" + input.slice(start - 15, start);
+        } else {
+            left = input.slice(0, start);
+        }
+        var right;
+        if (end + 15 < input.length) {
+            right = input.slice(end, end + 15) + "…";
+        } else {
+            right = input.slice(end);
+        }
+        error += left + underlined + right;
+    }
+
+    // Some hackery to make ParseError a prototype of Error
+    // See http://stackoverflow.com/a/8460753
+    var self = new Error(error);
+    self.name = "ParseError";
+    self.__proto__ = ParseError.prototype;
+
+    self.position = start;
+    return self;
+}
+
+// More hackery
+ParseError.prototype.__proto__ = Error.prototype;
+
+module.exports = ParseError;
+
+},{}],7:[function(require,module,exports){
+/* eslint no-constant-condition:0 */
+var functions = require("./functions");
+var environments = require("./environments");
+var MacroExpander = require("./MacroExpander");
+var symbols = require("./symbols");
+var utils = require("./utils");
+var cjkRegex = require("./unicodeRegexes").cjkRegex;
+
+var parseData = require("./parseData");
+var ParseError = require("./ParseError");
+
+/**
+ * This file contains the parser used to parse out a TeX expression from the
+ * input. Since TeX isn't context-free, standard parsers don't work particularly
+ * well.
+ *
+ * The strategy of this parser is as such:
+ *
+ * The main functions (the `.parse...` ones) take a position in the current
+ * parse string to parse tokens from. The lexer (found in Lexer.js, stored at
+ * this.lexer) also supports pulling out tokens at arbitrary places. When
+ * individual tokens are needed at a position, the lexer is called to pull out a
+ * token, which is then used.
+ *
+ * The parser has a property called "mode" indicating the mode that
+ * the parser is currently in. Currently it has to be one of "math" or
+ * "text", which denotes whether the current environment is a math-y
+ * one or a text-y one (e.g. inside \text). Currently, this serves to
+ * limit the functions which can be used in text mode.
+ *
+ * The main functions then return an object which contains the useful data that
+ * was parsed at its given point, and a new position at the end of the parsed
+ * data. The main functions can call each other and continue the parsing by
+ * using the returned position as a new starting point.
+ *
+ * There are also extra `.handle...` functions, which pull out some reused
+ * functionality into self-contained functions.
+ *
+ * The earlier functions return ParseNodes.
+ * The later functions (which are called deeper in the parse) sometimes return
+ * ParseFuncOrArgument, which contain a ParseNode as well as some data about
+ * whether the parsed object is a function which is missing some arguments, or a
+ * standalone object which can be used as an argument to another function.
+ */
+
+/**
+ * Main Parser class
+ */
+function Parser(input, settings) {
+    // Create a new macro expander (gullet) and (indirectly via that) also a
+    // new lexer (mouth) for this parser (stomach, in the language of TeX)
+    this.gullet = new MacroExpander(input, settings.macros);
+    // Store the settings for use in parsing
+    this.settings = settings;
+}
+
+var ParseNode = parseData.ParseNode;
+
+/**
+ * An initial function (without its arguments), or an argument to a function.
+ * The `result` argument should be a ParseNode.
+ */
+function ParseFuncOrArgument(result, isFunction, token) {
+    this.result = result;
+    // Is this a function (i.e. is it something defined in functions.js)?
+    this.isFunction = isFunction;
+    this.token = token;
+}
+
+/**
+ * Checks a result to make sure it has the right type, and throws an
+ * appropriate error otherwise.
+ *
+ * @param {boolean=} consume whether to consume the expected token,
+ *                           defaults to true
+ */
+Parser.prototype.expect = function(text, consume) {
+    if (this.nextToken.text !== text) {
+        throw new ParseError(
+            "Expected '" + text + "', got '" + this.nextToken.text + "'",
+            this.nextToken
+        );
+    }
+    if (consume !== false) {
+        this.consume();
+    }
+};
+
+/**
+ * Considers the current look ahead token as consumed,
+ * and fetches the one after that as the new look ahead.
+ */
+Parser.prototype.consume = function() {
+    this.nextToken = this.gullet.get(this.mode === "math");
+};
+
+Parser.prototype.switchMode = function(newMode) {
+    this.gullet.unget(this.nextToken);
+    this.mode = newMode;
+    this.consume();
+};
+
+/**
+ * Main parsing function, which parses an entire input.
+ *
+ * @return {?Array.<ParseNode>}
+ */
+Parser.prototype.parse = function() {
+    // Try to parse the input
+    this.mode = "math";
+    this.consume();
+    var parse = this.parseInput();
+    return parse;
+};
+
+/**
+ * Parses an entire input tree.
+ */
+Parser.prototype.parseInput = function() {
+    // Parse an expression
+    var expression = this.parseExpression(false);
+    // If we succeeded, make sure there's an EOF at the end
+    this.expect("EOF", false);
+    return expression;
+};
+
+var endOfExpression = ["}", "\\end", "\\right", "&", "\\\\", "\\cr"];
+
+/**
+ * Parses an "expression", which is a list of atoms.
+ *
+ * @param {boolean} breakOnInfix  Should the parsing stop when we hit infix
+ *                  nodes? This happens when functions have higher precendence
+ *                  than infix nodes in implicit parses.
+ *
+ * @param {?string} breakOnTokenText  The text of the token that the expression
+ *                  should end with, or `null` if something else should end the
+ *                  expression.
+ *
+ * @return {ParseNode}
+ */
+Parser.prototype.parseExpression = function(breakOnInfix, breakOnTokenText) {
+    var body = [];
+    // Keep adding atoms to the body until we can't parse any more atoms (either
+    // we reached the end, a }, or a \right)
+    while (true) {
+        var lex = this.nextToken;
+        if (endOfExpression.indexOf(lex.text) !== -1) {
+            break;
+        }
+        if (breakOnTokenText && lex.text === breakOnTokenText) {
+            break;
+        }
+        if (breakOnInfix && functions[lex.text] && functions[lex.text].infix) {
+            break;
+        }
+        var atom = this.parseAtom();
+        if (!atom) {
+            if (!this.settings.throwOnError && lex.text[0] === "\\") {
+                var errorNode = this.handleUnsupportedCmd();
+                body.push(errorNode);
+                continue;
+            }
+
+            break;
+        }
+        body.push(atom);
+    }
+    return this.handleInfixNodes(body);
+};
+
+/**
+ * Rewrites infix operators such as \over with corresponding commands such
+ * as \frac.
+ *
+ * There can only be one infix operator per group.  If there's more than one
+ * then the expression is ambiguous.  This can be resolved by adding {}.
+ *
+ * @returns {Array}
+ */
+Parser.prototype.handleInfixNodes = function(body) {
+    var overIndex = -1;
+    var funcName;
+
+    for (var i = 0; i < body.length; i++) {
+        var node = body[i];
+        if (node.type === "infix") {
+            if (overIndex !== -1) {
+                throw new ParseError(
+                    "only one infix operator per group",
+                    node.value.token);
+            }
+            overIndex = i;
+            funcName = node.value.replaceWith;
+        }
+    }
+
+    if (overIndex !== -1) {
+        var numerNode;
+        var denomNode;
+
+        var numerBody = body.slice(0, overIndex);
+        var denomBody = body.slice(overIndex + 1);
+
+        if (numerBody.length === 1 && numerBody[0].type === "ordgroup") {
+            numerNode = numerBody[0];
+        } else {
+            numerNode = new ParseNode("ordgroup", numerBody, this.mode);
+        }
+
+        if (denomBody.length === 1 && denomBody[0].type === "ordgroup") {
+            denomNode = denomBody[0];
+        } else {
+            denomNode = new ParseNode("ordgroup", denomBody, this.mode);
+        }
+
+        var value = this.callFunction(
+            funcName, [numerNode, denomNode], null);
+        return [new ParseNode(value.type, value, this.mode)];
+    } else {
+        return body;
+    }
+};
+
+// The greediness of a superscript or subscript
+var SUPSUB_GREEDINESS = 1;
+
+/**
+ * Handle a subscript or superscript with nice errors.
+ */
+Parser.prototype.handleSupSubscript = function(name) {
+    var symbolToken = this.nextToken;
+    var symbol = symbolToken.text;
+    this.consume();
+    var group = this.parseGroup();
+
+    if (!group) {
+        if (!this.settings.throwOnError && this.nextToken.text[0] === "\\") {
+            return this.handleUnsupportedCmd();
+        } else {
+            throw new ParseError(
+                "Expected group after '" + symbol + "'",
+                symbolToken
+            );
+        }
+    } else if (group.isFunction) {
+        // ^ and _ have a greediness, so handle interactions with functions'
+        // greediness
+        var funcGreediness = functions[group.result].greediness;
+        if (funcGreediness > SUPSUB_GREEDINESS) {
+            return this.parseFunction(group);
+        } else {
+            throw new ParseError(
+                "Got function '" + group.result + "' with no arguments " +
+                    "as " + name, symbolToken);
+        }
+    } else {
+        return group.result;
+    }
+};
+
+/**
+ * Converts the textual input of an unsupported command into a text node
+ * contained within a color node whose color is determined by errorColor
+ */
+Parser.prototype.handleUnsupportedCmd = function() {
+    var text = this.nextToken.text;
+    var textordArray = [];
+
+    for (var i = 0; i < text.length; i++) {
+        textordArray.push(new ParseNode("textord", text[i], "text"));
+    }
+
+    var textNode = new ParseNode(
+        "text",
+        {
+            body: textordArray,
+            type: "text",
+        },
+        this.mode);
+
+    var colorNode = new ParseNode(
+        "color",
+        {
+            color: this.settings.errorColor,
+            value: [textNode],
+            type: "color",
+        },
+        this.mode);
+
+    this.consume();
+    return colorNode;
+};
+
+/**
+ * Parses a group with optional super/subscripts.
+ *
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseAtom = function() {
+    // The body of an atom is an implicit group, so that things like
+    // \left(x\right)^2 work correctly.
+    var base = this.parseImplicitGroup();
+
+    // In text mode, we don't have superscripts or subscripts
+    if (this.mode === "text") {
+        return base;
+    }
+
+    // Note that base may be empty (i.e. null) at this point.
+
+    var superscript;
+    var subscript;
+    while (true) {
+        // Lex the first token
+        var lex = this.nextToken;
+
+        if (lex.text === "\\limits" || lex.text === "\\nolimits") {
+            // We got a limit control
+            if (!base || base.type !== "op") {
+                throw new ParseError(
+                    "Limit controls must follow a math operator",
+                    lex);
+            } else {
+                var limits = lex.text === "\\limits";
+                base.value.limits = limits;
+                base.value.alwaysHandleSupSub = true;
+            }
+            this.consume();
+        } else if (lex.text === "^") {
+            // We got a superscript start
+            if (superscript) {
+                throw new ParseError("Double superscript", lex);
+            }
+            superscript = this.handleSupSubscript("superscript");
+        } else if (lex.text === "_") {
+            // We got a subscript start
+            if (subscript) {
+                throw new ParseError("Double subscript", lex);
+            }
+            subscript = this.handleSupSubscript("subscript");
+        } else if (lex.text === "'") {
+            // We got a prime
+            var prime = new ParseNode("textord", "\\prime", this.mode);
+
+            // Many primes can be grouped together, so we handle this here
+            var primes = [prime];
+            this.consume();
+            // Keep lexing tokens until we get something that's not a prime
+            while (this.nextToken.text === "'") {
+                // For each one, add another prime to the list
+                primes.push(prime);
+                this.consume();
+            }
+            // Put them into an ordgroup as the superscript
+            superscript = new ParseNode("ordgroup", primes, this.mode);
+        } else {
+            // If it wasn't ^, _, or ', stop parsing super/subscripts
+            break;
+        }
+    }
+
+    if (superscript || subscript) {
+        // If we got either a superscript or subscript, create a supsub
+        return new ParseNode("supsub", {
+            base: base,
+            sup: superscript,
+            sub: subscript,
+        }, this.mode);
+    } else {
+        // Otherwise return the original body
+        return base;
+    }
+};
+
+// A list of the size-changing functions, for use in parseImplicitGroup
+var sizeFuncs = [
+    "\\tiny", "\\scriptsize", "\\footnotesize", "\\small", "\\normalsize",
+    "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
+];
+
+// A list of the style-changing functions, for use in parseImplicitGroup
+var styleFuncs = [
+    "\\displaystyle", "\\textstyle", "\\scriptstyle", "\\scriptscriptstyle",
+];
+
+/**
+ * Parses an implicit group, which is a group that starts at the end of a
+ * specified, and ends right before a higher explicit group ends, or at EOL. It
+ * is used for functions that appear to affect the current style, like \Large or
+ * \textrm, where instead of keeping a style we just pretend that there is an
+ * implicit grouping after it until the end of the group. E.g.
+ *   small text {\Large large text} small text again
+ * It is also used for \left and \right to get the correct grouping.
+ *
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseImplicitGroup = function() {
+    var start = this.parseSymbol();
+
+    if (start == null) {
+        // If we didn't get anything we handle, fall back to parseFunction
+        return this.parseFunction();
+    }
+
+    var func = start.result;
+    var body;
+
+    if (func === "\\left") {
+        // If we see a left:
+        // Parse the entire left function (including the delimiter)
+        var left = this.parseFunction(start);
+        // Parse out the implicit body
+        body = this.parseExpression(false);
+        // Check the next token
+        this.expect("\\right", false);
+        var right = this.parseFunction();
+        return new ParseNode("leftright", {
+            body: body,
+            left: left.value.value,
+            right: right.value.value,
+        }, this.mode);
+    } else if (func === "\\begin") {
+        // begin...end is similar to left...right
+        var begin = this.parseFunction(start);
+        var envName = begin.value.name;
+        if (!environments.hasOwnProperty(envName)) {
+            throw new ParseError(
+                "No such environment: " + envName, begin.value.nameGroup);
+        }
+        // Build the environment object. Arguments and other information will
+        // be made available to the begin and end methods using properties.
+        var env = environments[envName];
+        var args = this.parseArguments("\\begin{" + envName + "}", env);
+        var context = {
+            mode: this.mode,
+            envName: envName,
+            parser: this,
+            positions: args.pop(),
+        };
+        var result = env.handler(context, args);
+        this.expect("\\end", false);
+        var endNameToken = this.nextToken;
+        var end = this.parseFunction();
+        if (end.value.name !== envName) {
+            throw new ParseError(
+                "Mismatch: \\begin{" + envName + "} matched " +
+                "by \\end{" + end.value.name + "}",
+                endNameToken);
+        }
+        result.position = end.position;
+        return result;
+    } else if (utils.contains(sizeFuncs, func)) {
+        // If we see a sizing function, parse out the implict body
+        body = this.parseExpression(false);
+        return new ParseNode("sizing", {
+            // Figure out what size to use based on the list of functions above
+            size: "size" + (utils.indexOf(sizeFuncs, func) + 1),
+            value: body,
+        }, this.mode);
+    } else if (utils.contains(styleFuncs, func)) {
+        // If we see a styling function, parse out the implict body
+        body = this.parseExpression(true);
+        return new ParseNode("styling", {
+            // Figure out what style to use by pulling out the style from
+            // the function name
+            style: func.slice(1, func.length - 5),
+            value: body,
+        }, this.mode);
+    } else {
+        // Defer to parseFunction if it's not a function we handle
+        return this.parseFunction(start);
+    }
+};
+
+/**
+ * Parses an entire function, including its base and all of its arguments.
+ * The base might either have been parsed already, in which case
+ * it is provided as an argument, or it's the next group in the input.
+ *
+ * @param {ParseFuncOrArgument=} baseGroup optional as described above
+ * @return {?ParseNode}
+ */
+Parser.prototype.parseFunction = function(baseGroup) {
+    if (!baseGroup) {
+        baseGroup = this.parseGroup();
+    }
+
+    if (baseGroup) {
+        if (baseGroup.isFunction) {
+            var func = baseGroup.result;
+            var funcData = functions[func];
+            if (this.mode === "text" && !funcData.allowedInText) {
+                throw new ParseError(
+                    "Can't use function '" + func + "' in text mode",
+                    baseGroup.token);
+            }
+
+            var args = this.parseArguments(func, funcData);
+            var token = baseGroup.token;
+            var result = this.callFunction(func, args, args.pop(), token);
+            return new ParseNode(result.type, result, this.mode);
+        } else {
+            return baseGroup.result;
+        }
+    } else {
+        return null;
+    }
+};
+
+/**
+ * Call a function handler with a suitable context and arguments.
+ */
+Parser.prototype.callFunction = function(name, args, positions, token) {
+    var context = {
+        funcName: name,
+        parser: this,
+        positions: positions,
+        token: token,
+    };
+    return functions[name].handler(context, args);
+};
+
+/**
+ * Parses the arguments of a function or environment
+ *
+ * @param {string} func  "\name" or "\begin{name}"
+ * @param {{numArgs:number,numOptionalArgs:number|undefined}} funcData
+ * @return the array of arguments, with the list of positions as last element
+ */
+Parser.prototype.parseArguments = function(func, funcData) {
+    var totalArgs = funcData.numArgs + funcData.numOptionalArgs;
+    if (totalArgs === 0) {
+        return [[this.pos]];
+    }
+
+    var baseGreediness = funcData.greediness;
+    var positions = [this.pos];
+    var args = [];
+
+    for (var i = 0; i < totalArgs; i++) {
+        var nextToken = this.nextToken;
+        var argType = funcData.argTypes && funcData.argTypes[i];
+        var arg;
+        if (i < funcData.numOptionalArgs) {
+            if (argType) {
+                arg = this.parseGroupOfType(argType, true);
+            } else {
+                arg = this.parseGroup(true);
+            }
+            if (!arg) {
+                args.push(null);
+                positions.push(this.pos);
+                continue;
+            }
+        } else {
+            if (argType) {
+                arg = this.parseGroupOfType(argType);
+            } else {
+                arg = this.parseGroup();
+            }
+            if (!arg) {
+                if (!this.settings.throwOnError &&
+                    this.nextToken.text[0] === "\\") {
+                    arg = new ParseFuncOrArgument(
+                        this.handleUnsupportedCmd(this.nextToken.text),
+                        false);
+                } else {
+                    throw new ParseError(
+                        "Expected group after '" + func + "'", nextToken);
+                }
+            }
+        }
+        var argNode;
+        if (arg.isFunction) {
+            var argGreediness =
+                functions[arg.result].greediness;
+            if (argGreediness > baseGreediness) {
+                argNode = this.parseFunction(arg);
+            } else {
+                throw new ParseError(
+                    "Got function '" + arg.result + "' as " +
+                    "argument to '" + func + "'", nextToken);
+            }
+        } else {
+            argNode = arg.result;
+        }
+        args.push(argNode);
+        positions.push(this.pos);
+    }
+
+    args.push(positions);
+
+    return args;
+};
+
+
+/**
+ * Parses a group when the mode is changing.
+ *
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseGroupOfType = function(innerMode, optional) {
+    var outerMode = this.mode;
+    // Handle `original` argTypes
+    if (innerMode === "original") {
+        innerMode = outerMode;
+    }
+
+    if (innerMode === "color") {
+        return this.parseColorGroup(optional);
+    }
+    if (innerMode === "size") {
+        return this.parseSizeGroup(optional);
+    }
+
+    this.switchMode(innerMode);
+    if (innerMode === "text") {
+        // text mode is special because it should ignore the whitespace before
+        // it
+        while (this.nextToken.text === " ") {
+            this.consume();
+        }
+    }
+    // By the time we get here, innerMode is one of "text" or "math".
+    // We switch the mode of the parser, recurse, then restore the old mode.
+    var res = this.parseGroup(optional);
+    this.switchMode(outerMode);
+    return res;
+};
+
+/**
+ * Parses a group, essentially returning the string formed by the
+ * brace-enclosed tokens plus some position information.
+ *
+ * @param {string} modeName  Used to describe the mode in error messages
+ * @param {boolean=} optional  Whether the group is optional or required
+ */
+Parser.prototype.parseStringGroup = function(modeName, optional) {
+    if (optional && this.nextToken.text !== "[") {
+        return null;
+    }
+    var outerMode = this.mode;
+    this.mode = "text";
+    this.expect(optional ? "[" : "{");
+    var str = "";
+    var firstToken = this.nextToken;
+    var lastToken = firstToken;
+    while (this.nextToken.text !== (optional ? "]" : "}")) {
+        if (this.nextToken.text === "EOF") {
+            throw new ParseError(
+                "Unexpected end of input in " + modeName,
+                firstToken.range(this.nextToken, str));
+        }
+        lastToken = this.nextToken;
+        str += lastToken.text;
+        this.consume();
+    }
+    this.mode = outerMode;
+    this.expect(optional ? "]" : "}");
+    return firstToken.range(lastToken, str);
+};
+
+/**
+ * Parses a color description.
+ */
+Parser.prototype.parseColorGroup = function(optional) {
+    var res = this.parseStringGroup("color", optional);
+    if (!res) {
+        return null;
+    }
+    var match = (/^(#[a-z0-9]+|[a-z]+)$/i).exec(res.text);
+    if (!match) {
+        throw new ParseError("Invalid color: '" + res.text + "'", res);
+    }
+    return new ParseFuncOrArgument(
+        new ParseNode("color", match[0], this.mode),
+        false);
+};
+
+/**
+ * Parses a size specification, consisting of magnitude and unit.
+ */
+Parser.prototype.parseSizeGroup = function(optional) {
+    var res = this.parseStringGroup("size", optional);
+    if (!res) {
+        return null;
+    }
+    var match = (/(-?) *(\d+(?:\.\d*)?|\.\d+) *([a-z]{2})/).exec(res.text);
+    if (!match) {
+        throw new ParseError("Invalid size: '" + res.text + "'", res);
+    }
+    var data = {
+        number: +(match[1] + match[2]), // sign + magnitude, cast to number
+        unit: match[3],
+    };
+    if (data.unit !== "em" && data.unit !== "ex") {
+        throw new ParseError("Invalid unit: '" + data.unit + "'", res);
+    }
+    return new ParseFuncOrArgument(
+        new ParseNode("color", data, this.mode),
+        false);
+};
+
+/**
+ * If the argument is false or absent, this parses an ordinary group,
+ * which is either a single nucleus (like "x") or an expression
+ * in braces (like "{x+y}").
+ * If the argument is true, it parses either a bracket-delimited expression
+ * (like "[x+y]") or returns null to indicate the absence of a
+ * bracket-enclosed group.
+ *
+ * @param {boolean=} optional  Whether the group is optional or required
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseGroup = function(optional) {
+    var firstToken = this.nextToken;
+    // Try to parse an open brace
+    if (this.nextToken.text === (optional ? "[" : "{")) {
+        // If we get a brace, parse an expression
+        this.consume();
+        var expression = this.parseExpression(false, optional ? "]" : null);
+        var lastToken = this.nextToken;
+        // Make sure we get a close brace
+        this.expect(optional ? "]" : "}");
+        if (this.mode === "text") {
+            this.formLigatures(expression);
+        }
+        return new ParseFuncOrArgument(
+            new ParseNode("ordgroup", expression, this.mode,
+                          firstToken, lastToken),
+            false);
+    } else {
+        // Otherwise, just return a nucleus, or nothing for an optional group
+        return optional ? null : this.parseSymbol();
+    }
+};
+
+/**
+ * Form ligature-like combinations of characters for text mode.
+ * This includes inputs like "--", "---", "``" and "''".
+ * The result will simply replace multiple textord nodes with a single
+ * character in each value by a single textord node having multiple
+ * characters in its value.  The representation is still ASCII source.
+ *
+ * @param {Array.<ParseNode>} group  the nodes of this group,
+ *                                   list will be moified in place
+ */
+Parser.prototype.formLigatures = function(group) {
+    var i;
+    var n = group.length - 1;
+    for (i = 0; i < n; ++i) {
+        var a = group[i];
+        var v = a.value;
+        if (v === "-" && group[i + 1].value === "-") {
+            if (i + 1 < n && group[i + 2].value === "-") {
+                group.splice(i, 3, new ParseNode(
+                    "textord", "---", "text", a, group[i + 2]));
+                n -= 2;
+            } else {
+                group.splice(i, 2, new ParseNode(
+                    "textord", "--", "text", a, group[i + 1]));
+                n -= 1;
+            }
+        }
+        if ((v === "'" || v === "`") && group[i + 1].value === v) {
+            group.splice(i, 2, new ParseNode(
+                "textord", v + v, "text", a, group[i + 1]));
+            n -= 1;
+        }
+    }
+};
+
+/**
+ * Parse a single symbol out of the string. Here, we handle both the functions
+ * we have defined, as well as the single character symbols
+ *
+ * @return {?ParseFuncOrArgument}
+ */
+Parser.prototype.parseSymbol = function() {
+    var nucleus = this.nextToken;
+
+    if (functions[nucleus.text]) {
+        this.consume();
+        // If there exists a function with this name, we return the function and
+        // say that it is a function.
+        return new ParseFuncOrArgument(
+            nucleus.text,
+            true, nucleus);
+    } else if (symbols[this.mode][nucleus.text]) {
+        this.consume();
+        // Otherwise if this is a no-argument function, find the type it
+        // corresponds to in the symbols map
+        return new ParseFuncOrArgument(
+            new ParseNode(symbols[this.mode][nucleus.text].group,
+                          nucleus.text, this.mode, nucleus),
+            false, nucleus);
+    } else if (this.mode === "text" && cjkRegex.test(nucleus.text)) {
+        this.consume();
+        return new ParseFuncOrArgument(
+            new ParseNode("textord", nucleus.text, this.mode, nucleus),
+            false, nucleus);
+    } else {
+        return null;
+    }
+};
+
+Parser.prototype.ParseNode = ParseNode;
+
+module.exports = Parser;
+
+},{"./MacroExpander":4,"./ParseError":6,"./environments":16,"./functions":19,"./parseData":21,"./symbols":23,"./unicodeRegexes":24,"./utils":25}],8:[function(require,module,exports){
+/**
+ * This is a module for storing settings passed into KaTeX. It correctly handles
+ * default settings.
+ */
+
+/**
+ * Helper function for getting a default value if the value is undefined
+ */
+function get(option, defaultValue) {
+    return option === undefined ? defaultValue : option;
+}
+
+/**
+ * The main Settings object
+ *
+ * The current options stored are:
+ *  - displayMode: Whether the expression should be typeset by default in
+ *                 textstyle or displaystyle (default false)
+ */
+function Settings(options) {
+    // allow null options
+    options = options || {};
+    this.displayMode = get(options.displayMode, false);
+    this.throwOnError = get(options.throwOnError, true);
+    this.errorColor = get(options.errorColor, "#cc0000");
+    this.macros = options.macros || {};
+}
+
+module.exports = Settings;
+
+},{}],9:[function(require,module,exports){
+/**
+ * This file contains information and classes for the various kinds of styles
+ * used in TeX. It provides a generic `Style` class, which holds information
+ * about a specific style. It then provides instances of all the different kinds
+ * of styles possible, and provides functions to move between them and get
+ * information about them.
+ */
+
+/**
+ * The main style class. Contains a unique id for the style, a size (which is
+ * the same for cramped and uncramped version of a style), a cramped flag, and a
+ * size multiplier, which gives the size difference between a style and
+ * textstyle.
+ */
+function Style(id, size, multiplier, cramped) {
+    this.id = id;
+    this.size = size;
+    this.cramped = cramped;
+    this.sizeMultiplier = multiplier;
+}
+
+/**
+ * Get the style of a superscript given a base in the current style.
+ */
+Style.prototype.sup = function() {
+    return styles[sup[this.id]];
+};
+
+/**
+ * Get the style of a subscript given a base in the current style.
+ */
+Style.prototype.sub = function() {
+    return styles[sub[this.id]];
+};
+
+/**
+ * Get the style of a fraction numerator given the fraction in the current
+ * style.
+ */
+Style.prototype.fracNum = function() {
+    return styles[fracNum[this.id]];
+};
+
+/**
+ * Get the style of a fraction denominator given the fraction in the current
+ * style.
+ */
+Style.prototype.fracDen = function() {
+    return styles[fracDen[this.id]];
+};
+
+/**
+ * Get the cramped version of a style (in particular, cramping a cramped style
+ * doesn't change the style).
+ */
+Style.prototype.cramp = function() {
+    return styles[cramp[this.id]];
+};
+
+/**
+ * HTML class name, like "displaystyle cramped"
+ */
+Style.prototype.cls = function() {
+    return sizeNames[this.size] + (this.cramped ? " cramped" : " uncramped");
+};
+
+/**
+ * HTML Reset class name, like "reset-textstyle"
+ */
+Style.prototype.reset = function() {
+    return resetNames[this.size];
+};
+
+// IDs of the different styles
+var D = 0;
+var Dc = 1;
+var T = 2;
+var Tc = 3;
+var S = 4;
+var Sc = 5;
+var SS = 6;
+var SSc = 7;
+
+// String names for the different sizes
+var sizeNames = [
+    "displaystyle textstyle",
+    "textstyle",
+    "scriptstyle",
+    "scriptscriptstyle",
+];
+
+// Reset names for the different sizes
+var resetNames = [
+    "reset-textstyle",
+    "reset-textstyle",
+    "reset-scriptstyle",
+    "reset-scriptscriptstyle",
+];
+
+// Instances of the different styles
+var styles = [
+    new Style(D, 0, 1.0, false),
+    new Style(Dc, 0, 1.0, true),
+    new Style(T, 1, 1.0, false),
+    new Style(Tc, 1, 1.0, true),
+    new Style(S, 2, 0.7, false),
+    new Style(Sc, 2, 0.7, true),
+    new Style(SS, 3, 0.5, false),
+    new Style(SSc, 3, 0.5, true),
+];
+
+// Lookup tables for switching from one style to another
+var sup = [S, Sc, S, Sc, SS, SSc, SS, SSc];
+var sub = [Sc, Sc, Sc, Sc, SSc, SSc, SSc, SSc];
+var fracNum = [T, Tc, S, Sc, SS, SSc, SS, SSc];
+var fracDen = [Tc, Tc, Sc, Sc, SSc, SSc, SSc, SSc];
+var cramp = [Dc, Dc, Tc, Tc, Sc, Sc, SSc, SSc];
+
+// We only export some of the styles. Also, we don't export the `Style` class so
+// no more styles can be generated.
+module.exports = {
+    DISPLAY: styles[D],
+    TEXT: styles[T],
+    SCRIPT: styles[S],
+    SCRIPTSCRIPT: styles[SS],
+};
+
+},{}],10:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This module contains general functions that can be used for building
+ * different kinds of domTree nodes in a consistent manner.
+ */
+
+var domTree = require("./domTree");
+var fontMetrics = require("./fontMetrics");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var greekCapitals = [
+    "\\Gamma",
+    "\\Delta",
+    "\\Theta",
+    "\\Lambda",
+    "\\Xi",
+    "\\Pi",
+    "\\Sigma",
+    "\\Upsilon",
+    "\\Phi",
+    "\\Psi",
+    "\\Omega",
+];
+
+// The following have to be loaded from Main-Italic font, using class mainit
+var mainitLetters = [
+    "\u0131",   // dotless i, \imath
+    "\u0237",   // dotless j, \jmath
+    "\u00a3",   // \pounds
+];
+
+/**
+ * Makes a symbolNode after translation via the list of symbols in symbols.js.
+ * Correctly pulls out metrics for the character, and optionally takes a list of
+ * classes to be attached to the node.
+ */
+var makeSymbol = function(value, style, mode, color, classes) {
+    // Replace the value with its replaced value from symbol.js
+    if (symbols[mode][value] && symbols[mode][value].replace) {
+        value = symbols[mode][value].replace;
+    }
+
+    var metrics = fontMetrics.getCharacterMetrics(value, style);
+
+    var symbolNode;
+    if (metrics) {
+        symbolNode = new domTree.symbolNode(
+            value, metrics.height, metrics.depth, metrics.italic, metrics.skew,
+            classes);
+    } else {
+        // TODO(emily): Figure out a good way to only print this in development
+        typeof console !== "undefined" && console.warn(
+            "No character metrics for '" + value + "' in style '" +
+                style + "'");
+        symbolNode = new domTree.symbolNode(value, 0, 0, 0, 0, classes);
+    }
+
+    if (color) {
+        symbolNode.style.color = color;
+    }
+
+    return symbolNode;
+};
+
+/**
+ * Makes a symbol in Main-Regular or AMS-Regular.
+ * Used for rel, bin, open, close, inner, and punct.
+ */
+var mathsym = function(value, mode, color, classes) {
+    // Decide what font to render the symbol in by its entry in the symbols
+    // table.
+    // Have a special case for when the value = \ because the \ is used as a
+    // textord in unsupported command errors but cannot be parsed as a regular
+    // text ordinal and is therefore not present as a symbol in the symbols
+    // table for text
+    if (value === "\\" || symbols[mode][value].font === "main") {
+        return makeSymbol(value, "Main-Regular", mode, color, classes);
+    } else {
+        return makeSymbol(
+            value, "AMS-Regular", mode, color, classes.concat(["amsrm"]));
+    }
+};
+
+/**
+ * Makes a symbol in the default font for mathords and textords.
+ */
+var mathDefault = function(value, mode, color, classes, type) {
+    if (type === "mathord") {
+        return mathit(value, mode, color, classes);
+    } else if (type === "textord") {
+        return makeSymbol(
+            value, "Main-Regular", mode, color, classes.concat(["mathrm"]));
+    } else {
+        throw new Error("unexpected type: " + type + " in mathDefault");
+    }
+};
+
+/**
+ * Makes a symbol in the italic math font.
+ */
+var mathit = function(value, mode, color, classes) {
+    if (/[0-9]/.test(value.charAt(0)) ||
+            // glyphs for \imath and \jmath do not exist in Math-Italic so we
+            // need to use Main-Italic instead
+            utils.contains(mainitLetters, value) ||
+            utils.contains(greekCapitals, value)) {
+        return makeSymbol(
+            value, "Main-Italic", mode, color, classes.concat(["mainit"]));
+    } else {
+        return makeSymbol(
+            value, "Math-Italic", mode, color, classes.concat(["mathit"]));
+    }
+};
+
+/**
+ * Makes either a mathord or textord in the correct font and color.
+ */
+var makeOrd = function(group, options, type) {
+    var mode = group.mode;
+    var value = group.value;
+    if (symbols[mode][value] && symbols[mode][value].replace) {
+        value = symbols[mode][value].replace;
+    }
+
+    var classes = ["mord"];
+    var color = options.getColor();
+
+    var font = options.font;
+    if (font) {
+        if (font === "mathit" || utils.contains(mainitLetters, value)) {
+            return mathit(value, mode, color, classes);
+        } else {
+            var fontName = fontMap[font].fontName;
+            if (fontMetrics.getCharacterMetrics(value, fontName)) {
+                return makeSymbol(
+                    value, fontName, mode, color, classes.concat([font]));
+            } else {
+                return mathDefault(value, mode, color, classes, type);
+            }
+        }
+    } else {
+        return mathDefault(value, mode, color, classes, type);
+    }
+};
+
+/**
+ * Calculate the height, depth, and maxFontSize of an element based on its
+ * children.
+ */
+var sizeElementFromChildren = function(elem) {
+    var height = 0;
+    var depth = 0;
+    var maxFontSize = 0;
+
+    if (elem.children) {
+        for (var i = 0; i < elem.children.length; i++) {
+            if (elem.children[i].height > height) {
+                height = elem.children[i].height;
+            }
+            if (elem.children[i].depth > depth) {
+                depth = elem.children[i].depth;
+            }
+            if (elem.children[i].maxFontSize > maxFontSize) {
+                maxFontSize = elem.children[i].maxFontSize;
+            }
+        }
+    }
+
+    elem.height = height;
+    elem.depth = depth;
+    elem.maxFontSize = maxFontSize;
+};
+
+/**
+ * Makes a span with the given list of classes, list of children, and color.
+ */
+var makeSpan = function(classes, children, color) {
+    var span = new domTree.span(classes, children);
+
+    sizeElementFromChildren(span);
+
+    if (color) {
+        span.style.color = color;
+    }
+
+    return span;
+};
+
+/**
+ * Makes a document fragment with the given list of children.
+ */
+var makeFragment = function(children) {
+    var fragment = new domTree.documentFragment(children);
+
+    sizeElementFromChildren(fragment);
+
+    return fragment;
+};
+
+/**
+ * Makes an element placed in each of the vlist elements to ensure that each
+ * element has the same max font size. To do this, we create a zero-width space
+ * with the correct font size.
+ */
+var makeFontSizer = function(options, fontSize) {
+    var fontSizeInner = makeSpan([], [new domTree.symbolNode("\u200b")]);
+    fontSizeInner.style.fontSize =
+        (fontSize / options.style.sizeMultiplier) + "em";
+
+    var fontSizer = makeSpan(
+        ["fontsize-ensurer", "reset-" + options.size, "size5"],
+        [fontSizeInner]);
+
+    return fontSizer;
+};
+
+/**
+ * Makes a vertical list by stacking elements and kerns on top of each other.
+ * Allows for many different ways of specifying the positioning method.
+ *
+ * Arguments:
+ *  - children: A list of child or kern nodes to be stacked on top of each other
+ *              (i.e. the first element will be at the bottom, and the last at
+ *              the top). Element nodes are specified as
+ *                {type: "elem", elem: node}
+ *              while kern nodes are specified as
+ *                {type: "kern", size: size}
+ *  - positionType: The method by which the vlist should be positioned. Valid
+ *                  values are:
+ *                   - "individualShift": The children list only contains elem
+ *                                        nodes, and each node contains an extra
+ *                                        "shift" value of how much it should be
+ *                                        shifted (note that shifting is always
+ *                                        moving downwards). positionData is
+ *                                        ignored.
+ *                   - "top": The positionData specifies the topmost point of
+ *                            the vlist (note this is expected to be a height,
+ *                            so positive values move up)
+ *                   - "bottom": The positionData specifies the bottommost point
+ *                               of the vlist (note this is expected to be a
+ *                               depth, so positive values move down
+ *                   - "shift": The vlist will be positioned such that its
+ *                              baseline is positionData away from the baseline
+ *                              of the first child. Positive values move
+ *                              downwards.
+ *                   - "firstBaseline": The vlist will be positioned such that
+ *                                      its baseline is aligned with the
+ *                                      baseline of the first child.
+ *                                      positionData is ignored. (this is
+ *                                      equivalent to "shift" with
+ *                                      positionData=0)
+ *  - positionData: Data used in different ways depending on positionType
+ *  - options: An Options object
+ *
+ */
+var makeVList = function(children, positionType, positionData, options) {
+    var depth;
+    var currPos;
+    var i;
+    if (positionType === "individualShift") {
+        var oldChildren = children;
+        children = [oldChildren[0]];
+
+        // Add in kerns to the list of children to get each element to be
+        // shifted to the correct specified shift
+        depth = -oldChildren[0].shift - oldChildren[0].elem.depth;
+        currPos = depth;
+        for (i = 1; i < oldChildren.length; i++) {
+            var diff = -oldChildren[i].shift - currPos -
+                oldChildren[i].elem.depth;
+            var size = diff -
+                (oldChildren[i - 1].elem.height +
+                 oldChildren[i - 1].elem.depth);
+
+            currPos = currPos + diff;
+
+            children.push({type: "kern", size: size});
+            children.push(oldChildren[i]);
+        }
+    } else if (positionType === "top") {
+        // We always start at the bottom, so calculate the bottom by adding up
+        // all the sizes
+        var bottom = positionData;
+        for (i = 0; i < children.length; i++) {
+            if (children[i].type === "kern") {
+                bottom -= children[i].size;
+            } else {
+                bottom -= children[i].elem.height + children[i].elem.depth;
+            }
+        }
+        depth = bottom;
+    } else if (positionType === "bottom") {
+        depth = -positionData;
+    } else if (positionType === "shift") {
+        depth = -children[0].elem.depth - positionData;
+    } else if (positionType === "firstBaseline") {
+        depth = -children[0].elem.depth;
+    } else {
+        depth = 0;
+    }
+
+    // Make the fontSizer
+    var maxFontSize = 0;
+    for (i = 0; i < children.length; i++) {
+        if (children[i].type === "elem") {
+            maxFontSize = Math.max(maxFontSize, children[i].elem.maxFontSize);
+        }
+    }
+    var fontSizer = makeFontSizer(options, maxFontSize);
+
+    // Create a new list of actual children at the correct offsets
+    var realChildren = [];
+    currPos = depth;
+    for (i = 0; i < children.length; i++) {
+        if (children[i].type === "kern") {
+            currPos += children[i].size;
+        } else {
+            var child = children[i].elem;
+
+            var shift = -child.depth - currPos;
+            currPos += child.height + child.depth;
+
+            var childWrap = makeSpan([], [fontSizer, child]);
+            childWrap.height -= shift;
+            childWrap.depth += shift;
+            childWrap.style.top = shift + "em";
+
+            realChildren.push(childWrap);
+        }
+    }
+
+    // Add in an element at the end with no offset to fix the calculation of
+    // baselines in some browsers (namely IE, sometimes safari)
+    var baselineFix = makeSpan(
+        ["baseline-fix"], [fontSizer, new domTree.symbolNode("\u200b")]);
+    realChildren.push(baselineFix);
+
+    var vlist = makeSpan(["vlist"], realChildren);
+    // Fix the final height and depth, in case there were kerns at the ends
+    // since the makeSpan calculation won't take that in to account.
+    vlist.height = Math.max(currPos, vlist.height);
+    vlist.depth = Math.max(-depth, vlist.depth);
+    return vlist;
+};
+
+// A table of size -> font size for the different sizing functions
+var sizingMultiplier = {
+    size1: 0.5,
+    size2: 0.7,
+    size3: 0.8,
+    size4: 0.9,
+    size5: 1.0,
+    size6: 1.2,
+    size7: 1.44,
+    size8: 1.73,
+    size9: 2.07,
+    size10: 2.49,
+};
+
+// A map of spacing functions to their attributes, like size and corresponding
+// CSS class
+var spacingFunctions = {
+    "\\qquad": {
+        size: "2em",
+        className: "qquad",
+    },
+    "\\quad": {
+        size: "1em",
+        className: "quad",
+    },
+    "\\enspace": {
+        size: "0.5em",
+        className: "enspace",
+    },
+    "\\;": {
+        size: "0.277778em",
+        className: "thickspace",
+    },
+    "\\:": {
+        size: "0.22222em",
+        className: "mediumspace",
+    },
+    "\\,": {
+        size: "0.16667em",
+        className: "thinspace",
+    },
+    "\\!": {
+        size: "-0.16667em",
+        className: "negativethinspace",
+    },
+};
+
+/**
+ * Maps TeX font commands to objects containing:
+ * - variant: string used for "mathvariant" attribute in buildMathML.js
+ * - fontName: the "style" parameter to fontMetrics.getCharacterMetrics
+ */
+// A map between tex font commands an MathML mathvariant attribute values
+var fontMap = {
+    // styles
+    "mathbf": {
+        variant: "bold",
+        fontName: "Main-Bold",
+    },
+    "mathrm": {
+        variant: "normal",
+        fontName: "Main-Regular",
+    },
+
+    // "mathit" is missing because it requires the use of two fonts: Main-Italic
+    // and Math-Italic.  This is handled by a special case in makeOrd which ends
+    // up calling mathit.
+
+    // families
+    "mathbb": {
+        variant: "double-struck",
+        fontName: "AMS-Regular",
+    },
+    "mathcal": {
+        variant: "script",
+        fontName: "Caligraphic-Regular",
+    },
+    "mathfrak": {
+        variant: "fraktur",
+        fontName: "Fraktur-Regular",
+    },
+    "mathscr": {
+        variant: "script",
+        fontName: "Script-Regular",
+    },
+    "mathsf": {
+        variant: "sans-serif",
+        fontName: "SansSerif-Regular",
+    },
+    "mathtt": {
+        variant: "monospace",
+        fontName: "Typewriter-Regular",
+    },
+};
+
+module.exports = {
+    fontMap: fontMap,
+    makeSymbol: makeSymbol,
+    mathsym: mathsym,
+    makeSpan: makeSpan,
+    makeFragment: makeFragment,
+    makeVList: makeVList,
+    makeOrd: makeOrd,
+    sizingMultiplier: sizingMultiplier,
+    spacingFunctions: spacingFunctions,
+};
+
+},{"./domTree":15,"./fontMetrics":17,"./symbols":23,"./utils":25}],11:[function(require,module,exports){
+/* eslint no-console:0 */
+/**
+ * This file does the main work of building a domTree structure from a parse
+ * tree. The entry point is the `buildHTML` function, which takes a parse tree.
+ * Then, the buildExpression, buildGroup, and various groupTypes functions are
+ * called, to produce a final HTML tree.
+ */
+
+var ParseError = require("./ParseError");
+var Style = require("./Style");
+
+var buildCommon = require("./buildCommon");
+var delimiter = require("./delimiter");
+var domTree = require("./domTree");
+var fontMetrics = require("./fontMetrics");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+
+/**
+ * Take a list of nodes, build them in order, and return a list of the built
+ * nodes. This function handles the `prev` node correctly, and passes the
+ * previous element from the list as the prev of the next element.
+ */
+var buildExpression = function(expression, options, prev) {
+    var groups = [];
+    for (var i = 0; i < expression.length; i++) {
+        var group = expression[i];
+        groups.push(buildGroup(group, options, prev));
+        prev = group;
+    }
+    return groups;
+};
+
+// List of types used by getTypeOfGroup,
+// see https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types
+var groupToType = {
+    mathord: "mord",
+    textord: "mord",
+    bin: "mbin",
+    rel: "mrel",
+    text: "mord",
+    open: "mopen",
+    close: "mclose",
+    inner: "minner",
+    genfrac: "mord",
+    array: "mord",
+    spacing: "mord",
+    punct: "mpunct",
+    ordgroup: "mord",
+    op: "mop",
+    katex: "mord",
+    overline: "mord",
+    underline: "mord",
+    rule: "mord",
+    leftright: "minner",
+    sqrt: "mord",
+    accent: "mord",
+};
+
+/**
+ * Gets the final math type of an expression, given its group type. This type is
+ * used to determine spacing between elements, and affects bin elements by
+ * causing them to change depending on what types are around them. This type
+ * must be attached to the outermost node of an element as a CSS class so that
+ * spacing with its surrounding elements works correctly.
+ *
+ * Some elements can be mapped one-to-one from group type to math type, and
+ * those are listed in the `groupToType` table.
+ *
+ * Others (usually elements that wrap around other elements) often have
+ * recursive definitions, and thus call `getTypeOfGroup` on their inner
+ * elements.
+ */
+var getTypeOfGroup = function(group) {
+    if (group == null) {
+        // Like when typesetting $^3$
+        return groupToType.mathord;
+    } else if (group.type === "supsub") {
+        return getTypeOfGroup(group.value.base);
+    } else if (group.type === "llap" || group.type === "rlap") {
+        return getTypeOfGroup(group.value);
+    } else if (group.type === "color") {
+        return getTypeOfGroup(group.value.value);
+    } else if (group.type === "sizing") {
+        return getTypeOfGroup(group.value.value);
+    } else if (group.type === "styling") {
+        return getTypeOfGroup(group.value.value);
+    } else if (group.type === "delimsizing") {
+        return groupToType[group.value.delimType];
+    } else {
+        return groupToType[group.type];
+    }
+};
+
+/**
+ * Sometimes, groups perform special rules when they have superscripts or
+ * subscripts attached to them. This function lets the `supsub` group know that
+ * its inner element should handle the superscripts and subscripts instead of
+ * handling them itself.
+ */
+var shouldHandleSupSub = function(group, options) {
+    if (!group) {
+        return false;
+    } else if (group.type === "op") {
+        // Operators handle supsubs differently when they have limits
+        // (e.g. `\displaystyle\sum_2^3`)
+        return group.value.limits &&
+            (options.style.size === Style.DISPLAY.size ||
+            group.value.alwaysHandleSupSub);
+    } else if (group.type === "accent") {
+        return isCharacterBox(group.value.base);
+    } else {
+        return null;
+    }
+};
+
+/**
+ * Sometimes we want to pull out the innermost element of a group. In most
+ * cases, this will just be the group itself, but when ordgroups and colors have
+ * a single element, we want to pull that out.
+ */
+var getBaseElem = function(group) {
+    if (!group) {
+        return false;
+    } else if (group.type === "ordgroup") {
+        if (group.value.length === 1) {
+            return getBaseElem(group.value[0]);
+        } else {
+            return group;
+        }
+    } else if (group.type === "color") {
+        if (group.value.value.length === 1) {
+            return getBaseElem(group.value.value[0]);
+        } else {
+            return group;
+        }
+    } else if (group.type === "font") {
+        return getBaseElem(group.value.body);
+    } else {
+        return group;
+    }
+};
+
+/**
+ * TeXbook algorithms often reference "character boxes", which are simply groups
+ * with a single character in them. To decide if something is a character box,
+ * we find its innermost group, and see if it is a single character.
+ */
+var isCharacterBox = function(group) {
+    var baseElem = getBaseElem(group);
+
+    // These are all they types of groups which hold single characters
+    return baseElem.type === "mathord" ||
+        baseElem.type === "textord" ||
+        baseElem.type === "bin" ||
+        baseElem.type === "rel" ||
+        baseElem.type === "inner" ||
+        baseElem.type === "open" ||
+        baseElem.type === "close" ||
+        baseElem.type === "punct";
+};
+
+var makeNullDelimiter = function(options) {
+    return makeSpan([
+        "sizing", "reset-" + options.size, "size5",
+        options.style.reset(), Style.TEXT.cls(),
+        "nulldelimiter",
+    ]);
+};
+
+/**
+ * This is a map of group types to the function used to handle that type.
+ * Simpler types come at the beginning, while complicated types come afterwards.
+ */
+var groupTypes = {};
+
+groupTypes.mathord = function(group, options, prev) {
+    return buildCommon.makeOrd(group, options, "mathord");
+};
+
+groupTypes.textord = function(group, options, prev) {
+    return buildCommon.makeOrd(group, options, "textord");
+};
+
+groupTypes.bin = function(group, options, prev) {
+    var className = "mbin";
+    // Pull out the most recent element. Do some special handling to find
+    // things at the end of a \color group. Note that we don't use the same
+    // logic for ordgroups (which count as ords).
+    var prevAtom = prev;
+    while (prevAtom && prevAtom.type === "color") {
+        var atoms = prevAtom.value.value;
+        prevAtom = atoms[atoms.length - 1];
+    }
+    // See TeXbook pg. 442-446, Rules 5 and 6, and the text before Rule 19.
+    // Here, we determine whether the bin should turn into an ord. We
+    // currently only apply Rule 5.
+    if (!prev || utils.contains(["mbin", "mopen", "mrel", "mop", "mpunct"],
+            getTypeOfGroup(prevAtom))) {
+        group.type = "textord";
+        className = "mord";
+    }
+
+    return buildCommon.mathsym(
+        group.value, group.mode, options.getColor(), [className]);
+};
+
+groupTypes.rel = function(group, options, prev) {
+    return buildCommon.mathsym(
+        group.value, group.mode, options.getColor(), ["mrel"]);
+};
+
+groupTypes.open = function(group, options, prev) {
+    return buildCommon.mathsym(
+        group.value, group.mode, options.getColor(), ["mopen"]);
+};
+
+groupTypes.close = function(group, options, prev) {
+    return buildCommon.mathsym(
+        group.value, group.mode, options.getColor(), ["mclose"]);
+};
+
+groupTypes.inner = function(group, options, prev) {
+    return buildCommon.mathsym(
+        group.value, group.mode, options.getColor(), ["minner"]);
+};
+
+groupTypes.punct = function(group, options, prev) {
+    return buildCommon.mathsym(
+        group.value, group.mode, options.getColor(), ["mpunct"]);
+};
+
+groupTypes.ordgroup = function(group, options, prev) {
+    return makeSpan(
+        ["mord", options.style.cls()],
+        buildExpression(group.value, options.reset())
+    );
+};
+
+groupTypes.text = function(group, options, prev) {
+    return makeSpan(["text", "mord", options.style.cls()],
+        buildExpression(group.value.body, options.reset()));
+};
+
+groupTypes.color = function(group, options, prev) {
+    var elements = buildExpression(
+        group.value.value,
+        options.withColor(group.value.color),
+        prev
+    );
+
+    // \color isn't supposed to affect the type of the elements it contains.
+    // To accomplish this, we wrap the results in a fragment, so the inner
+    // elements will be able to directly interact with their neighbors. For
+    // example, `\color{red}{2 +} 3` has the same spacing as `2 + 3`
+    return new buildCommon.makeFragment(elements);
+};
+
+groupTypes.supsub = function(group, options, prev) {
+    // Superscript and subscripts are handled in the TeXbook on page
+    // 445-446, rules 18(a-f).
+
+    // Here is where we defer to the inner group if it should handle
+    // superscripts and subscripts itself.
+    if (shouldHandleSupSub(group.value.base, options)) {
+        return groupTypes[group.value.base.type](group, options, prev);
+    }
+
+    var base = buildGroup(group.value.base, options.reset());
+    var supmid;
+    var submid;
+    var sup;
+    var sub;
+
+    if (group.value.sup) {
+        sup = buildGroup(group.value.sup,
+                options.withStyle(options.style.sup()));
+        supmid = makeSpan(
+                [options.style.reset(), options.style.sup().cls()], [sup]);
+    }
+
+    if (group.value.sub) {
+        sub = buildGroup(group.value.sub,
+                options.withStyle(options.style.sub()));
+        submid = makeSpan(
+                [options.style.reset(), options.style.sub().cls()], [sub]);
+    }
+
+    // Rule 18a
+    var supShift;
+    var subShift;
+    if (isCharacterBox(group.value.base)) {
+        supShift = 0;
+        subShift = 0;
+    } else {
+        supShift = base.height - fontMetrics.metrics.supDrop;
+        subShift = base.depth + fontMetrics.metrics.subDrop;
+    }
+
+    // Rule 18c
+    var minSupShift;
+    if (options.style === Style.DISPLAY) {
+        minSupShift = fontMetrics.metrics.sup1;
+    } else if (options.style.cramped) {
+        minSupShift = fontMetrics.metrics.sup3;
+    } else {
+        minSupShift = fontMetrics.metrics.sup2;
+    }
+
+    // scriptspace is a font-size-independent size, so scale it
+    // appropriately
+    var multiplier = Style.TEXT.sizeMultiplier *
+            options.style.sizeMultiplier;
+    var scriptspace =
+        (0.5 / fontMetrics.metrics.ptPerEm) / multiplier + "em";
+
+    var supsub;
+    if (!group.value.sup) {
+        // Rule 18b
+        subShift = Math.max(
+            subShift, fontMetrics.metrics.sub1,
+            sub.height - 0.8 * fontMetrics.metrics.xHeight);
+
+        supsub = buildCommon.makeVList([
+            {type: "elem", elem: submid},
+        ], "shift", subShift, options);
+
+        supsub.children[0].style.marginRight = scriptspace;
+
+        // Subscripts shouldn't be shifted by the base's italic correction.
+        // Account for that by shifting the subscript back the appropriate
+        // amount. Note we only do this when the base is a single symbol.
+        if (base instanceof domTree.symbolNode) {
+            supsub.children[0].style.marginLeft = -base.italic + "em";
+        }
+    } else if (!group.value.sub) {
+        // Rule 18c, d
+        supShift = Math.max(supShift, minSupShift,
+            sup.depth + 0.25 * fontMetrics.metrics.xHeight);
+
+        supsub = buildCommon.makeVList([
+            {type: "elem", elem: supmid},
+        ], "shift", -supShift, options);
+
+        supsub.children[0].style.marginRight = scriptspace;
+    } else {
+        supShift = Math.max(
+            supShift, minSupShift,
+            sup.depth + 0.25 * fontMetrics.metrics.xHeight);
+        subShift = Math.max(subShift, fontMetrics.metrics.sub2);
+
+        var ruleWidth = fontMetrics.metrics.defaultRuleThickness;
+
+        // Rule 18e
+        if ((supShift - sup.depth) - (sub.height - subShift) <
+                4 * ruleWidth) {
+            subShift = 4 * ruleWidth - (supShift - sup.depth) + sub.height;
+            var psi = 0.8 * fontMetrics.metrics.xHeight -
+                (supShift - sup.depth);
+            if (psi > 0) {
+                supShift += psi;
+                subShift -= psi;
+            }
+        }
+
+        supsub = buildCommon.makeVList([
+            {type: "elem", elem: submid, shift: subShift},
+            {type: "elem", elem: supmid, shift: -supShift},
+        ], "individualShift", null, options);
+
+        // See comment above about subscripts not being shifted
+        if (base instanceof domTree.symbolNode) {
+            supsub.children[0].style.marginLeft = -base.italic + "em";
+        }
+
+        supsub.children[0].style.marginRight = scriptspace;
+        supsub.children[1].style.marginRight = scriptspace;
+    }
+
+    // We ensure to wrap the supsub vlist in a span.msupsub to reset text-align
+    return makeSpan([getTypeOfGroup(group.value.base)],
+        [base, makeSpan(["msupsub"], [supsub])]);
+};
+
+groupTypes.genfrac = function(group, options, prev) {
+    // Fractions are handled in the TeXbook on pages 444-445, rules 15(a-e).
+    // Figure out what style this fraction should be in based on the
+    // function used
+    var fstyle = options.style;
+    if (group.value.size === "display") {
+        fstyle = Style.DISPLAY;
+    } else if (group.value.size === "text") {
+        fstyle = Style.TEXT;
+    }
+
+    var nstyle = fstyle.fracNum();
+    var dstyle = fstyle.fracDen();
+
+    var numer = buildGroup(group.value.numer, options.withStyle(nstyle));
+    var numerreset = makeSpan([fstyle.reset(), nstyle.cls()], [numer]);
+
+    var denom = buildGroup(group.value.denom, options.withStyle(dstyle));
+    var denomreset = makeSpan([fstyle.reset(), dstyle.cls()], [denom]);
+
+    var ruleWidth;
+    if (group.value.hasBarLine) {
+        ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+            options.style.sizeMultiplier;
+    } else {
+        ruleWidth = 0;
+    }
+
+    // Rule 15b
+    var numShift;
+    var clearance;
+    var denomShift;
+    if (fstyle.size === Style.DISPLAY.size) {
+        numShift = fontMetrics.metrics.num1;
+        if (ruleWidth > 0) {
+            clearance = 3 * ruleWidth;
+        } else {
+            clearance = 7 * fontMetrics.metrics.defaultRuleThickness;
+        }
+        denomShift = fontMetrics.metrics.denom1;
+    } else {
+        if (ruleWidth > 0) {
+            numShift = fontMetrics.metrics.num2;
+            clearance = ruleWidth;
+        } else {
+            numShift = fontMetrics.metrics.num3;
+            clearance = 3 * fontMetrics.metrics.defaultRuleThickness;
+        }
+        denomShift = fontMetrics.metrics.denom2;
+    }
+
+    var frac;
+    if (ruleWidth === 0) {
+        // Rule 15c
+        var candiateClearance =
+            (numShift - numer.depth) - (denom.height - denomShift);
+        if (candiateClearance < clearance) {
+            numShift += 0.5 * (clearance - candiateClearance);
+            denomShift += 0.5 * (clearance - candiateClearance);
+        }
+
+        frac = buildCommon.makeVList([
+            {type: "elem", elem: denomreset, shift: denomShift},
+            {type: "elem", elem: numerreset, shift: -numShift},
+        ], "individualShift", null, options);
+    } else {
+        // Rule 15d
+        var axisHeight = fontMetrics.metrics.axisHeight;
+
+        if ((numShift - numer.depth) - (axisHeight + 0.5 * ruleWidth) <
+                clearance) {
+            numShift +=
+                clearance - ((numShift - numer.depth) -
+                             (axisHeight + 0.5 * ruleWidth));
+        }
+
+        if ((axisHeight - 0.5 * ruleWidth) - (denom.height - denomShift) <
+                clearance) {
+            denomShift +=
+                clearance - ((axisHeight - 0.5 * ruleWidth) -
+                             (denom.height - denomShift));
+        }
+
+        var mid = makeSpan(
+            [options.style.reset(), Style.TEXT.cls(), "frac-line"]);
+        // Manually set the height of the line because its height is
+        // created in CSS
+        mid.height = ruleWidth;
+
+        var midShift = -(axisHeight - 0.5 * ruleWidth);
+
+        frac = buildCommon.makeVList([
+            {type: "elem", elem: denomreset, shift: denomShift},
+            {type: "elem", elem: mid,        shift: midShift},
+            {type: "elem", elem: numerreset, shift: -numShift},
+        ], "individualShift", null, options);
+    }
+
+    // Since we manually change the style sometimes (with \dfrac or \tfrac),
+    // account for the possible size change here.
+    frac.height *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
+    frac.depth *= fstyle.sizeMultiplier / options.style.sizeMultiplier;
+
+    // Rule 15e
+    var delimSize;
+    if (fstyle.size === Style.DISPLAY.size) {
+        delimSize = fontMetrics.metrics.delim1;
+    } else {
+        delimSize = fontMetrics.metrics.getDelim2(fstyle);
+    }
+
+    var leftDelim;
+    var rightDelim;
+    if (group.value.leftDelim == null) {
+        leftDelim = makeNullDelimiter(options);
+    } else {
+        leftDelim = delimiter.customSizedDelim(
+            group.value.leftDelim, delimSize, true,
+            options.withStyle(fstyle), group.mode);
+    }
+    if (group.value.rightDelim == null) {
+        rightDelim = makeNullDelimiter(options);
+    } else {
+        rightDelim = delimiter.customSizedDelim(
+            group.value.rightDelim, delimSize, true,
+            options.withStyle(fstyle), group.mode);
+    }
+
+    return makeSpan(
+        ["mord", options.style.reset(), fstyle.cls()],
+        [leftDelim, makeSpan(["mfrac"], [frac]), rightDelim],
+        options.getColor());
+};
+
+groupTypes.array = function(group, options, prev) {
+    var r;
+    var c;
+    var nr = group.value.body.length;
+    var nc = 0;
+    var body = new Array(nr);
+
+    // Horizontal spacing
+    var pt = 1 / fontMetrics.metrics.ptPerEm;
+    var arraycolsep = 5 * pt; // \arraycolsep in article.cls
+
+    // Vertical spacing
+    var baselineskip = 12 * pt; // see size10.clo
+    // Default \arraystretch from lttab.dtx
+    // TODO(gagern): may get redefined once we have user-defined macros
+    var arraystretch = utils.deflt(group.value.arraystretch, 1);
+    var arrayskip = arraystretch * baselineskip;
+    var arstrutHeight = 0.7 * arrayskip; // \strutbox in ltfsstrc.dtx and
+    var arstrutDepth = 0.3 * arrayskip;  // \@arstrutbox in lttab.dtx
+
+    var totalHeight = 0;
+    for (r = 0; r < group.value.body.length; ++r) {
+        var inrow = group.value.body[r];
+        var height = arstrutHeight; // \@array adds an \@arstrut
+        var depth = arstrutDepth;   // to each tow (via the template)
+
+        if (nc < inrow.length) {
+            nc = inrow.length;
+        }
+
+        var outrow = new Array(inrow.length);
+        for (c = 0; c < inrow.length; ++c) {
+            var elt = buildGroup(inrow[c], options);
+            if (depth < elt.depth) {
+                depth = elt.depth;
+            }
+            if (height < elt.height) {
+                height = elt.height;
+            }
+            outrow[c] = elt;
+        }
+
+        var gap = 0;
+        if (group.value.rowGaps[r]) {
+            gap = group.value.rowGaps[r].value;
+            switch (gap.unit) {
+                case "em":
+                    gap = gap.number;
+                    break;
+                case "ex":
+                    gap = gap.number * fontMetrics.metrics.emPerEx;
+                    break;
+                default:
+                    console.error("Can't handle unit " + gap.unit);
+                    gap = 0;
+            }
+            if (gap > 0) { // \@argarraycr
+                gap += arstrutDepth;
+                if (depth < gap) {
+                    depth = gap; // \@xargarraycr
+                }
+                gap = 0;
+            }
+        }
+
+        outrow.height = height;
+        outrow.depth = depth;
+        totalHeight += height;
+        outrow.pos = totalHeight;
+        totalHeight += depth + gap; // \@yargarraycr
+        body[r] = outrow;
+    }
+
+    var offset = totalHeight / 2 + fontMetrics.metrics.axisHeight;
+    var colDescriptions = group.value.cols || [];
+    var cols = [];
+    var colSep;
+    var colDescrNum;
+    for (c = 0, colDescrNum = 0;
+         // Continue while either there are more columns or more column
+         // descriptions, so trailing separators don't get lost.
+         c < nc || colDescrNum < colDescriptions.length;
+         ++c, ++colDescrNum) {
+
+        var colDescr = colDescriptions[colDescrNum] || {};
+
+        var firstSeparator = true;
+        while (colDescr.type === "separator") {
+            // If there is more than one separator in a row, add a space
+            // between them.
+            if (!firstSeparator) {
+                colSep = makeSpan(["arraycolsep"], []);
+                colSep.style.width =
+                    fontMetrics.metrics.doubleRuleSep + "em";
+                cols.push(colSep);
+            }
+
+            if (colDescr.separator === "|") {
+                var separator = makeSpan(
+                    ["vertical-separator"],
+                    []);
+                separator.style.height = totalHeight + "em";
+                separator.style.verticalAlign =
+                    -(totalHeight - offset) + "em";
+
+                cols.push(separator);
+            } else {
+                throw new ParseError(
+                    "Invalid separator type: " + colDescr.separator);
+            }
+
+            colDescrNum++;
+            colDescr = colDescriptions[colDescrNum] || {};
+            firstSeparator = false;
+        }
+
+        if (c >= nc) {
+            continue;
+        }
+
+        var sepwidth;
+        if (c > 0 || group.value.hskipBeforeAndAfter) {
+            sepwidth = utils.deflt(colDescr.pregap, arraycolsep);
+            if (sepwidth !== 0) {
+                colSep = makeSpan(["arraycolsep"], []);
+                colSep.style.width = sepwidth + "em";
+                cols.push(colSep);
+            }
+        }
+
+        var col = [];
+        for (r = 0; r < nr; ++r) {
+            var row = body[r];
+            var elem = row[c];
+            if (!elem) {
+                continue;
+            }
+            var shift = row.pos - offset;
+            elem.depth = row.depth;
+            elem.height = row.height;
+            col.push({type: "elem", elem: elem, shift: shift});
+        }
+
+        col = buildCommon.makeVList(col, "individualShift", null, options);
+        col = makeSpan(
+            ["col-align-" + (colDescr.align || "c")],
+            [col]);
+        cols.push(col);
+
+        if (c < nc - 1 || group.value.hskipBeforeAndAfter) {
+            sepwidth = utils.deflt(colDescr.postgap, arraycolsep);
+            if (sepwidth !== 0) {
+                colSep = makeSpan(["arraycolsep"], []);
+                colSep.style.width = sepwidth + "em";
+                cols.push(colSep);
+            }
+        }
+    }
+    body = makeSpan(["mtable"], cols);
+    return makeSpan(["mord"], [body], options.getColor());
+};
+
+groupTypes.spacing = function(group, options, prev) {
+    if (group.value === "\\ " || group.value === "\\space" ||
+        group.value === " " || group.value === "~") {
+        // Spaces are generated by adding an actual space. Each of these
+        // things has an entry in the symbols table, so these will be turned
+        // into appropriate outputs.
+        return makeSpan(
+            ["mord", "mspace"],
+            [buildCommon.mathsym(group.value, group.mode)]
+        );
+    } else {
+        // Other kinds of spaces are of arbitrary width. We use CSS to
+        // generate these.
+        return makeSpan(
+            ["mord", "mspace",
+             buildCommon.spacingFunctions[group.value].className]);
+    }
+};
+
+groupTypes.llap = function(group, options, prev) {
+    var inner = makeSpan(
+        ["inner"], [buildGroup(group.value.body, options.reset())]);
+    var fix = makeSpan(["fix"], []);
+    return makeSpan(
+        ["llap", options.style.cls()], [inner, fix]);
+};
+
+groupTypes.rlap = function(group, options, prev) {
+    var inner = makeSpan(
+        ["inner"], [buildGroup(group.value.body, options.reset())]);
+    var fix = makeSpan(["fix"], []);
+    return makeSpan(
+        ["rlap", options.style.cls()], [inner, fix]);
+};
+
+groupTypes.op = function(group, options, prev) {
+    // Operators are handled in the TeXbook pg. 443-444, rule 13(a).
+    var supGroup;
+    var subGroup;
+    var hasLimits = false;
+    if (group.type === "supsub" ) {
+        // If we have limits, supsub will pass us its group to handle. Pull
+        // out the superscript and subscript and set the group to the op in
+        // its base.
+        supGroup = group.value.sup;
+        subGroup = group.value.sub;
+        group = group.value.base;
+        hasLimits = true;
+    }
+
+    // Most operators have a large successor symbol, but these don't.
+    var noSuccessor = [
+        "\\smallint",
+    ];
+
+    var large = false;
+    if (options.style.size === Style.DISPLAY.size &&
+        group.value.symbol &&
+        !utils.contains(noSuccessor, group.value.body)) {
+
+        // Most symbol operators get larger in displaystyle (rule 13)
+        large = true;
+    }
+
+    var base;
+    var baseShift = 0;
+    var slant = 0;
+    if (group.value.symbol) {
+        // If this is a symbol, create the symbol.
+        var style = large ? "Size2-Regular" : "Size1-Regular";
+        base = buildCommon.makeSymbol(
+            group.value.body, style, "math", options.getColor(),
+            ["op-symbol", large ? "large-op" : "small-op", "mop"]);
+
+        // Shift the symbol so its center lies on the axis (rule 13). It
+        // appears that our fonts have the centers of the symbols already
+        // almost on the axis, so these numbers are very small. Note we
+        // don't actually apply this here, but instead it is used either in
+        // the vlist creation or separately when there are no limits.
+        baseShift = (base.height - base.depth) / 2 -
+            fontMetrics.metrics.axisHeight *
+            options.style.sizeMultiplier;
+
+        // The slant of the symbol is just its italic correction.
+        slant = base.italic;
+    } else {
+        // Otherwise, this is a text operator. Build the text from the
+        // operator's name.
+        // TODO(emily): Add a space in the middle of some of these
+        // operators, like \limsup
+        var output = [];
+        for (var i = 1; i < group.value.body.length; i++) {
+            output.push(buildCommon.mathsym(group.value.body[i], group.mode));
+        }
+        base = makeSpan(["mop"], output, options.getColor());
+    }
+
+    if (hasLimits) {
+        // IE 8 clips \int if it is in a display: inline-block. We wrap it
+        // in a new span so it is an inline, and works.
+        base = makeSpan([], [base]);
+
+        var supmid;
+        var supKern;
+        var submid;
+        var subKern;
+        // We manually have to handle the superscripts and subscripts. This,
+        // aside from the kern calculations, is copied from supsub.
+        if (supGroup) {
+            var sup = buildGroup(
+                supGroup, options.withStyle(options.style.sup()));
+            supmid = makeSpan(
+                [options.style.reset(), options.style.sup().cls()], [sup]);
+
+            supKern = Math.max(
+                fontMetrics.metrics.bigOpSpacing1,
+                fontMetrics.metrics.bigOpSpacing3 - sup.depth);
+        }
+
+        if (subGroup) {
+            var sub = buildGroup(
+                subGroup, options.withStyle(options.style.sub()));
+            submid = makeSpan(
+                [options.style.reset(), options.style.sub().cls()],
+                [sub]);
+
+            subKern = Math.max(
+                fontMetrics.metrics.bigOpSpacing2,
+                fontMetrics.metrics.bigOpSpacing4 - sub.height);
+        }
+
+        // Build the final group as a vlist of the possible subscript, base,
+        // and possible superscript.
+        var finalGroup;
+        var top;
+        var bottom;
+        if (!supGroup) {
+            top = base.height - baseShift;
+
+            finalGroup = buildCommon.makeVList([
+                {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+                {type: "elem", elem: submid},
+                {type: "kern", size: subKern},
+                {type: "elem", elem: base},
+            ], "top", top, options);
+
+            // Here, we shift the limits by the slant of the symbol. Note
+            // that we are supposed to shift the limits by 1/2 of the slant,
+            // but since we are centering the limits adding a full slant of
+            // margin will shift by 1/2 that.
+            finalGroup.children[0].style.marginLeft = -slant + "em";
+        } else if (!subGroup) {
+            bottom = base.depth + baseShift;
+
+            finalGroup = buildCommon.makeVList([
+                {type: "elem", elem: base},
+                {type: "kern", size: supKern},
+                {type: "elem", elem: supmid},
+                {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+            ], "bottom", bottom, options);
+
+            // See comment above about slants
+            finalGroup.children[1].style.marginLeft = slant + "em";
+        } else if (!supGroup && !subGroup) {
+            // This case probably shouldn't occur (this would mean the
+            // supsub was sending us a group with no superscript or
+            // subscript) but be safe.
+            return base;
+        } else {
+            bottom = fontMetrics.metrics.bigOpSpacing5 +
+                submid.height + submid.depth +
+                subKern +
+                base.depth + baseShift;
+
+            finalGroup = buildCommon.makeVList([
+                {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+                {type: "elem", elem: submid},
+                {type: "kern", size: subKern},
+                {type: "elem", elem: base},
+                {type: "kern", size: supKern},
+                {type: "elem", elem: supmid},
+                {type: "kern", size: fontMetrics.metrics.bigOpSpacing5},
+            ], "bottom", bottom, options);
+
+            // See comment above about slants
+            finalGroup.children[0].style.marginLeft = -slant + "em";
+            finalGroup.children[2].style.marginLeft = slant + "em";
+        }
+
+        return makeSpan(["mop", "op-limits"], [finalGroup]);
+    } else {
+        if (group.value.symbol) {
+            base.style.top = baseShift + "em";
+        }
+
+        return base;
+    }
+};
+
+groupTypes.katex = function(group, options, prev) {
+    // The KaTeX logo. The offsets for the K and a were chosen to look
+    // good, but the offsets for the T, E, and X were taken from the
+    // definition of \TeX in TeX (see TeXbook pg. 356)
+    var k = makeSpan(
+        ["k"], [buildCommon.mathsym("K", group.mode)]);
+    var a = makeSpan(
+        ["a"], [buildCommon.mathsym("A", group.mode)]);
+
+    a.height = (a.height + 0.2) * 0.75;
+    a.depth = (a.height - 0.2) * 0.75;
+
+    var t = makeSpan(
+        ["t"], [buildCommon.mathsym("T", group.mode)]);
+    var e = makeSpan(
+        ["e"], [buildCommon.mathsym("E", group.mode)]);
+
+    e.height = (e.height - 0.2155);
+    e.depth = (e.depth + 0.2155);
+
+    var x = makeSpan(
+        ["x"], [buildCommon.mathsym("X", group.mode)]);
+
+    return makeSpan(
+        ["katex-logo", "mord"], [k, a, t, e, x], options.getColor());
+};
+
+groupTypes.overline = function(group, options, prev) {
+    // Overlines are handled in the TeXbook pg 443, Rule 9.
+
+    // Build the inner group in the cramped style.
+    var innerGroup = buildGroup(group.value.body,
+            options.withStyle(options.style.cramp()));
+
+    var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+        options.style.sizeMultiplier;
+
+    // Create the line above the body
+    var line = makeSpan(
+        [options.style.reset(), Style.TEXT.cls(), "overline-line"]);
+    line.height = ruleWidth;
+    line.maxFontSize = 1.0;
+
+    // Generate the vlist, with the appropriate kerns
+    var vlist = buildCommon.makeVList([
+        {type: "elem", elem: innerGroup},
+        {type: "kern", size: 3 * ruleWidth},
+        {type: "elem", elem: line},
+        {type: "kern", size: ruleWidth},
+    ], "firstBaseline", null, options);
+
+    return makeSpan(["overline", "mord"], [vlist], options.getColor());
+};
+
+groupTypes.underline = function(group, options, prev) {
+    // Underlines are handled in the TeXbook pg 443, Rule 10.
+
+    // Build the inner group.
+    var innerGroup = buildGroup(group.value.body, options);
+
+    var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+        options.style.sizeMultiplier;
+
+    // Create the line above the body
+    var line = makeSpan(
+        [options.style.reset(), Style.TEXT.cls(), "underline-line"]);
+    line.height = ruleWidth;
+    line.maxFontSize = 1.0;
+
+    // Generate the vlist, with the appropriate kerns
+    var vlist = buildCommon.makeVList([
+        {type: "kern", size: ruleWidth},
+        {type: "elem", elem: line},
+        {type: "kern", size: 3 * ruleWidth},
+        {type: "elem", elem: innerGroup},
+    ], "top", innerGroup.height, options);
+
+    return makeSpan(["underline", "mord"], [vlist], options.getColor());
+};
+
+groupTypes.sqrt = function(group, options, prev) {
+    // Square roots are handled in the TeXbook pg. 443, Rule 11.
+
+    // First, we do the same steps as in overline to build the inner group
+    // and line
+    var inner = buildGroup(group.value.body,
+            options.withStyle(options.style.cramp()));
+
+    var ruleWidth = fontMetrics.metrics.defaultRuleThickness /
+        options.style.sizeMultiplier;
+
+    var line = makeSpan(
+        [options.style.reset(), Style.TEXT.cls(), "sqrt-line"], [],
+        options.getColor());
+    line.height = ruleWidth;
+    line.maxFontSize = 1.0;
+
+    var phi = ruleWidth;
+    if (options.style.id < Style.TEXT.id) {
+        phi = fontMetrics.metrics.xHeight;
+    }
+
+    // Calculate the clearance between the body and line
+    var lineClearance = ruleWidth + phi / 4;
+
+    var innerHeight =
+        (inner.height + inner.depth) * options.style.sizeMultiplier;
+    var minDelimiterHeight = innerHeight + lineClearance + ruleWidth;
+
+    // Create a \surd delimiter of the required minimum size
+    var delim = makeSpan(["sqrt-sign"], [
+        delimiter.customSizedDelim("\\surd", minDelimiterHeight,
+                                   false, options, group.mode)],
+                         options.getColor());
+
+    var delimDepth = (delim.height + delim.depth) - ruleWidth;
+
+    // Adjust the clearance based on the delimiter size
+    if (delimDepth > inner.height + inner.depth + lineClearance) {
+        lineClearance =
+            (lineClearance + delimDepth - inner.height - inner.depth) / 2;
+    }
+
+    // Shift the delimiter so that its top lines up with the top of the line
+    var delimShift = -(inner.height + lineClearance + ruleWidth) + delim.height;
+    delim.style.top = delimShift + "em";
+    delim.height -= delimShift;
+    delim.depth += delimShift;
+
+    // We add a special case here, because even when `inner` is empty, we
+    // still get a line. So, we use a simple heuristic to decide if we
+    // should omit the body entirely. (note this doesn't work for something
+    // like `\sqrt{\rlap{x}}`, but if someone is doing that they deserve for
+    // it not to work.
+    var body;
+    if (inner.height === 0 && inner.depth === 0) {
+        body = makeSpan();
+    } else {
+        body = buildCommon.makeVList([
+            {type: "elem", elem: inner},
+            {type: "kern", size: lineClearance},
+            {type: "elem", elem: line},
+            {type: "kern", size: ruleWidth},
+        ], "firstBaseline", null, options);
+    }
+
+    if (!group.value.index) {
+        return makeSpan(["sqrt", "mord"], [delim, body]);
+    } else {
+        // Handle the optional root index
+
+        // The index is always in scriptscript style
+        var root = buildGroup(
+            group.value.index,
+            options.withStyle(Style.SCRIPTSCRIPT));
+        var rootWrap = makeSpan(
+            [options.style.reset(), Style.SCRIPTSCRIPT.cls()],
+            [root]);
+
+        // Figure out the height and depth of the inner part
+        var innerRootHeight = Math.max(delim.height, body.height);
+        var innerRootDepth = Math.max(delim.depth, body.depth);
+
+        // The amount the index is shifted by. This is taken from the TeX
+        // source, in the definition of `\r@@t`.
+        var toShift = 0.6 * (innerRootHeight - innerRootDepth);
+
+        // Build a VList with the superscript shifted up correctly
+        var rootVList = buildCommon.makeVList(
+            [{type: "elem", elem: rootWrap}],
+            "shift", -toShift, options);
+        // Add a class surrounding it so we can add on the appropriate
+        // kerning
+        var rootVListWrap = makeSpan(["root"], [rootVList]);
+
+        return makeSpan(["sqrt", "mord"], [rootVListWrap, delim, body]);
+    }
+};
+
+groupTypes.sizing = function(group, options, prev) {
+    // Handle sizing operators like \Huge. Real TeX doesn't actually allow
+    // these functions inside of math expressions, so we do some special
+    // handling.
+    var inner = buildExpression(group.value.value,
+            options.withSize(group.value.size), prev);
+
+    var span = makeSpan(["mord"],
+        [makeSpan(["sizing", "reset-" + options.size, group.value.size,
+                   options.style.cls()],
+                  inner)]);
+
+    // Calculate the correct maxFontSize manually
+    var fontSize = buildCommon.sizingMultiplier[group.value.size];
+    span.maxFontSize = fontSize * options.style.sizeMultiplier;
+
+    return span;
+};
+
+groupTypes.styling = function(group, options, prev) {
+    // Style changes are handled in the TeXbook on pg. 442, Rule 3.
+
+    // Figure out what style we're changing to.
+    var style = {
+        "display": Style.DISPLAY,
+        "text": Style.TEXT,
+        "script": Style.SCRIPT,
+        "scriptscript": Style.SCRIPTSCRIPT,
+    };
+
+    var newStyle = style[group.value.style];
+
+    // Build the inner expression in the new style.
+    var inner = buildExpression(
+        group.value.value, options.withStyle(newStyle), prev);
+
+    return makeSpan([options.style.reset(), newStyle.cls()], inner);
+};
+
+groupTypes.font = function(group, options, prev) {
+    var font = group.value.font;
+    return buildGroup(group.value.body, options.withFont(font), prev);
+};
+
+groupTypes.delimsizing = function(group, options, prev) {
+    var delim = group.value.value;
+
+    if (delim === ".") {
+        // Empty delimiters still count as elements, even though they don't
+        // show anything.
+        return makeSpan([groupToType[group.value.delimType]]);
+    }
+
+    // Use delimiter.sizedDelim to generate the delimiter.
+    return makeSpan(
+        [groupToType[group.value.delimType]],
+        [delimiter.sizedDelim(
+            delim, group.value.size, options, group.mode)]);
+};
+
+groupTypes.leftright = function(group, options, prev) {
+    // Build the inner expression
+    var inner = buildExpression(group.value.body, options.reset());
+
+    var innerHeight = 0;
+    var innerDepth = 0;
+
+    // Calculate its height and depth
+    for (var i = 0; i < inner.length; i++) {
+        innerHeight = Math.max(inner[i].height, innerHeight);
+        innerDepth = Math.max(inner[i].depth, innerDepth);
+    }
+
+    // The size of delimiters is the same, regardless of what style we are
+    // in. Thus, to correctly calculate the size of delimiter we need around
+    // a group, we scale down the inner size based on the size.
+    innerHeight *= options.style.sizeMultiplier;
+    innerDepth *= options.style.sizeMultiplier;
+
+    var leftDelim;
+    if (group.value.left === ".") {
+        // Empty delimiters in \left and \right make null delimiter spaces.
+        leftDelim = makeNullDelimiter(options);
+    } else {
+        // Otherwise, use leftRightDelim to generate the correct sized
+        // delimiter.
+        leftDelim = delimiter.leftRightDelim(
+            group.value.left, innerHeight, innerDepth, options,
+            group.mode);
+    }
+    // Add it to the beginning of the expression
+    inner.unshift(leftDelim);
+
+    var rightDelim;
+    // Same for the right delimiter
+    if (group.value.right === ".") {
+        rightDelim = makeNullDelimiter(options);
+    } else {
+        rightDelim = delimiter.leftRightDelim(
+            group.value.right, innerHeight, innerDepth, options,
+            group.mode);
+    }
+    // Add it to the end of the expression.
+    inner.push(rightDelim);
+
+    return makeSpan(
+        ["minner", options.style.cls()], inner, options.getColor());
+};
+
+groupTypes.rule = function(group, options, prev) {
+    // Make an empty span for the rule
+    var rule = makeSpan(["mord", "rule"], [], options.getColor());
+
+    // Calculate the shift, width, and height of the rule, and account for units
+    var shift = 0;
+    if (group.value.shift) {
+        shift = group.value.shift.number;
+        if (group.value.shift.unit === "ex") {
+            shift *= fontMetrics.metrics.xHeight;
+        }
+    }
+
+    var width = group.value.width.number;
+    if (group.value.width.unit === "ex") {
+        width *= fontMetrics.metrics.xHeight;
+    }
+
+    var height = group.value.height.number;
+    if (group.value.height.unit === "ex") {
+        height *= fontMetrics.metrics.xHeight;
+    }
+
+    // The sizes of rules are absolute, so make it larger if we are in a
+    // smaller style.
+    shift /= options.style.sizeMultiplier;
+    width /= options.style.sizeMultiplier;
+    height /= options.style.sizeMultiplier;
+
+    // Style the rule to the right size
+    rule.style.borderRightWidth = width + "em";
+    rule.style.borderTopWidth = height + "em";
+    rule.style.bottom = shift + "em";
+
+    // Record the height and width
+    rule.width = width;
+    rule.height = height + shift;
+    rule.depth = -shift;
+
+    return rule;
+};
+
+groupTypes.kern = function(group, options, prev) {
+    // Make an empty span for the rule
+    var rule = makeSpan(["mord", "rule"], [], options.getColor());
+
+    var dimension = 0;
+    if (group.value.dimension) {
+        dimension = group.value.dimension.number;
+        if (group.value.dimension.unit === "ex") {
+            dimension *= fontMetrics.metrics.xHeight;
+        }
+    }
+
+    dimension /= options.style.sizeMultiplier;
+
+    rule.style.marginLeft = dimension + "em";
+
+    return rule;
+};
+
+groupTypes.accent = function(group, options, prev) {
+    // Accents are handled in the TeXbook pg. 443, rule 12.
+    var base = group.value.base;
+
+    var supsubGroup;
+    if (group.type === "supsub") {
+        // If our base is a character box, and we have superscripts and
+        // subscripts, the supsub will defer to us. In particular, we want
+        // to attach the superscripts and subscripts to the inner body (so
+        // that the position of the superscripts and subscripts won't be
+        // affected by the height of the accent). We accomplish this by
+        // sticking the base of the accent into the base of the supsub, and
+        // rendering that, while keeping track of where the accent is.
+
+        // The supsub group is the group that was passed in
+        var supsub = group;
+        // The real accent group is the base of the supsub group
+        group = supsub.value.base;
+        // The character box is the base of the accent group
+        base = group.value.base;
+        // Stick the character box into the base of the supsub group
+        supsub.value.base = base;
+
+        // Rerender the supsub group with its new base, and store that
+        // result.
+        supsubGroup = buildGroup(
+            supsub, options.reset(), prev);
+    }
+
+    // Build the base group
+    var body = buildGroup(
+        base, options.withStyle(options.style.cramp()));
+
+    // Calculate the skew of the accent. This is based on the line "If the
+    // nucleus is not a single character, let s = 0; otherwise set s to the
+    // kern amount for the nucleus followed by the \skewchar of its font."
+    // Note that our skew metrics are just the kern between each character
+    // and the skewchar.
+    var skew;
+    if (isCharacterBox(base)) {
+        // If the base is a character box, then we want the skew of the
+        // innermost character. To do that, we find the innermost character:
+        var baseChar = getBaseElem(base);
+        // Then, we render its group to get the symbol inside it
+        var baseGroup = buildGroup(
+            baseChar, options.withStyle(options.style.cramp()));
+        // Finally, we pull the skew off of the symbol.
+        skew = baseGroup.skew;
+        // Note that we now throw away baseGroup, because the layers we
+        // removed with getBaseElem might contain things like \color which
+        // we can't get rid of.
+        // TODO(emily): Find a better way to get the skew
+    } else {
+        skew = 0;
+    }
+
+    // calculate the amount of space between the body and the accent
+    var clearance = Math.min(body.height, fontMetrics.metrics.xHeight);
+
+    // Build the accent
+    var accent = buildCommon.makeSymbol(
+        group.value.accent, "Main-Regular", "math", options.getColor());
+    // Remove the italic correction of the accent, because it only serves to
+    // shift the accent over to a place we don't want.
+    accent.italic = 0;
+
+    // The \vec character that the fonts use is a combining character, and
+    // thus shows up much too far to the left. To account for this, we add a
+    // specific class which shifts the accent over to where we want it.
+    // TODO(emily): Fix this in a better way, like by changing the font
+    var vecClass = group.value.accent === "\\vec" ? "accent-vec" : null;
+
+    var accentBody = makeSpan(["accent-body", vecClass], [
+        makeSpan([], [accent])]);
+
+    accentBody = buildCommon.makeVList([
+        {type: "elem", elem: body},
+        {type: "kern", size: -clearance},
+        {type: "elem", elem: accentBody},
+    ], "firstBaseline", null, options);
+
+    // Shift the accent over by the skew. Note we shift by twice the skew
+    // because we are centering the accent, so by adding 2*skew to the left,
+    // we shift it to the right by 1*skew.
+    accentBody.children[1].style.marginLeft = 2 * skew + "em";
+
+    var accentWrap = makeSpan(["mord", "accent"], [accentBody]);
+
+    if (supsubGroup) {
+        // Here, we replace the "base" child of the supsub with our newly
+        // generated accent.
+        supsubGroup.children[0] = accentWrap;
+
+        // Since we don't rerun the height calculation after replacing the
+        // accent, we manually recalculate height.
+        supsubGroup.height = Math.max(accentWrap.height, supsubGroup.height);
+
+        // Accents should always be ords, even when their innards are not.
+        supsubGroup.classes[0] = "mord";
+
+        return supsubGroup;
+    } else {
+        return accentWrap;
+    }
+};
+
+groupTypes.phantom = function(group, options, prev) {
+    var elements = buildExpression(
+        group.value.value,
+        options.withPhantom(),
+        prev
+    );
+
+    // \phantom isn't supposed to affect the elements it contains.
+    // See "color" for more details.
+    return new buildCommon.makeFragment(elements);
+};
+
+/**
+ * buildGroup is the function that takes a group and calls the correct groupType
+ * function for it. It also handles the interaction of size and style changes
+ * between parents and children.
+ */
+var buildGroup = function(group, options, prev) {
+    if (!group) {
+        return makeSpan();
+    }
+
+    if (groupTypes[group.type]) {
+        // Call the groupTypes function
+        var groupNode = groupTypes[group.type](group, options, prev);
+        var multiplier;
+
+        // If the style changed between the parent and the current group,
+        // account for the size difference
+        if (options.style !== options.parentStyle) {
+            multiplier = options.style.sizeMultiplier /
+                    options.parentStyle.sizeMultiplier;
+
+            groupNode.height *= multiplier;
+            groupNode.depth *= multiplier;
+        }
+
+        // If the size changed between the parent and the current group, account
+        // for that size difference.
+        if (options.size !== options.parentSize) {
+            multiplier = buildCommon.sizingMultiplier[options.size] /
+                    buildCommon.sizingMultiplier[options.parentSize];
+
+            groupNode.height *= multiplier;
+            groupNode.depth *= multiplier;
+        }
+
+        return groupNode;
+    } else {
+        throw new ParseError(
+            "Got group of unknown type: '" + group.type + "'");
+    }
+};
+
+/**
+ * Take an entire parse tree, and build it into an appropriate set of HTML
+ * nodes.
+ */
+var buildHTML = function(tree, options) {
+    // buildExpression is destructive, so we need to make a clone
+    // of the incoming tree so that it isn't accidentally changed
+    tree = JSON.parse(JSON.stringify(tree));
+
+    // Build the expression contained in the tree
+    var expression = buildExpression(tree, options);
+    var body = makeSpan(["base", options.style.cls()], expression);
+
+    // Add struts, which ensure that the top of the HTML element falls at the
+    // height of the expression, and the bottom of the HTML element falls at the
+    // depth of the expression.
+    var topStrut = makeSpan(["strut"]);
+    var bottomStrut = makeSpan(["strut", "bottom"]);
+
+    topStrut.style.height = body.height + "em";
+    bottomStrut.style.height = (body.height + body.depth) + "em";
+    // We'd like to use `vertical-align: top` but in IE 9 this lowers the
+    // baseline of the box to the bottom of this strut (instead staying in the
+    // normal place) so we use an absolute value for vertical-align instead
+    bottomStrut.style.verticalAlign = -body.depth + "em";
+
+    // Wrap the struts and body together
+    var htmlNode = makeSpan(["katex-html"], [topStrut, bottomStrut, body]);
+
+    htmlNode.setAttribute("aria-hidden", "true");
+
+    return htmlNode;
+};
+
+module.exports = buildHTML;
+
+},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./delimiter":14,"./domTree":15,"./fontMetrics":17,"./utils":25}],12:[function(require,module,exports){
+/**
+ * This file converts a parse tree into a cooresponding MathML tree. The main
+ * entry point is the `buildMathML` function, which takes a parse tree from the
+ * parser.
+ */
+
+var buildCommon = require("./buildCommon");
+var fontMetrics = require("./fontMetrics");
+var mathMLTree = require("./mathMLTree");
+var ParseError = require("./ParseError");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+var fontMap = buildCommon.fontMap;
+
+/**
+ * Takes a symbol and converts it into a MathML text node after performing
+ * optional replacement from symbols.js.
+ */
+var makeText = function(text, mode) {
+    if (symbols[mode][text] && symbols[mode][text].replace) {
+        text = symbols[mode][text].replace;
+    }
+
+    return new mathMLTree.TextNode(text);
+};
+
+/**
+ * Returns the math variant as a string or null if none is required.
+ */
+var getVariant = function(group, options) {
+    var font = options.font;
+    if (!font) {
+        return null;
+    }
+
+    var mode = group.mode;
+    if (font === "mathit") {
+        return "italic";
+    }
+
+    var value = group.value;
+    if (utils.contains(["\\imath", "\\jmath"], value)) {
+        return null;
+    }
+
+    if (symbols[mode][value] && symbols[mode][value].replace) {
+        value = symbols[mode][value].replace;
+    }
+
+    var fontName = fontMap[font].fontName;
+    if (fontMetrics.getCharacterMetrics(value, fontName)) {
+        return fontMap[options.font].variant;
+    }
+
+    return null;
+};
+
+/**
+ * Functions for handling the different types of groups found in the parse
+ * tree. Each function should take a parse group and return a MathML node.
+ */
+var groupTypes = {};
+
+groupTypes.mathord = function(group, options) {
+    var node = new mathMLTree.MathNode(
+        "mi",
+        [makeText(group.value, group.mode)]);
+
+    var variant = getVariant(group, options);
+    if (variant) {
+        node.setAttribute("mathvariant", variant);
+    }
+    return node;
+};
+
+groupTypes.textord = function(group, options) {
+    var text = makeText(group.value, group.mode);
+
+    var variant = getVariant(group, options) || "normal";
+
+    var node;
+    if (/[0-9]/.test(group.value)) {
+        // TODO(kevinb) merge adjacent <mn> nodes
+        // do it as a post processing step
+        node = new mathMLTree.MathNode("mn", [text]);
+        if (options.font) {
+            node.setAttribute("mathvariant", variant);
+        }
+    } else {
+        node = new mathMLTree.MathNode("mi", [text]);
+        node.setAttribute("mathvariant", variant);
+    }
+
+    return node;
+};
+
+groupTypes.bin = function(group) {
+    var node = new mathMLTree.MathNode(
+        "mo", [makeText(group.value, group.mode)]);
+
+    return node;
+};
+
+groupTypes.rel = function(group) {
+    var node = new mathMLTree.MathNode(
+        "mo", [makeText(group.value, group.mode)]);
+
+    return node;
+};
+
+groupTypes.open = function(group) {
+    var node = new mathMLTree.MathNode(
+        "mo", [makeText(group.value, group.mode)]);
+
+    return node;
+};
+
+groupTypes.close = function(group) {
+    var node = new mathMLTree.MathNode(
+        "mo", [makeText(group.value, group.mode)]);
+
+    return node;
+};
+
+groupTypes.inner = function(group) {
+    var node = new mathMLTree.MathNode(
+        "mo", [makeText(group.value, group.mode)]);
+
+    return node;
+};
+
+groupTypes.punct = function(group) {
+    var node = new mathMLTree.MathNode(
+        "mo", [makeText(group.value, group.mode)]);
+
+    node.setAttribute("separator", "true");
+
+    return node;
+};
+
+groupTypes.ordgroup = function(group, options) {
+    var inner = buildExpression(group.value, options);
+
+    var node = new mathMLTree.MathNode("mrow", inner);
+
+    return node;
+};
+
+groupTypes.text = function(group, options) {
+    var inner = buildExpression(group.value.body, options);
+
+    var node = new mathMLTree.MathNode("mtext", inner);
+
+    return node;
+};
+
+groupTypes.color = function(group, options) {
+    var inner = buildExpression(group.value.value, options);
+
+    var node = new mathMLTree.MathNode("mstyle", inner);
+
+    node.setAttribute("mathcolor", group.value.color);
+
+    return node;
+};
+
+groupTypes.supsub = function(group, options) {
+    var children = [buildGroup(group.value.base, options)];
+
+    if (group.value.sub) {
+        children.push(buildGroup(group.value.sub, options));
+    }
+
+    if (group.value.sup) {
+        children.push(buildGroup(group.value.sup, options));
+    }
+
+    var nodeType;
+    if (!group.value.sub) {
+        nodeType = "msup";
+    } else if (!group.value.sup) {
+        nodeType = "msub";
+    } else {
+        nodeType = "msubsup";
+    }
+
+    var node = new mathMLTree.MathNode(nodeType, children);
+
+    return node;
+};
+
+groupTypes.genfrac = function(group, options) {
+    var node = new mathMLTree.MathNode(
+        "mfrac",
+        [buildGroup(group.value.numer, options),
+         buildGroup(group.value.denom, options)]);
+
+    if (!group.value.hasBarLine) {
+        node.setAttribute("linethickness", "0px");
+    }
+
+    if (group.value.leftDelim != null || group.value.rightDelim != null) {
+        var withDelims = [];
+
+        if (group.value.leftDelim != null) {
+            var leftOp = new mathMLTree.MathNode(
+                "mo", [new mathMLTree.TextNode(group.value.leftDelim)]);
+
+            leftOp.setAttribute("fence", "true");
+
+            withDelims.push(leftOp);
+        }
+
+        withDelims.push(node);
+
+        if (group.value.rightDelim != null) {
+            var rightOp = new mathMLTree.MathNode(
+                "mo", [new mathMLTree.TextNode(group.value.rightDelim)]);
+
+            rightOp.setAttribute("fence", "true");
+
+            withDelims.push(rightOp);
+        }
+
+        var outerNode = new mathMLTree.MathNode("mrow", withDelims);
+
+        return outerNode;
+    }
+
+    return node;
+};
+
+groupTypes.array = function(group, options) {
+    return new mathMLTree.MathNode(
+        "mtable", group.value.body.map(function(row) {
+            return new mathMLTree.MathNode(
+                "mtr", row.map(function(cell) {
+                    return new mathMLTree.MathNode(
+                        "mtd", [buildGroup(cell, options)]);
+                }));
+        }));
+};
+
+groupTypes.sqrt = function(group, options) {
+    var node;
+    if (group.value.index) {
+        node = new mathMLTree.MathNode(
+            "mroot", [
+                buildGroup(group.value.body, options),
+                buildGroup(group.value.index, options),
+            ]);
+    } else {
+        node = new mathMLTree.MathNode(
+            "msqrt", [buildGroup(group.value.body, options)]);
+    }
+
+    return node;
+};
+
+groupTypes.leftright = function(group, options) {
+    var inner = buildExpression(group.value.body, options);
+
+    if (group.value.left !== ".") {
+        var leftNode = new mathMLTree.MathNode(
+            "mo", [makeText(group.value.left, group.mode)]);
+
+        leftNode.setAttribute("fence", "true");
+
+        inner.unshift(leftNode);
+    }
+
+    if (group.value.right !== ".") {
+        var rightNode = new mathMLTree.MathNode(
+            "mo", [makeText(group.value.right, group.mode)]);
+
+        rightNode.setAttribute("fence", "true");
+
+        inner.push(rightNode);
+    }
+
+    var outerNode = new mathMLTree.MathNode("mrow", inner);
+
+    return outerNode;
+};
+
+groupTypes.accent = function(group, options) {
+    var accentNode = new mathMLTree.MathNode(
+        "mo", [makeText(group.value.accent, group.mode)]);
+
+    var node = new mathMLTree.MathNode(
+        "mover",
+        [buildGroup(group.value.base, options),
+         accentNode]);
+
+    node.setAttribute("accent", "true");
+
+    return node;
+};
+
+groupTypes.spacing = function(group) {
+    var node;
+
+    if (group.value === "\\ " || group.value === "\\space" ||
+        group.value === " " || group.value === "~") {
+        node = new mathMLTree.MathNode(
+            "mtext", [new mathMLTree.TextNode("\u00a0")]);
+    } else {
+        node = new mathMLTree.MathNode("mspace");
+
+        node.setAttribute(
+            "width", buildCommon.spacingFunctions[group.value].size);
+    }
+
+    return node;
+};
+
+groupTypes.op = function(group) {
+    var node;
+
+    // TODO(emily): handle big operators using the `largeop` attribute
+
+    if (group.value.symbol) {
+        // This is a symbol. Just add the symbol.
+        node = new mathMLTree.MathNode(
+            "mo", [makeText(group.value.body, group.mode)]);
+    } else {
+        // This is a text operator. Add all of the characters from the
+        // operator's name.
+        // TODO(emily): Add a space in the middle of some of these
+        // operators, like \limsup.
+        node = new mathMLTree.MathNode(
+            "mi", [new mathMLTree.TextNode(group.value.body.slice(1))]);
+    }
+
+    return node;
+};
+
+groupTypes.katex = function(group) {
+    var node = new mathMLTree.MathNode(
+        "mtext", [new mathMLTree.TextNode("KaTeX")]);
+
+    return node;
+};
+
+groupTypes.font = function(group, options) {
+    var font = group.value.font;
+    return buildGroup(group.value.body, options.withFont(font));
+};
+
+groupTypes.delimsizing = function(group) {
+    var children = [];
+
+    if (group.value.value !== ".") {
+        children.push(makeText(group.value.value, group.mode));
+    }
+
+    var node = new mathMLTree.MathNode("mo", children);
+
+    if (group.value.delimType === "open" ||
+        group.value.delimType === "close") {
+        // Only some of the delimsizing functions act as fences, and they
+        // return "open" or "close" delimTypes.
+        node.setAttribute("fence", "true");
+    } else {
+        // Explicitly disable fencing if it's not a fence, to override the
+        // defaults.
+        node.setAttribute("fence", "false");
+    }
+
+    return node;
+};
+
+groupTypes.styling = function(group, options) {
+    var inner = buildExpression(group.value.value, options);
+
+    var node = new mathMLTree.MathNode("mstyle", inner);
+
+    var styleAttributes = {
+        "display": ["0", "true"],
+        "text": ["0", "false"],
+        "script": ["1", "false"],
+        "scriptscript": ["2", "false"],
+    };
+
+    var attr = styleAttributes[group.value.style];
+
+    node.setAttribute("scriptlevel", attr[0]);
+    node.setAttribute("displaystyle", attr[1]);
+
+    return node;
+};
+
+groupTypes.sizing = function(group, options) {
+    var inner = buildExpression(group.value.value, options);
+
+    var node = new mathMLTree.MathNode("mstyle", inner);
+
+    // TODO(emily): This doesn't produce the correct size for nested size
+    // changes, because we don't keep state of what style we're currently
+    // in, so we can't reset the size to normal before changing it.  Now
+    // that we're passing an options parameter we should be able to fix
+    // this.
+    node.setAttribute(
+        "mathsize", buildCommon.sizingMultiplier[group.value.size] + "em");
+
+    return node;
+};
+
+groupTypes.overline = function(group, options) {
+    var operator = new mathMLTree.MathNode(
+        "mo", [new mathMLTree.TextNode("\u203e")]);
+    operator.setAttribute("stretchy", "true");
+
+    var node = new mathMLTree.MathNode(
+        "mover",
+        [buildGroup(group.value.body, options),
+         operator]);
+    node.setAttribute("accent", "true");
+
+    return node;
+};
+
+groupTypes.underline = function(group, options) {
+    var operator = new mathMLTree.MathNode(
+        "mo", [new mathMLTree.TextNode("\u203e")]);
+    operator.setAttribute("stretchy", "true");
+
+    var node = new mathMLTree.MathNode(
+        "munder",
+        [buildGroup(group.value.body, options),
+         operator]);
+    node.setAttribute("accentunder", "true");
+
+    return node;
+};
+
+groupTypes.rule = function(group) {
+    // TODO(emily): Figure out if there's an actual way to draw black boxes
+    // in MathML.
+    var node = new mathMLTree.MathNode("mrow");
+
+    return node;
+};
+
+groupTypes.kern = function(group) {
+    // TODO(kevin): Figure out if there's a way to add space in MathML
+    var node = new mathMLTree.MathNode("mrow");
+
+    return node;
+};
+
+groupTypes.llap = function(group, options) {
+    var node = new mathMLTree.MathNode(
+        "mpadded", [buildGroup(group.value.body, options)]);
+
+    node.setAttribute("lspace", "-1width");
+    node.setAttribute("width", "0px");
+
+    return node;
+};
+
+groupTypes.rlap = function(group, options) {
+    var node = new mathMLTree.MathNode(
+        "mpadded", [buildGroup(group.value.body, options)]);
+
+    node.setAttribute("width", "0px");
+
+    return node;
+};
+
+groupTypes.phantom = function(group, options, prev) {
+    var inner = buildExpression(group.value.value, options);
+    return new mathMLTree.MathNode("mphantom", inner);
+};
+
+/**
+ * Takes a list of nodes, builds them, and returns a list of the generated
+ * MathML nodes. A little simpler than the HTML version because we don't do any
+ * previous-node handling.
+ */
+var buildExpression = function(expression, options) {
+    var groups = [];
+    for (var i = 0; i < expression.length; i++) {
+        var group = expression[i];
+        groups.push(buildGroup(group, options));
+    }
+    return groups;
+};
+
+/**
+ * Takes a group from the parser and calls the appropriate groupTypes function
+ * on it to produce a MathML node.
+ */
+var buildGroup = function(group, options) {
+    if (!group) {
+        return new mathMLTree.MathNode("mrow");
+    }
+
+    if (groupTypes[group.type]) {
+        // Call the groupTypes function
+        return groupTypes[group.type](group, options);
+    } else {
+        throw new ParseError(
+            "Got group of unknown type: '" + group.type + "'");
+    }
+};
+
+/**
+ * Takes a full parse tree and settings and builds a MathML representation of
+ * it. In particular, we put the elements from building the parse tree into a
+ * <semantics> tag so we can also include that TeX source as an annotation.
+ *
+ * Note that we actually return a domTree element with a `<math>` inside it so
+ * we can do appropriate styling.
+ */
+var buildMathML = function(tree, texExpression, options) {
+    var expression = buildExpression(tree, options);
+
+    // Wrap up the expression in an mrow so it is presented in the semantics
+    // tag correctly.
+    var wrapper = new mathMLTree.MathNode("mrow", expression);
+
+    // Build a TeX annotation of the source
+    var annotation = new mathMLTree.MathNode(
+        "annotation", [new mathMLTree.TextNode(texExpression)]);
+
+    annotation.setAttribute("encoding", "application/x-tex");
+
+    var semantics = new mathMLTree.MathNode(
+        "semantics", [wrapper, annotation]);
+
+    var math = new mathMLTree.MathNode("math", [semantics]);
+
+    // You can't style <math> nodes, so we wrap the node in a span.
+    return makeSpan(["katex-mathml"], [math]);
+};
+
+module.exports = buildMathML;
+
+},{"./ParseError":6,"./buildCommon":10,"./fontMetrics":17,"./mathMLTree":20,"./symbols":23,"./utils":25}],13:[function(require,module,exports){
+var buildHTML = require("./buildHTML");
+var buildMathML = require("./buildMathML");
+var buildCommon = require("./buildCommon");
+var Options = require("./Options");
+var Settings = require("./Settings");
+var Style = require("./Style");
+
+var makeSpan = buildCommon.makeSpan;
+
+var buildTree = function(tree, expression, settings) {
+    settings = settings || new Settings({});
+
+    var startStyle = Style.TEXT;
+    if (settings.displayMode) {
+        startStyle = Style.DISPLAY;
+    }
+
+    // Setup the default options
+    var options = new Options({
+        style: startStyle,
+        size: "size5",
+    });
+
+    // `buildHTML` sometimes messes with the parse tree (like turning bins ->
+    // ords), so we build the MathML version first.
+    var mathMLNode = buildMathML(tree, expression, options);
+    var htmlNode = buildHTML(tree, options);
+
+    var katexNode = makeSpan(["katex"], [
+        mathMLNode, htmlNode,
+    ]);
+
+    if (settings.displayMode) {
+        return makeSpan(["katex-display"], [katexNode]);
+    } else {
+        return katexNode;
+    }
+};
+
+module.exports = buildTree;
+
+},{"./Options":5,"./Settings":8,"./Style":9,"./buildCommon":10,"./buildHTML":11,"./buildMathML":12}],14:[function(require,module,exports){
+/**
+ * This file deals with creating delimiters of various sizes. The TeXbook
+ * discusses these routines on page 441-442, in the "Another subroutine sets box
+ * x to a specified variable delimiter" paragraph.
+ *
+ * There are three main routines here. `makeSmallDelim` makes a delimiter in the
+ * normal font, but in either text, script, or scriptscript style.
+ * `makeLargeDelim` makes a delimiter in textstyle, but in one of the Size1,
+ * Size2, Size3, or Size4 fonts. `makeStackedDelim` makes a delimiter out of
+ * smaller pieces that are stacked on top of one another.
+ *
+ * The functions take a parameter `center`, which determines if the delimiter
+ * should be centered around the axis.
+ *
+ * Then, there are three exposed functions. `sizedDelim` makes a delimiter in
+ * one of the given sizes. This is used for things like `\bigl`.
+ * `customSizedDelim` makes a delimiter with a given total height+depth. It is
+ * called in places like `\sqrt`. `leftRightDelim` makes an appropriate
+ * delimiter which surrounds an expression of a given height an depth. It is
+ * used in `\left` and `\right`.
+ */
+
+var ParseError = require("./ParseError");
+var Style = require("./Style");
+
+var buildCommon = require("./buildCommon");
+var fontMetrics = require("./fontMetrics");
+var symbols = require("./symbols");
+var utils = require("./utils");
+
+var makeSpan = buildCommon.makeSpan;
+
+/**
+ * Get the metrics for a given symbol and font, after transformation (i.e.
+ * after following replacement from symbols.js)
+ */
+var getMetrics = function(symbol, font) {
+    if (symbols.math[symbol] && symbols.math[symbol].replace) {
+        return fontMetrics.getCharacterMetrics(
+            symbols.math[symbol].replace, font);
+    } else {
+        return fontMetrics.getCharacterMetrics(
+            symbol, font);
+    }
+};
+
+/**
+ * Builds a symbol in the given font size (note size is an integer)
+ */
+var mathrmSize = function(value, size, mode) {
+    return buildCommon.makeSymbol(value, "Size" + size + "-Regular", mode);
+};
+
+/**
+ * Puts a delimiter span in a given style, and adds appropriate height, depth,
+ * and maxFontSizes.
+ */
+var styleWrap = function(delim, toStyle, options) {
+    var span = makeSpan(
+        ["style-wrap", options.style.reset(), toStyle.cls()], [delim]);
+
+    var multiplier = toStyle.sizeMultiplier / options.style.sizeMultiplier;
+
+    span.height *= multiplier;
+    span.depth *= multiplier;
+    span.maxFontSize = toStyle.sizeMultiplier;
+
+    return span;
+};
+
+/**
+ * Makes a small delimiter. This is a delimiter that comes in the Main-Regular
+ * font, but is restyled to either be in textstyle, scriptstyle, or
+ * scriptscriptstyle.
+ */
+var makeSmallDelim = function(delim, style, center, options, mode) {
+    var text = buildCommon.makeSymbol(delim, "Main-Regular", mode);
+
+    var span = styleWrap(text, style, options);
+
+    if (center) {
+        var shift =
+            (1 - options.style.sizeMultiplier / style.sizeMultiplier) *
+            fontMetrics.metrics.axisHeight;
+
+        span.style.top = shift + "em";
+        span.height -= shift;
+        span.depth += shift;
+    }
+
+    return span;
+};
+
+/**
+ * Makes a large delimiter. This is a delimiter that comes in the Size1, Size2,
+ * Size3, or Size4 fonts. It is always rendered in textstyle.
+ */
+var makeLargeDelim = function(delim, size, center, options, mode) {
+    var inner = mathrmSize(delim, size, mode);
+
+    var span = styleWrap(
+        makeSpan(["delimsizing", "size" + size],
+                 [inner], options.getColor()),
+        Style.TEXT, options);
+
+    if (center) {
+        var shift = (1 - options.style.sizeMultiplier) *
+            fontMetrics.metrics.axisHeight;
+
+        span.style.top = shift + "em";
+        span.height -= shift;
+        span.depth += shift;
+    }
+
+    return span;
+};
+
+/**
+ * Make an inner span with the given offset and in the given font. This is used
+ * in `makeStackedDelim` to make the stacking pieces for the delimiter.
+ */
+var makeInner = function(symbol, font, mode) {
+    var sizeClass;
+    // Apply the correct CSS class to choose the right font.
+    if (font === "Size1-Regular") {
+        sizeClass = "delim-size1";
+    } else if (font === "Size4-Regular") {
+        sizeClass = "delim-size4";
+    }
+
+    var inner = makeSpan(
+        ["delimsizinginner", sizeClass],
+        [makeSpan([], [buildCommon.makeSymbol(symbol, font, mode)])]);
+
+    // Since this will be passed into `makeVList` in the end, wrap the element
+    // in the appropriate tag that VList uses.
+    return {type: "elem", elem: inner};
+};
+
+/**
+ * Make a stacked delimiter out of a given delimiter, with the total height at
+ * least `heightTotal`. This routine is mentioned on page 442 of the TeXbook.
+ */
+var makeStackedDelim = function(delim, heightTotal, center, options, mode) {
+    // There are four parts, the top, an optional middle, a repeated part, and a
+    // bottom.
+    var top;
+    var middle;
+    var repeat;
+    var bottom;
+    top = repeat = bottom = delim;
+    middle = null;
+    // Also keep track of what font the delimiters are in
+    var font = "Size1-Regular";
+
+    // We set the parts and font based on the symbol. Note that we use
+    // '\u23d0' instead of '|' and '\u2016' instead of '\\|' for the
+    // repeats of the arrows
+    if (delim === "\\uparrow") {
+        repeat = bottom = "\u23d0";
+    } else if (delim === "\\Uparrow") {
+        repeat = bottom = "\u2016";
+    } else if (delim === "\\downarrow") {
+        top = repeat = "\u23d0";
+    } else if (delim === "\\Downarrow") {
+        top = repeat = "\u2016";
+    } else if (delim === "\\updownarrow") {
+        top = "\\uparrow";
+        repeat = "\u23d0";
+        bottom = "\\downarrow";
+    } else if (delim === "\\Updownarrow") {
+        top = "\\Uparrow";
+        repeat = "\u2016";
+        bottom = "\\Downarrow";
+    } else if (delim === "[" || delim === "\\lbrack") {
+        top = "\u23a1";
+        repeat = "\u23a2";
+        bottom = "\u23a3";
+        font = "Size4-Regular";
+    } else if (delim === "]" || delim === "\\rbrack") {
+        top = "\u23a4";
+        repeat = "\u23a5";
+        bottom = "\u23a6";
+        font = "Size4-Regular";
+    } else if (delim === "\\lfloor") {
+        repeat = top = "\u23a2";
+        bottom = "\u23a3";
+        font = "Size4-Regular";
+    } else if (delim === "\\lceil") {
+        top = "\u23a1";
+        repeat = bottom = "\u23a2";
+        font = "Size4-Regular";
+    } else if (delim === "\\rfloor") {
+        repeat = top = "\u23a5";
+        bottom = "\u23a6";
+        font = "Size4-Regular";
+    } else if (delim === "\\rceil") {
+        top = "\u23a4";
+        repeat = bottom = "\u23a5";
+        font = "Size4-Regular";
+    } else if (delim === "(") {
+        top = "\u239b";
+        repeat = "\u239c";
+        bottom = "\u239d";
+        font = "Size4-Regular";
+    } else if (delim === ")") {
+        top = "\u239e";
+        repeat = "\u239f";
+        bottom = "\u23a0";
+        font = "Size4-Regular";
+    } else if (delim === "\\{" || delim === "\\lbrace") {
+        top = "\u23a7";
+        middle = "\u23a8";
+        bottom = "\u23a9";
+        repeat = "\u23aa";
+        font = "Size4-Regular";
+    } else if (delim === "\\}" || delim === "\\rbrace") {
+        top = "\u23ab";
+        middle = "\u23ac";
+        bottom = "\u23ad";
+        repeat = "\u23aa";
+        font = "Size4-Regular";
+    } else if (delim === "\\lgroup") {
+        top = "\u23a7";
+        bottom = "\u23a9";
+        repeat = "\u23aa";
+        font = "Size4-Regular";
+    } else if (delim === "\\rgroup") {
+        top = "\u23ab";
+        bottom = "\u23ad";
+        repeat = "\u23aa";
+        font = "Size4-Regular";
+    } else if (delim === "\\lmoustache") {
+        top = "\u23a7";
+        bottom = "\u23ad";
+        repeat = "\u23aa";
+        font = "Size4-Regular";
+    } else if (delim === "\\rmoustache") {
+        top = "\u23ab";
+        bottom = "\u23a9";
+        repeat = "\u23aa";
+        font = "Size4-Regular";
+    } else if (delim === "\\surd") {
+        top = "\ue001";
+        bottom = "\u23b7";
+        repeat = "\ue000";
+        font = "Size4-Regular";
+    }
+
+    // Get the metrics of the four sections
+    var topMetrics = getMetrics(top, font);
+    var topHeightTotal = topMetrics.height + topMetrics.depth;
+    var repeatMetrics = getMetrics(repeat, font);
+    var repeatHeightTotal = repeatMetrics.height + repeatMetrics.depth;
+    var bottomMetrics = getMetrics(bottom, font);
+    var bottomHeightTotal = bottomMetrics.height + bottomMetrics.depth;
+    var middleHeightTotal = 0;
+    var middleFactor = 1;
+    if (middle !== null) {
+        var middleMetrics = getMetrics(middle, font);
+        middleHeightTotal = middleMetrics.height + middleMetrics.depth;
+        middleFactor = 2; // repeat symmetrically above and below middle
+    }
+
+    // Calcuate the minimal height that the delimiter can have.
+    // It is at least the size of the top, bottom, and optional middle combined.
+    var minHeight = topHeightTotal + bottomHeightTotal + middleHeightTotal;
+
+    // Compute the number of copies of the repeat symbol we will need
+    var repeatCount = Math.ceil(
+        (heightTotal - minHeight) / (middleFactor * repeatHeightTotal));
+
+    // Compute the total height of the delimiter including all the symbols
+    var realHeightTotal =
+        minHeight + repeatCount * middleFactor * repeatHeightTotal;
+
+    // The center of the delimiter is placed at the center of the axis. Note
+    // that in this context, "center" means that the delimiter should be
+    // centered around the axis in the current style, while normally it is
+    // centered around the axis in textstyle.
+    var axisHeight = fontMetrics.metrics.axisHeight;
+    if (center) {
+        axisHeight *= options.style.sizeMultiplier;
+    }
+    // Calculate the depth
+    var depth = realHeightTotal / 2 - axisHeight;
+
+    // Now, we start building the pieces that will go into the vlist
+
+    // Keep a list of the inner pieces
+    var inners = [];
+
+    // Add the bottom symbol
+    inners.push(makeInner(bottom, font, mode));
+
+    var i;
+    if (middle === null) {
+        // Add that many symbols
+        for (i = 0; i < repeatCount; i++) {
+            inners.push(makeInner(repeat, font, mode));
+        }
+    } else {
+        // When there is a middle bit, we need the middle part and two repeated
+        // sections
+        for (i = 0; i < repeatCount; i++) {
+            inners.push(makeInner(repeat, font, mode));
+        }
+        inners.push(makeInner(middle, font, mode));
+        for (i = 0; i < repeatCount; i++) {
+            inners.push(makeInner(repeat, font, mode));
+        }
+    }
+
+    // Add the top symbol
+    inners.push(makeInner(top, font, mode));
+
+    // Finally, build the vlist
+    var inner = buildCommon.makeVList(inners, "bottom", depth, options);
+
+    return styleWrap(
+        makeSpan(["delimsizing", "mult"], [inner], options.getColor()),
+        Style.TEXT, options);
+};
+
+// There are three kinds of delimiters, delimiters that stack when they become
+// too large
+var stackLargeDelimiters = [
+    "(", ")", "[", "\\lbrack", "]", "\\rbrack",
+    "\\{", "\\lbrace", "\\}", "\\rbrace",
+    "\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
+    "\\surd",
+];
+
+// delimiters that always stack
+var stackAlwaysDelimiters = [
+    "\\uparrow", "\\downarrow", "\\updownarrow",
+    "\\Uparrow", "\\Downarrow", "\\Updownarrow",
+    "|", "\\|", "\\vert", "\\Vert",
+    "\\lvert", "\\rvert", "\\lVert", "\\rVert",
+    "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
+];
+
+// and delimiters that never stack
+var stackNeverDelimiters = [
+    "<", ">", "\\langle", "\\rangle", "/", "\\backslash", "\\lt", "\\gt",
+];
+
+// Metrics of the different sizes. Found by looking at TeX's output of
+// $\bigl| // \Bigl| \biggl| \Biggl| \showlists$
+// Used to create stacked delimiters of appropriate sizes in makeSizedDelim.
+var sizeToMaxHeight = [0, 1.2, 1.8, 2.4, 3.0];
+
+/**
+ * Used to create a delimiter of a specific size, where `size` is 1, 2, 3, or 4.
+ */
+var makeSizedDelim = function(delim, size, options, mode) {
+    // < and > turn into \langle and \rangle in delimiters
+    if (delim === "<" || delim === "\\lt") {
+        delim = "\\langle";
+    } else if (delim === ">" || delim === "\\gt") {
+        delim = "\\rangle";
+    }
+
+    // Sized delimiters are never centered.
+    if (utils.contains(stackLargeDelimiters, delim) ||
+        utils.contains(stackNeverDelimiters, delim)) {
+        return makeLargeDelim(delim, size, false, options, mode);
+    } else if (utils.contains(stackAlwaysDelimiters, delim)) {
+        return makeStackedDelim(
+            delim, sizeToMaxHeight[size], false, options, mode);
+    } else {
+        throw new ParseError("Illegal delimiter: '" + delim + "'");
+    }
+};
+
+/**
+ * There are three different sequences of delimiter sizes that the delimiters
+ * follow depending on the kind of delimiter. This is used when creating custom
+ * sized delimiters to decide whether to create a small, large, or stacked
+ * delimiter.
+ *
+ * In real TeX, these sequences aren't explicitly defined, but are instead
+ * defined inside the font metrics. Since there are only three sequences that
+ * are possible for the delimiters that TeX defines, it is easier to just encode
+ * them explicitly here.
+ */
+
+// Delimiters that never stack try small delimiters and large delimiters only
+var stackNeverDelimiterSequence = [
+    {type: "small", style: Style.SCRIPTSCRIPT},
+    {type: "small", style: Style.SCRIPT},
+    {type: "small", style: Style.TEXT},
+    {type: "large", size: 1},
+    {type: "large", size: 2},
+    {type: "large", size: 3},
+    {type: "large", size: 4},
+];
+
+// Delimiters that always stack try the small delimiters first, then stack
+var stackAlwaysDelimiterSequence = [
+    {type: "small", style: Style.SCRIPTSCRIPT},
+    {type: "small", style: Style.SCRIPT},
+    {type: "small", style: Style.TEXT},
+    {type: "stack"},
+];
+
+// Delimiters that stack when large try the small and then large delimiters, and
+// stack afterwards
+var stackLargeDelimiterSequence = [
+    {type: "small", style: Style.SCRIPTSCRIPT},
+    {type: "small", style: Style.SCRIPT},
+    {type: "small", style: Style.TEXT},
+    {type: "large", size: 1},
+    {type: "large", size: 2},
+    {type: "large", size: 3},
+    {type: "large", size: 4},
+    {type: "stack"},
+];
+
+/**
+ * Get the font used in a delimiter based on what kind of delimiter it is.
+ */
+var delimTypeToFont = function(type) {
+    if (type.type === "small") {
+        return "Main-Regular";
+    } else if (type.type === "large") {
+        return "Size" + type.size + "-Regular";
+    } else if (type.type === "stack") {
+        return "Size4-Regular";
+    }
+};
+
+/**
+ * Traverse a sequence of types of delimiters to decide what kind of delimiter
+ * should be used to create a delimiter of the given height+depth.
+ */
+var traverseSequence = function(delim, height, sequence, options) {
+    // Here, we choose the index we should start at in the sequences. In smaller
+    // sizes (which correspond to larger numbers in style.size) we start earlier
+    // in the sequence. Thus, scriptscript starts at index 3-3=0, script starts
+    // at index 3-2=1, text starts at 3-1=2, and display starts at min(2,3-0)=2
+    var start = Math.min(2, 3 - options.style.size);
+    for (var i = start; i < sequence.length; i++) {
+        if (sequence[i].type === "stack") {
+            // This is always the last delimiter, so we just break the loop now.
+            break;
+        }
+
+        var metrics = getMetrics(delim, delimTypeToFont(sequence[i]));
+        var heightDepth = metrics.height + metrics.depth;
+
+        // Small delimiters are scaled down versions of the same font, so we
+        // account for the style change size.
+
+        if (sequence[i].type === "small") {
+            heightDepth *= sequence[i].style.sizeMultiplier;
+        }
+
+        // Check if the delimiter at this size works for the given height.
+        if (heightDepth > height) {
+            return sequence[i];
+        }
+    }
+
+    // If we reached the end of the sequence, return the last sequence element.
+    return sequence[sequence.length - 1];
+};
+
+/**
+ * Make a delimiter of a given height+depth, with optional centering. Here, we
+ * traverse the sequences, and create a delimiter that the sequence tells us to.
+ */
+var makeCustomSizedDelim = function(delim, height, center, options, mode) {
+    if (delim === "<" || delim === "\\lt") {
+        delim = "\\langle";
+    } else if (delim === ">" || delim === "\\gt") {
+        delim = "\\rangle";
+    }
+
+    // Decide what sequence to use
+    var sequence;
+    if (utils.contains(stackNeverDelimiters, delim)) {
+        sequence = stackNeverDelimiterSequence;
+    } else if (utils.contains(stackLargeDelimiters, delim)) {
+        sequence = stackLargeDelimiterSequence;
+    } else {
+        sequence = stackAlwaysDelimiterSequence;
+    }
+
+    // Look through the sequence
+    var delimType = traverseSequence(delim, height, sequence, options);
+
+    // Depending on the sequence element we decided on, call the appropriate
+    // function.
+    if (delimType.type === "small") {
+        return makeSmallDelim(delim, delimType.style, center, options, mode);
+    } else if (delimType.type === "large") {
+        return makeLargeDelim(delim, delimType.size, center, options, mode);
+    } else if (delimType.type === "stack") {
+        return makeStackedDelim(delim, height, center, options, mode);
+    }
+};
+
+/**
+ * Make a delimiter for use with `\left` and `\right`, given a height and depth
+ * of an expression that the delimiters surround.
+ */
+var makeLeftRightDelim = function(delim, height, depth, options, mode) {
+    // We always center \left/\right delimiters, so the axis is always shifted
+    var axisHeight =
+        fontMetrics.metrics.axisHeight * options.style.sizeMultiplier;
+
+    // Taken from TeX source, tex.web, function make_left_right
+    var delimiterFactor = 901;
+    var delimiterExtend = 5.0 / fontMetrics.metrics.ptPerEm;
+
+    var maxDistFromAxis = Math.max(
+        height - axisHeight, depth + axisHeight);
+
+    var totalHeight = Math.max(
+        // In real TeX, calculations are done using integral values which are
+        // 65536 per pt, or 655360 per em. So, the division here truncates in
+        // TeX but doesn't here, producing different results. If we wanted to
+        // exactly match TeX's calculation, we could do
+        //   Math.floor(655360 * maxDistFromAxis / 500) *
+        //    delimiterFactor / 655360
+        // (To see the difference, compare
+        //    x^{x^{\left(\rule{0.1em}{0.68em}\right)}}
+        // in TeX and KaTeX)
+        maxDistFromAxis / 500 * delimiterFactor,
+        2 * maxDistFromAxis - delimiterExtend);
+
+    // Finally, we defer to `makeCustomSizedDelim` with our calculated total
+    // height
+    return makeCustomSizedDelim(delim, totalHeight, true, options, mode);
+};
+
+module.exports = {
+    sizedDelim: makeSizedDelim,
+    customSizedDelim: makeCustomSizedDelim,
+    leftRightDelim: makeLeftRightDelim,
+};
+
+},{"./ParseError":6,"./Style":9,"./buildCommon":10,"./fontMetrics":17,"./symbols":23,"./utils":25}],15:[function(require,module,exports){
+/**
+ * These objects store the data about the DOM nodes we create, as well as some
+ * extra data. They can then be transformed into real DOM nodes with the
+ * `toNode` function or HTML markup using `toMarkup`. They are useful for both
+ * storing extra properties on the nodes, as well as providing a way to easily
+ * work with the DOM.
+ *
+ * Similar functions for working with MathML nodes exist in mathMLTree.js.
+ */
+var unicodeRegexes = require("./unicodeRegexes");
+var utils = require("./utils");
+
+/**
+ * Create an HTML className based on a list of classes. In addition to joining
+ * with spaces, we also remove null or empty classes.
+ */
+var createClass = function(classes) {
+    classes = classes.slice();
+    for (var i = classes.length - 1; i >= 0; i--) {
+        if (!classes[i]) {
+            classes.splice(i, 1);
+        }
+    }
+
+    return classes.join(" ");
+};
+
+/**
+ * This node represents a span node, with a className, a list of children, and
+ * an inline style. It also contains information about its height, depth, and
+ * maxFontSize.
+ */
+function span(classes, children, height, depth, maxFontSize, style) {
+    this.classes = classes || [];
+    this.children = children || [];
+    this.height = height || 0;
+    this.depth = depth || 0;
+    this.maxFontSize = maxFontSize || 0;
+    this.style = style || {};
+    this.attributes = {};
+}
+
+/**
+ * Sets an arbitrary attribute on the span. Warning: use this wisely. Not all
+ * browsers support attributes the same, and having too many custom attributes
+ * is probably bad.
+ */
+span.prototype.setAttribute = function(attribute, value) {
+    this.attributes[attribute] = value;
+};
+
+/**
+ * Convert the span into an HTML node
+ */
+span.prototype.toNode = function() {
+    var span = document.createElement("span");
+
+    // Apply the class
+    span.className = createClass(this.classes);
+
+    // Apply inline styles
+    for (var style in this.style) {
+        if (Object.prototype.hasOwnProperty.call(this.style, style)) {
+            span.style[style] = this.style[style];
+        }
+    }
+
+    // Apply attributes
+    for (var attr in this.attributes) {
+        if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+            span.setAttribute(attr, this.attributes[attr]);
+        }
+    }
+
+    // Append the children, also as HTML nodes
+    for (var i = 0; i < this.children.length; i++) {
+        span.appendChild(this.children[i].toNode());
+    }
+
+    return span;
+};
+
+/**
+ * Convert the span into an HTML markup string
+ */
+span.prototype.toMarkup = function() {
+    var markup = "<span";
+
+    // Add the class
+    if (this.classes.length) {
+        markup += " class=\"";
+        markup += utils.escape(createClass(this.classes));
+        markup += "\"";
+    }
+
+    var styles = "";
+
+    // Add the styles, after hyphenation
+    for (var style in this.style) {
+        if (this.style.hasOwnProperty(style)) {
+            styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
+        }
+    }
+
+    if (styles) {
+        markup += " style=\"" + utils.escape(styles) + "\"";
+    }
+
+    // Add the attributes
+    for (var attr in this.attributes) {
+        if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+            markup += " " + attr + "=\"";
+            markup += utils.escape(this.attributes[attr]);
+            markup += "\"";
+        }
+    }
+
+    markup += ">";
+
+    // Add the markup of the children, also as markup
+    for (var i = 0; i < this.children.length; i++) {
+        markup += this.children[i].toMarkup();
+    }
+
+    markup += "</span>";
+
+    return markup;
+};
+
+/**
+ * This node represents a document fragment, which contains elements, but when
+ * placed into the DOM doesn't have any representation itself. Thus, it only
+ * contains children and doesn't have any HTML properties. It also keeps track
+ * of a height, depth, and maxFontSize.
+ */
+function documentFragment(children, height, depth, maxFontSize) {
+    this.children = children || [];
+    this.height = height || 0;
+    this.depth = depth || 0;
+    this.maxFontSize = maxFontSize || 0;
+}
+
+/**
+ * Convert the fragment into a node
+ */
+documentFragment.prototype.toNode = function() {
+    // Create a fragment
+    var frag = document.createDocumentFragment();
+
+    // Append the children
+    for (var i = 0; i < this.children.length; i++) {
+        frag.appendChild(this.children[i].toNode());
+    }
+
+    return frag;
+};
+
+/**
+ * Convert the fragment into HTML markup
+ */
+documentFragment.prototype.toMarkup = function() {
+    var markup = "";
+
+    // Simply concatenate the markup for the children together
+    for (var i = 0; i < this.children.length; i++) {
+        markup += this.children[i].toMarkup();
+    }
+
+    return markup;
+};
+
+var iCombinations = {
+    'î': '\u0131\u0302',
+    'ï': '\u0131\u0308',
+    'í': '\u0131\u0301',
+    // 'ī': '\u0131\u0304', // enable when we add Extended Latin
+    'ì': '\u0131\u0300',
+};
+
+/**
+ * A symbol node contains information about a single symbol. It either renders
+ * to a single text node, or a span with a single text node in it, depending on
+ * whether it has CSS classes, styles, or needs italic correction.
+ */
+function symbolNode(value, height, depth, italic, skew, classes, style) {
+    this.value = value || "";
+    this.height = height || 0;
+    this.depth = depth || 0;
+    this.italic = italic || 0;
+    this.skew = skew || 0;
+    this.classes = classes || [];
+    this.style = style || {};
+    this.maxFontSize = 0;
+
+    // Mark CJK characters with specific classes so that we can specify which
+    // fonts to use.  This allows us to render these characters with a serif
+    // font in situations where the browser would either default to a sans serif
+    // or render a placeholder character.
+    if (unicodeRegexes.cjkRegex.test(value)) {
+        // I couldn't find any fonts that contained Hangul as well as all of
+        // the other characters we wanted to test there for it gets its own
+        // CSS class.
+        if (unicodeRegexes.hangulRegex.test(value)) {
+            this.classes.push('hangul_fallback');
+        } else {
+            this.classes.push('cjk_fallback');
+        }
+    }
+
+    if (/[îïíì]/.test(this.value)) {    // add ī when we add Extended Latin
+        this.value = iCombinations[this.value];
+    }
+}
+
+/**
+ * Creates a text node or span from a symbol node. Note that a span is only
+ * created if it is needed.
+ */
+symbolNode.prototype.toNode = function() {
+    var node = document.createTextNode(this.value);
+    var span = null;
+
+    if (this.italic > 0) {
+        span = document.createElement("span");
+        span.style.marginRight = this.italic + "em";
+    }
+
+    if (this.classes.length > 0) {
+        span = span || document.createElement("span");
+        span.className = createClass(this.classes);
+    }
+
+    for (var style in this.style) {
+        if (this.style.hasOwnProperty(style)) {
+            span = span || document.createElement("span");
+            span.style[style] = this.style[style];
+        }
+    }
+
+    if (span) {
+        span.appendChild(node);
+        return span;
+    } else {
+        return node;
+    }
+};
+
+/**
+ * Creates markup for a symbol node.
+ */
+symbolNode.prototype.toMarkup = function() {
+    // TODO(alpert): More duplication than I'd like from
+    // span.prototype.toMarkup and symbolNode.prototype.toNode...
+    var needsSpan = false;
+
+    var markup = "<span";
+
+    if (this.classes.length) {
+        needsSpan = true;
+        markup += " class=\"";
+        markup += utils.escape(createClass(this.classes));
+        markup += "\"";
+    }
+
+    var styles = "";
+
+    if (this.italic > 0) {
+        styles += "margin-right:" + this.italic + "em;";
+    }
+    for (var style in this.style) {
+        if (this.style.hasOwnProperty(style)) {
+            styles += utils.hyphenate(style) + ":" + this.style[style] + ";";
+        }
+    }
+
+    if (styles) {
+        needsSpan = true;
+        markup += " style=\"" + utils.escape(styles) + "\"";
+    }
+
+    var escaped = utils.escape(this.value);
+    if (needsSpan) {
+        markup += ">";
+        markup += escaped;
+        markup += "</span>";
+        return markup;
+    } else {
+        return escaped;
+    }
+};
+
+module.exports = {
+    span: span,
+    documentFragment: documentFragment,
+    symbolNode: symbolNode,
+};
+
+},{"./unicodeRegexes":24,"./utils":25}],16:[function(require,module,exports){
+/* eslint no-constant-condition:0 */
+var fontMetrics = require("./fontMetrics");
+var parseData = require("./parseData");
+var ParseError = require("./ParseError");
+
+var ParseNode = parseData.ParseNode;
+
+/**
+ * Parse the body of the environment, with rows delimited by \\ and
+ * columns delimited by &, and create a nested list in row-major order
+ * with one group per cell.
+ */
+function parseArray(parser, result) {
+    var row = [];
+    var body = [row];
+    var rowGaps = [];
+    while (true) {
+        var cell = parser.parseExpression(false, null);
+        row.push(new ParseNode("ordgroup", cell, parser.mode));
+        var next = parser.nextToken.text;
+        if (next === "&") {
+            parser.consume();
+        } else if (next === "\\end") {
+            break;
+        } else if (next === "\\\\" || next === "\\cr") {
+            var cr = parser.parseFunction();
+            rowGaps.push(cr.value.size);
+            row = [];
+            body.push(row);
+        } else {
+            throw new ParseError("Expected & or \\\\ or \\end",
+                                 parser.nextToken);
+        }
+    }
+    result.body = body;
+    result.rowGaps = rowGaps;
+    return new ParseNode(result.type, result, parser.mode);
+}
+
+/*
+ * An environment definition is very similar to a function definition:
+ * it is declared with a name or a list of names, a set of properties
+ * and a handler containing the actual implementation.
+ *
+ * The properties include:
+ *  - numArgs: The number of arguments after the \begin{name} function.
+ *  - argTypes: (optional) Just like for a function
+ *  - allowedInText: (optional) Whether or not the environment is allowed inside
+ *                   text mode (default false) (not enforced yet)
+ *  - numOptionalArgs: (optional) Just like for a function
+ * A bare number instead of that object indicates the numArgs value.
+ *
+ * The handler function will receive two arguments
+ *  - context: information and references provided by the parser
+ *  - args: an array of arguments passed to \begin{name}
+ * The context contains the following properties:
+ *  - envName: the name of the environment, one of the listed names.
+ *  - parser: the parser object
+ *  - lexer: the lexer object
+ *  - positions: the positions associated with these arguments from args.
+ * The handler must return a ParseResult.
+ */
+
+function defineEnvironment(names, props, handler) {
+    if (typeof names === "string") {
+        names = [names];
+    }
+    if (typeof props === "number") {
+        props = { numArgs: props };
+    }
+    // Set default values of environments
+    var data = {
+        numArgs: props.numArgs || 0,
+        argTypes: props.argTypes,
+        greediness: 1,
+        allowedInText: !!props.allowedInText,
+        numOptionalArgs: props.numOptionalArgs || 0,
+        handler: handler,
+    };
+    for (var i = 0; i < names.length; ++i) {
+        module.exports[names[i]] = data;
+    }
+}
+
+// Arrays are part of LaTeX, defined in lttab.dtx so its documentation
+// is part of the source2e.pdf file of LaTeX2e source documentation.
+defineEnvironment("array", {
+    numArgs: 1,
+}, function(context, args) {
+    var colalign = args[0];
+    colalign = colalign.value.map ? colalign.value : [colalign];
+    var cols = colalign.map(function(node) {
+        var ca = node.value;
+        if ("lcr".indexOf(ca) !== -1) {
+            return {
+                type: "align",
+                align: ca,
+            };
+        } else if (ca === "|") {
+            return {
+                type: "separator",
+                separator: "|",
+            };
+        }
+        throw new ParseError(
+            "Unknown column alignment: " + node.value,
+            node);
+    });
+    var res = {
+        type: "array",
+        cols: cols,
+        hskipBeforeAndAfter: true, // \@preamble in lttab.dtx
+    };
+    res = parseArray(context.parser, res);
+    return res;
+});
+
+// The matrix environments of amsmath builds on the array environment
+// of LaTeX, which is discussed above.
+defineEnvironment([
+    "matrix",
+    "pmatrix",
+    "bmatrix",
+    "Bmatrix",
+    "vmatrix",
+    "Vmatrix",
+], {
+}, function(context) {
+    var delimiters = {
+        "matrix": null,
+        "pmatrix": ["(", ")"],
+        "bmatrix": ["[", "]"],
+        "Bmatrix": ["\\{", "\\}"],
+        "vmatrix": ["|", "|"],
+        "Vmatrix": ["\\Vert", "\\Vert"],
+    }[context.envName];
+    var res = {
+        type: "array",
+        hskipBeforeAndAfter: false, // \hskip -\arraycolsep in amsmath
+    };
+    res = parseArray(context.parser, res);
+    if (delimiters) {
+        res = new ParseNode("leftright", {
+            body: [res],
+            left: delimiters[0],
+            right: delimiters[1],
+        }, context.mode);
+    }
+    return res;
+});
+
+// A cases environment (in amsmath.sty) is almost equivalent to
+// \def\arraystretch{1.2}%
+// \left\{\begin{array}{@{}l@{\quad}l@{}} … \end{array}\right.
+defineEnvironment("cases", {
+}, function(context) {
+    var res = {
+        type: "array",
+        arraystretch: 1.2,
+        cols: [{
+            type: "align",
+            align: "l",
+            pregap: 0,
+            postgap: fontMetrics.metrics.quad,
+        }, {
+            type: "align",
+            align: "l",
+            pregap: 0,
+            postgap: 0,
+        }],
+    };
+    res = parseArray(context.parser, res);
+    res = new ParseNode("leftright", {
+        body: [res],
+        left: "\\{",
+        right: ".",
+    }, context.mode);
+    return res;
+});
+
+// An aligned environment is like the align* environment
+// except it operates within math mode.
+// Note that we assume \nomallineskiplimit to be zero,
+// so that \strut@ is the same as \strut.
+defineEnvironment("aligned", {
+}, function(context) {
+    var res = {
+        type: "array",
+        cols: [],
+    };
+    res = parseArray(context.parser, res);
+    var emptyGroup = new ParseNode("ordgroup", [], context.mode);
+    var numCols = 0;
+    res.value.body.forEach(function(row) {
+        var i;
+        for (i = 1; i < row.length; i += 2) {
+            row[i].value.unshift(emptyGroup);
+        }
+        if (numCols < row.length) {
+            numCols = row.length;
+        }
+    });
+    for (var i = 0; i < numCols; ++i) {
+        var align = "r";
+        var pregap = 0;
+        if (i % 2 === 1) {
+            align = "l";
+        } else if (i > 0) {
+            pregap = 2; // one \qquad between columns
+        }
+        res.value.cols[i] = {
+            type: "align",
+            align: align,
+            pregap: pregap,
+            postgap: 0,
+        };
+    }
+    return res;
+});
+
+},{"./ParseError":6,"./fontMetrics":17,"./parseData":21}],17:[function(require,module,exports){
+/* eslint no-unused-vars:0 */
+
+var Style = require("./Style");
+var cjkRegex = require("./unicodeRegexes").cjkRegex;
+
+/**
+ * This file contains metrics regarding fonts and individual symbols. The sigma
+ * and xi variables, as well as the metricMap map contain data extracted from
+ * TeX, TeX font metrics, and the TTF files. These data are then exposed via the
+ * `metrics` variable and the getCharacterMetrics function.
+ */
+
+// These font metrics are extracted from TeX by using
+// \font\a=cmmi10
+// \showthe\fontdimenX\a
+// where X is the corresponding variable number. These correspond to the font
+// parameters of the symbol fonts. In TeX, there are actually three sets of
+// dimensions, one for each of textstyle, scriptstyle, and scriptscriptstyle,
+// but we only use the textstyle ones, and scale certain dimensions accordingly.
+// See the TeXbook, page 441.
+var sigma1 = 0.025;
+var sigma2 = 0;
+var sigma3 = 0;
+var sigma4 = 0;
+var sigma5 = 0.431;
+var sigma6 = 1;
+var sigma7 = 0;
+var sigma8 = 0.677;
+var sigma9 = 0.394;
+var sigma10 = 0.444;
+var sigma11 = 0.686;
+var sigma12 = 0.345;
+var sigma13 = 0.413;
+var sigma14 = 0.363;
+var sigma15 = 0.289;
+var sigma16 = 0.150;
+var sigma17 = 0.247;
+var sigma18 = 0.386;
+var sigma19 = 0.050;
+var sigma20 = 2.390;
+var sigma21 = 1.01;
+var sigma21Script = 0.81;
+var sigma21ScriptScript = 0.71;
+var sigma22 = 0.250;
+
+// These font metrics are extracted from TeX by using
+// \font\a=cmex10
+// \showthe\fontdimenX\a
+// where X is the corresponding variable number. These correspond to the font
+// parameters of the extension fonts (family 3). See the TeXbook, page 441.
+var xi1 = 0;
+var xi2 = 0;
+var xi3 = 0;
+var xi4 = 0;
+var xi5 = 0.431;
+var xi6 = 1;
+var xi7 = 0;
+var xi8 = 0.04;
+var xi9 = 0.111;
+var xi10 = 0.166;
+var xi11 = 0.2;
+var xi12 = 0.6;
+var xi13 = 0.1;
+
+// This value determines how large a pt is, for metrics which are defined in
+// terms of pts.
+// This value is also used in katex.less; if you change it make sure the values
+// match.
+var ptPerEm = 10.0;
+
+// The space between adjacent `|` columns in an array definition. From
+// `\showthe\doublerulesep` in LaTeX.
+var doubleRuleSep = 2.0 / ptPerEm;
+
+/**
+ * This is just a mapping from common names to real metrics
+ */
+var metrics = {
+    xHeight: sigma5,
+    quad: sigma6,
+    num1: sigma8,
+    num2: sigma9,
+    num3: sigma10,
+    denom1: sigma11,
+    denom2: sigma12,
+    sup1: sigma13,
+    sup2: sigma14,
+    sup3: sigma15,
+    sub1: sigma16,
+    sub2: sigma17,
+    supDrop: sigma18,
+    subDrop: sigma19,
+    axisHeight: sigma22,
+    defaultRuleThickness: xi8,
+    bigOpSpacing1: xi9,
+    bigOpSpacing2: xi10,
+    bigOpSpacing3: xi11,
+    bigOpSpacing4: xi12,
+    bigOpSpacing5: xi13,
+    ptPerEm: ptPerEm,
+    emPerEx: sigma5 / sigma6,
+    doubleRuleSep: doubleRuleSep,
+
+    // TODO(alpert): Missing parallel structure here. We should probably add
+    // style-specific metrics for all of these.
+    delim1: sigma20,
+    getDelim2: function(style) {
+        if (style.size === Style.TEXT.size) {
+            return sigma21;
+        } else if (style.size === Style.SCRIPT.size) {
+            return sigma21Script;
+        } else if (style.size === Style.SCRIPTSCRIPT.size) {
+            return sigma21ScriptScript;
+        }
+        throw new Error("Unexpected style size: " + style.size);
+    },
+};
+
+// This map contains a mapping from font name and character code to character
+// metrics, including height, depth, italic correction, and skew (kern from the
+// character to the corresponding \skewchar)
+// This map is generated via `make metrics`. It should not be changed manually.
+var metricMap = require("./fontMetricsData");
+
+// These are very rough approximations.  We default to Times New Roman which
+// should have Latin-1 and Cyrillic characters, but may not depending on the
+// operating system.  The metrics do not account for extra height from the
+// accents.  In the case of Cyrillic characters which have both ascenders and
+// descenders we prefer approximations with ascenders, primarily to prevent
+// the fraction bar or root line from intersecting the glyph.
+// TODO(kevinb) allow union of multiple glyph metrics for better accuracy.
+var extraCharacterMap = {
+    // Latin-1
+    'À': 'A',
+    'Á': 'A',
+    'Â': 'A',
+    'Ã': 'A',
+    'Ä': 'A',
+    'Å': 'A',
+    'Æ': 'A',
+    'Ç': 'C',
+    'È': 'E',
+    'É': 'E',
+    'Ê': 'E',
+    'Ë': 'E',
+    'Ì': 'I',
+    'Í': 'I',
+    'Î': 'I',
+    'Ï': 'I',
+    'Ð': 'D',
+    'Ñ': 'N',
+    'Ò': 'O',
+    'Ó': 'O',
+    'Ô': 'O',
+    'Õ': 'O',
+    'Ö': 'O',
+    'Ø': 'O',
+    'Ù': 'U',
+    'Ú': 'U',
+    'Û': 'U',
+    'Ü': 'U',
+    'Ý': 'Y',
+    'Þ': 'o',
+    'ß': 'B',
+    'à': 'a',
+    'á': 'a',
+    'â': 'a',
+    'ã': 'a',
+    'ä': 'a',
+    'å': 'a',
+    'æ': 'a',
+    'ç': 'c',
+    'è': 'e',
+    'é': 'e',
+    'ê': 'e',
+    'ë': 'e',
+    'ì': 'i',
+    'í': 'i',
+    'î': 'i',
+    'ï': 'i',
+    'ð': 'd',
+    'ñ': 'n',
+    'ò': 'o',
+    'ó': 'o',
+    'ô': 'o',
+    'õ': 'o',
+    'ö': 'o',
+    'ø': 'o',
+    'ù': 'u',
+    'ú': 'u',
+    'û': 'u',
+    'ü': 'u',
+    'ý': 'y',
+    'þ': 'o',
+    'ÿ': 'y',
+
+    // Cyrillic
+    'А': 'A',
+    'Б': 'B',
+    'В': 'B',
+    'Г': 'F',
+    'Д': 'A',
+    'Е': 'E',
+    'Ж': 'K',
+    'З': '3',
+    'И': 'N',
+    'Й': 'N',
+    'К': 'K',
+    'Л': 'N',
+    'М': 'M',
+    'Н': 'H',
+    'О': 'O',
+    'П': 'N',
+    'Р': 'P',
+    'С': 'C',
+    'Т': 'T',
+    'У': 'y',
+    'Ф': 'O',
+    'Х': 'X',
+    'Ц': 'U',
+    'Ч': 'h',
+    'Ш': 'W',
+    'Щ': 'W',
+    'Ъ': 'B',
+    'Ы': 'X',
+    'Ь': 'B',
+    'Э': '3',
+    'Ю': 'X',
+    'Я': 'R',
+    'а': 'a',
+    'б': 'b',
+    'в': 'a',
+    'г': 'r',
+    'д': 'y',
+    'е': 'e',
+    'ж': 'm',
+    'з': 'e',
+    'и': 'n',
+    'й': 'n',
+    'к': 'n',
+    'л': 'n',
+    'м': 'm',
+    'н': 'n',
+    'о': 'o',
+    'п': 'n',
+    'р': 'p',
+    'с': 'c',
+    'т': 'o',
+    'у': 'y',
+    'ф': 'b',
+    'х': 'x',
+    'ц': 'n',
+    'ч': 'n',
+    'ш': 'w',
+    'щ': 'w',
+    'ъ': 'a',
+    'ы': 'm',
+    'ь': 'a',
+    'э': 'e',
+    'ю': 'm',
+    'я': 'r',
+};
+
+/**
+ * This function is a convenience function for looking up information in the
+ * metricMap table. It takes a character as a string, and a style.
+ *
+ * Note: the `width` property may be undefined if fontMetricsData.js wasn't
+ * built using `Make extended_metrics`.
+ */
+var getCharacterMetrics = function(character, style) {
+    var ch = character.charCodeAt(0);
+    if (character[0] in extraCharacterMap) {
+        ch = extraCharacterMap[character[0]].charCodeAt(0);
+    } else if (cjkRegex.test(character[0])) {
+        ch = 'M'.charCodeAt(0);
+    }
+    var metrics = metricMap[style][ch];
+    if (metrics) {
+        return {
+            depth: metrics[0],
+            height: metrics[1],
+            italic: metrics[2],
+            skew: metrics[3],
+            width: metrics[4],
+        };
+    }
+};
+
+module.exports = {
+    metrics: metrics,
+    getCharacterMetrics: getCharacterMetrics,
+};
+
+},{"./Style":9,"./fontMetricsData":18,"./unicodeRegexes":24}],18:[function(require,module,exports){
+module.exports = {
+    "AMS-Regular": {
+        "65": [0, 0.68889, 0, 0],
+        "66": [0, 0.68889, 0, 0],
+        "67": [0, 0.68889, 0, 0],
+        "68": [0, 0.68889, 0, 0],
+        "69": [0, 0.68889, 0, 0],
+        "70": [0, 0.68889, 0, 0],
+        "71": [0, 0.68889, 0, 0],
+        "72": [0, 0.68889, 0, 0],
+        "73": [0, 0.68889, 0, 0],
+        "74": [0.16667, 0.68889, 0, 0],
+        "75": [0, 0.68889, 0, 0],
+        "76": [0, 0.68889, 0, 0],
+        "77": [0, 0.68889, 0, 0],
+        "78": [0, 0.68889, 0, 0],
+        "79": [0.16667, 0.68889, 0, 0],
+        "80": [0, 0.68889, 0, 0],
+        "81": [0.16667, 0.68889, 0, 0],
+        "82": [0, 0.68889, 0, 0],
+        "83": [0, 0.68889, 0, 0],
+        "84": [0, 0.68889, 0, 0],
+        "85": [0, 0.68889, 0, 0],
+        "86": [0, 0.68889, 0, 0],
+        "87": [0, 0.68889, 0, 0],
+        "88": [0, 0.68889, 0, 0],
+        "89": [0, 0.68889, 0, 0],
+        "90": [0, 0.68889, 0, 0],
+        "107": [0, 0.68889, 0, 0],
+        "165": [0, 0.675, 0.025, 0],
+        "174": [0.15559, 0.69224, 0, 0],
+        "240": [0, 0.68889, 0, 0],
+        "295": [0, 0.68889, 0, 0],
+        "710": [0, 0.825, 0, 0],
+        "732": [0, 0.9, 0, 0],
+        "770": [0, 0.825, 0, 0],
+        "771": [0, 0.9, 0, 0],
+        "989": [0.08167, 0.58167, 0, 0],
+        "1008": [0, 0.43056, 0.04028, 0],
+        "8245": [0, 0.54986, 0, 0],
+        "8463": [0, 0.68889, 0, 0],
+        "8487": [0, 0.68889, 0, 0],
+        "8498": [0, 0.68889, 0, 0],
+        "8502": [0, 0.68889, 0, 0],
+        "8503": [0, 0.68889, 0, 0],
+        "8504": [0, 0.68889, 0, 0],
+        "8513": [0, 0.68889, 0, 0],
+        "8592": [-0.03598, 0.46402, 0, 0],
+        "8594": [-0.03598, 0.46402, 0, 0],
+        "8602": [-0.13313, 0.36687, 0, 0],
+        "8603": [-0.13313, 0.36687, 0, 0],
+        "8606": [0.01354, 0.52239, 0, 0],
+        "8608": [0.01354, 0.52239, 0, 0],
+        "8610": [0.01354, 0.52239, 0, 0],
+        "8611": [0.01354, 0.52239, 0, 0],
+        "8619": [0, 0.54986, 0, 0],
+        "8620": [0, 0.54986, 0, 0],
+        "8621": [-0.13313, 0.37788, 0, 0],
+        "8622": [-0.13313, 0.36687, 0, 0],
+        "8624": [0, 0.69224, 0, 0],
+        "8625": [0, 0.69224, 0, 0],
+        "8630": [0, 0.43056, 0, 0],
+        "8631": [0, 0.43056, 0, 0],
+        "8634": [0.08198, 0.58198, 0, 0],
+        "8635": [0.08198, 0.58198, 0, 0],
+        "8638": [0.19444, 0.69224, 0, 0],
+        "8639": [0.19444, 0.69224, 0, 0],
+        "8642": [0.19444, 0.69224, 0, 0],
+        "8643": [0.19444, 0.69224, 0, 0],
+        "8644": [0.1808, 0.675, 0, 0],
+        "8646": [0.1808, 0.675, 0, 0],
+        "8647": [0.1808, 0.675, 0, 0],
+        "8648": [0.19444, 0.69224, 0, 0],
+        "8649": [0.1808, 0.675, 0, 0],
+        "8650": [0.19444, 0.69224, 0, 0],
+        "8651": [0.01354, 0.52239, 0, 0],
+        "8652": [0.01354, 0.52239, 0, 0],
+        "8653": [-0.13313, 0.36687, 0, 0],
+        "8654": [-0.13313, 0.36687, 0, 0],
+        "8655": [-0.13313, 0.36687, 0, 0],
+        "8666": [0.13667, 0.63667, 0, 0],
+        "8667": [0.13667, 0.63667, 0, 0],
+        "8669": [-0.13313, 0.37788, 0, 0],
+        "8672": [-0.064, 0.437, 0, 0],
+        "8674": [-0.064, 0.437, 0, 0],
+        "8705": [0, 0.825, 0, 0],
+        "8708": [0, 0.68889, 0, 0],
+        "8709": [0.08167, 0.58167, 0, 0],
+        "8717": [0, 0.43056, 0, 0],
+        "8722": [-0.03598, 0.46402, 0, 0],
+        "8724": [0.08198, 0.69224, 0, 0],
+        "8726": [0.08167, 0.58167, 0, 0],
+        "8733": [0, 0.69224, 0, 0],
+        "8736": [0, 0.69224, 0, 0],
+        "8737": [0, 0.69224, 0, 0],
+        "8738": [0.03517, 0.52239, 0, 0],
+        "8739": [0.08167, 0.58167, 0, 0],
+        "8740": [0.25142, 0.74111, 0, 0],
+        "8741": [0.08167, 0.58167, 0, 0],
+        "8742": [0.25142, 0.74111, 0, 0],
+        "8756": [0, 0.69224, 0, 0],
+        "8757": [0, 0.69224, 0, 0],
+        "8764": [-0.13313, 0.36687, 0, 0],
+        "8765": [-0.13313, 0.37788, 0, 0],
+        "8769": [-0.13313, 0.36687, 0, 0],
+        "8770": [-0.03625, 0.46375, 0, 0],
+        "8774": [0.30274, 0.79383, 0, 0],
+        "8776": [-0.01688, 0.48312, 0, 0],
+        "8778": [0.08167, 0.58167, 0, 0],
+        "8782": [0.06062, 0.54986, 0, 0],
+        "8783": [0.06062, 0.54986, 0, 0],
+        "8785": [0.08198, 0.58198, 0, 0],
+        "8786": [0.08198, 0.58198, 0, 0],
+        "8787": [0.08198, 0.58198, 0, 0],
+        "8790": [0, 0.69224, 0, 0],
+        "8791": [0.22958, 0.72958, 0, 0],
+        "8796": [0.08198, 0.91667, 0, 0],
+        "8806": [0.25583, 0.75583, 0, 0],
+        "8807": [0.25583, 0.75583, 0, 0],
+        "8808": [0.25142, 0.75726, 0, 0],
+        "8809": [0.25142, 0.75726, 0, 0],
+        "8812": [0.25583, 0.75583, 0, 0],
+        "8814": [0.20576, 0.70576, 0, 0],
+        "8815": [0.20576, 0.70576, 0, 0],
+        "8816": [0.30274, 0.79383, 0, 0],
+        "8817": [0.30274, 0.79383, 0, 0],
+        "8818": [0.22958, 0.72958, 0, 0],
+        "8819": [0.22958, 0.72958, 0, 0],
+        "8822": [0.1808, 0.675, 0, 0],
+        "8823": [0.1808, 0.675, 0, 0],
+        "8828": [0.13667, 0.63667, 0, 0],
+        "8829": [0.13667, 0.63667, 0, 0],
+        "8830": [0.22958, 0.72958, 0, 0],
+        "8831": [0.22958, 0.72958, 0, 0],
+        "8832": [0.20576, 0.70576, 0, 0],
+        "8833": [0.20576, 0.70576, 0, 0],
+        "8840": [0.30274, 0.79383, 0, 0],
+        "8841": [0.30274, 0.79383, 0, 0],
+        "8842": [0.13597, 0.63597, 0, 0],
+        "8843": [0.13597, 0.63597, 0, 0],
+        "8847": [0.03517, 0.54986, 0, 0],
+        "8848": [0.03517, 0.54986, 0, 0],
+        "8858": [0.08198, 0.58198, 0, 0],
+        "8859": [0.08198, 0.58198, 0, 0],
+        "8861": [0.08198, 0.58198, 0, 0],
+        "8862": [0, 0.675, 0, 0],
+        "8863": [0, 0.675, 0, 0],
+        "8864": [0, 0.675, 0, 0],
+        "8865": [0, 0.675, 0, 0],
+        "8872": [0, 0.69224, 0, 0],
+        "8873": [0, 0.69224, 0, 0],
+        "8874": [0, 0.69224, 0, 0],
+        "8876": [0, 0.68889, 0, 0],
+        "8877": [0, 0.68889, 0, 0],
+        "8878": [0, 0.68889, 0, 0],
+        "8879": [0, 0.68889, 0, 0],
+        "8882": [0.03517, 0.54986, 0, 0],
+        "8883": [0.03517, 0.54986, 0, 0],
+        "8884": [0.13667, 0.63667, 0, 0],
+        "8885": [0.13667, 0.63667, 0, 0],
+        "8888": [0, 0.54986, 0, 0],
+        "8890": [0.19444, 0.43056, 0, 0],
+        "8891": [0.19444, 0.69224, 0, 0],
+        "8892": [0.19444, 0.69224, 0, 0],
+        "8901": [0, 0.54986, 0, 0],
+        "8903": [0.08167, 0.58167, 0, 0],
+        "8905": [0.08167, 0.58167, 0, 0],
+        "8906": [0.08167, 0.58167, 0, 0],
+        "8907": [0, 0.69224, 0, 0],
+        "8908": [0, 0.69224, 0, 0],
+        "8909": [-0.03598, 0.46402, 0, 0],
+        "8910": [0, 0.54986, 0, 0],
+        "8911": [0, 0.54986, 0, 0],
+        "8912": [0.03517, 0.54986, 0, 0],
+        "8913": [0.03517, 0.54986, 0, 0],
+        "8914": [0, 0.54986, 0, 0],
+        "8915": [0, 0.54986, 0, 0],
+        "8916": [0, 0.69224, 0, 0],
+        "8918": [0.0391, 0.5391, 0, 0],
+        "8919": [0.0391, 0.5391, 0, 0],
+        "8920": [0.03517, 0.54986, 0, 0],
+        "8921": [0.03517, 0.54986, 0, 0],
+        "8922": [0.38569, 0.88569, 0, 0],
+        "8923": [0.38569, 0.88569, 0, 0],
+        "8926": [0.13667, 0.63667, 0, 0],
+        "8927": [0.13667, 0.63667, 0, 0],
+        "8928": [0.30274, 0.79383, 0, 0],
+        "8929": [0.30274, 0.79383, 0, 0],
+        "8934": [0.23222, 0.74111, 0, 0],
+        "8935": [0.23222, 0.74111, 0, 0],
+        "8936": [0.23222, 0.74111, 0, 0],
+        "8937": [0.23222, 0.74111, 0, 0],
+        "8938": [0.20576, 0.70576, 0, 0],
+        "8939": [0.20576, 0.70576, 0, 0],
+        "8940": [0.30274, 0.79383, 0, 0],
+        "8941": [0.30274, 0.79383, 0, 0],
+        "8994": [0.19444, 0.69224, 0, 0],
+        "8995": [0.19444, 0.69224, 0, 0],
+        "9416": [0.15559, 0.69224, 0, 0],
+        "9484": [0, 0.69224, 0, 0],
+        "9488": [0, 0.69224, 0, 0],
+        "9492": [0, 0.37788, 0, 0],
+        "9496": [0, 0.37788, 0, 0],
+        "9585": [0.19444, 0.68889, 0, 0],
+        "9586": [0.19444, 0.74111, 0, 0],
+        "9632": [0, 0.675, 0, 0],
+        "9633": [0, 0.675, 0, 0],
+        "9650": [0, 0.54986, 0, 0],
+        "9651": [0, 0.54986, 0, 0],
+        "9654": [0.03517, 0.54986, 0, 0],
+        "9660": [0, 0.54986, 0, 0],
+        "9661": [0, 0.54986, 0, 0],
+        "9664": [0.03517, 0.54986, 0, 0],
+        "9674": [0.11111, 0.69224, 0, 0],
+        "9733": [0.19444, 0.69224, 0, 0],
+        "10003": [0, 0.69224, 0, 0],
+        "10016": [0, 0.69224, 0, 0],
+        "10731": [0.11111, 0.69224, 0, 0],
+        "10846": [0.19444, 0.75583, 0, 0],
+        "10877": [0.13667, 0.63667, 0, 0],
+        "10878": [0.13667, 0.63667, 0, 0],
+        "10885": [0.25583, 0.75583, 0, 0],
+        "10886": [0.25583, 0.75583, 0, 0],
+        "10887": [0.13597, 0.63597, 0, 0],
+        "10888": [0.13597, 0.63597, 0, 0],
+        "10889": [0.26167, 0.75726, 0, 0],
+        "10890": [0.26167, 0.75726, 0, 0],
+        "10891": [0.48256, 0.98256, 0, 0],
+        "10892": [0.48256, 0.98256, 0, 0],
+        "10901": [0.13667, 0.63667, 0, 0],
+        "10902": [0.13667, 0.63667, 0, 0],
+        "10933": [0.25142, 0.75726, 0, 0],
+        "10934": [0.25142, 0.75726, 0, 0],
+        "10935": [0.26167, 0.75726, 0, 0],
+        "10936": [0.26167, 0.75726, 0, 0],
+        "10937": [0.26167, 0.75726, 0, 0],
+        "10938": [0.26167, 0.75726, 0, 0],
+        "10949": [0.25583, 0.75583, 0, 0],
+        "10950": [0.25583, 0.75583, 0, 0],
+        "10955": [0.28481, 0.79383, 0, 0],
+        "10956": [0.28481, 0.79383, 0, 0],
+        "57350": [0.08167, 0.58167, 0, 0],
+        "57351": [0.08167, 0.58167, 0, 0],
+        "57352": [0.08167, 0.58167, 0, 0],
+        "57353": [0, 0.43056, 0.04028, 0],
+        "57356": [0.25142, 0.75726, 0, 0],
+        "57357": [0.25142, 0.75726, 0, 0],
+        "57358": [0.41951, 0.91951, 0, 0],
+        "57359": [0.30274, 0.79383, 0, 0],
+        "57360": [0.30274, 0.79383, 0, 0],
+        "57361": [0.41951, 0.91951, 0, 0],
+        "57366": [0.25142, 0.75726, 0, 0],
+        "57367": [0.25142, 0.75726, 0, 0],
+        "57368": [0.25142, 0.75726, 0, 0],
+        "57369": [0.25142, 0.75726, 0, 0],
+        "57370": [0.13597, 0.63597, 0, 0],
+        "57371": [0.13597, 0.63597, 0, 0],
+    },
+    "Caligraphic-Regular": {
+        "48": [0, 0.43056, 0, 0],
+        "49": [0, 0.43056, 0, 0],
+        "50": [0, 0.43056, 0, 0],
+        "51": [0.19444, 0.43056, 0, 0],
+        "52": [0.19444, 0.43056, 0, 0],
+        "53": [0.19444, 0.43056, 0, 0],
+        "54": [0, 0.64444, 0, 0],
+        "55": [0.19444, 0.43056, 0, 0],
+        "56": [0, 0.64444, 0, 0],
+        "57": [0.19444, 0.43056, 0, 0],
+        "65": [0, 0.68333, 0, 0.19445],
+        "66": [0, 0.68333, 0.03041, 0.13889],
+        "67": [0, 0.68333, 0.05834, 0.13889],
+        "68": [0, 0.68333, 0.02778, 0.08334],
+        "69": [0, 0.68333, 0.08944, 0.11111],
+        "70": [0, 0.68333, 0.09931, 0.11111],
+        "71": [0.09722, 0.68333, 0.0593, 0.11111],
+        "72": [0, 0.68333, 0.00965, 0.11111],
+        "73": [0, 0.68333, 0.07382, 0],
+        "74": [0.09722, 0.68333, 0.18472, 0.16667],
+        "75": [0, 0.68333, 0.01445, 0.05556],
+        "76": [0, 0.68333, 0, 0.13889],
+        "77": [0, 0.68333, 0, 0.13889],
+        "78": [0, 0.68333, 0.14736, 0.08334],
+        "79": [0, 0.68333, 0.02778, 0.11111],
+        "80": [0, 0.68333, 0.08222, 0.08334],
+        "81": [0.09722, 0.68333, 0, 0.11111],
+        "82": [0, 0.68333, 0, 0.08334],
+        "83": [0, 0.68333, 0.075, 0.13889],
+        "84": [0, 0.68333, 0.25417, 0],
+        "85": [0, 0.68333, 0.09931, 0.08334],
+        "86": [0, 0.68333, 0.08222, 0],
+        "87": [0, 0.68333, 0.08222, 0.08334],
+        "88": [0, 0.68333, 0.14643, 0.13889],
+        "89": [0.09722, 0.68333, 0.08222, 0.08334],
+        "90": [0, 0.68333, 0.07944, 0.13889],
+    },
+    "Fraktur-Regular": {
+        "33": [0, 0.69141, 0, 0],
+        "34": [0, 0.69141, 0, 0],
+        "38": [0, 0.69141, 0, 0],
+        "39": [0, 0.69141, 0, 0],
+        "40": [0.24982, 0.74947, 0, 0],
+        "41": [0.24982, 0.74947, 0, 0],
+        "42": [0, 0.62119, 0, 0],
+        "43": [0.08319, 0.58283, 0, 0],
+        "44": [0, 0.10803, 0, 0],
+        "45": [0.08319, 0.58283, 0, 0],
+        "46": [0, 0.10803, 0, 0],
+        "47": [0.24982, 0.74947, 0, 0],
+        "48": [0, 0.47534, 0, 0],
+        "49": [0, 0.47534, 0, 0],
+        "50": [0, 0.47534, 0, 0],
+        "51": [0.18906, 0.47534, 0, 0],
+        "52": [0.18906, 0.47534, 0, 0],
+        "53": [0.18906, 0.47534, 0, 0],
+        "54": [0, 0.69141, 0, 0],
+        "55": [0.18906, 0.47534, 0, 0],
+        "56": [0, 0.69141, 0, 0],
+        "57": [0.18906, 0.47534, 0, 0],
+        "58": [0, 0.47534, 0, 0],
+        "59": [0.12604, 0.47534, 0, 0],
+        "61": [-0.13099, 0.36866, 0, 0],
+        "63": [0, 0.69141, 0, 0],
+        "65": [0, 0.69141, 0, 0],
+        "66": [0, 0.69141, 0, 0],
+        "67": [0, 0.69141, 0, 0],
+        "68": [0, 0.69141, 0, 0],
+        "69": [0, 0.69141, 0, 0],
+        "70": [0.12604, 0.69141, 0, 0],
+        "71": [0, 0.69141, 0, 0],
+        "72": [0.06302, 0.69141, 0, 0],
+        "73": [0, 0.69141, 0, 0],
+        "74": [0.12604, 0.69141, 0, 0],
+        "75": [0, 0.69141, 0, 0],
+        "76": [0, 0.69141, 0, 0],
+        "77": [0, 0.69141, 0, 0],
+        "78": [0, 0.69141, 0, 0],
+        "79": [0, 0.69141, 0, 0],
+        "80": [0.18906, 0.69141, 0, 0],
+        "81": [0.03781, 0.69141, 0, 0],
+        "82": [0, 0.69141, 0, 0],
+        "83": [0, 0.69141, 0, 0],
+        "84": [0, 0.69141, 0, 0],
+        "85": [0, 0.69141, 0, 0],
+        "86": [0, 0.69141, 0, 0],
+        "87": [0, 0.69141, 0, 0],
+        "88": [0, 0.69141, 0, 0],
+        "89": [0.18906, 0.69141, 0, 0],
+        "90": [0.12604, 0.69141, 0, 0],
+        "91": [0.24982, 0.74947, 0, 0],
+        "93": [0.24982, 0.74947, 0, 0],
+        "94": [0, 0.69141, 0, 0],
+        "97": [0, 0.47534, 0, 0],
+        "98": [0, 0.69141, 0, 0],
+        "99": [0, 0.47534, 0, 0],
+        "100": [0, 0.62119, 0, 0],
+        "101": [0, 0.47534, 0, 0],
+        "102": [0.18906, 0.69141, 0, 0],
+        "103": [0.18906, 0.47534, 0, 0],
+        "104": [0.18906, 0.69141, 0, 0],
+        "105": [0, 0.69141, 0, 0],
+        "106": [0, 0.69141, 0, 0],
+        "107": [0, 0.69141, 0, 0],
+        "108": [0, 0.69141, 0, 0],
+        "109": [0, 0.47534, 0, 0],
+        "110": [0, 0.47534, 0, 0],
+        "111": [0, 0.47534, 0, 0],
+        "112": [0.18906, 0.52396, 0, 0],
+        "113": [0.18906, 0.47534, 0, 0],
+        "114": [0, 0.47534, 0, 0],
+        "115": [0, 0.47534, 0, 0],
+        "116": [0, 0.62119, 0, 0],
+        "117": [0, 0.47534, 0, 0],
+        "118": [0, 0.52396, 0, 0],
+        "119": [0, 0.52396, 0, 0],
+        "120": [0.18906, 0.47534, 0, 0],
+        "121": [0.18906, 0.47534, 0, 0],
+        "122": [0.18906, 0.47534, 0, 0],
+        "8216": [0, 0.69141, 0, 0],
+        "8217": [0, 0.69141, 0, 0],
+        "58112": [0, 0.62119, 0, 0],
+        "58113": [0, 0.62119, 0, 0],
+        "58114": [0.18906, 0.69141, 0, 0],
+        "58115": [0.18906, 0.69141, 0, 0],
+        "58116": [0.18906, 0.47534, 0, 0],
+        "58117": [0, 0.69141, 0, 0],
+        "58118": [0, 0.62119, 0, 0],
+        "58119": [0, 0.47534, 0, 0],
+    },
+    "Main-Bold": {
+        "33": [0, 0.69444, 0, 0],
+        "34": [0, 0.69444, 0, 0],
+        "35": [0.19444, 0.69444, 0, 0],
+        "36": [0.05556, 0.75, 0, 0],
+        "37": [0.05556, 0.75, 0, 0],
+        "38": [0, 0.69444, 0, 0],
+        "39": [0, 0.69444, 0, 0],
+        "40": [0.25, 0.75, 0, 0],
+        "41": [0.25, 0.75, 0, 0],
+        "42": [0, 0.75, 0, 0],
+        "43": [0.13333, 0.63333, 0, 0],
+        "44": [0.19444, 0.15556, 0, 0],
+        "45": [0, 0.44444, 0, 0],
+        "46": [0, 0.15556, 0, 0],
+        "47": [0.25, 0.75, 0, 0],
+        "48": [0, 0.64444, 0, 0],
+        "49": [0, 0.64444, 0, 0],
+        "50": [0, 0.64444, 0, 0],
+        "51": [0, 0.64444, 0, 0],
+        "52": [0, 0.64444, 0, 0],
+        "53": [0, 0.64444, 0, 0],
+        "54": [0, 0.64444, 0, 0],
+        "55": [0, 0.64444, 0, 0],
+        "56": [0, 0.64444, 0, 0],
+        "57": [0, 0.64444, 0, 0],
+        "58": [0, 0.44444, 0, 0],
+        "59": [0.19444, 0.44444, 0, 0],
+        "60": [0.08556, 0.58556, 0, 0],
+        "61": [-0.10889, 0.39111, 0, 0],
+        "62": [0.08556, 0.58556, 0, 0],
+        "63": [0, 0.69444, 0, 0],
+        "64": [0, 0.69444, 0, 0],
+        "65": [0, 0.68611, 0, 0],
+        "66": [0, 0.68611, 0, 0],
+        "67": [0, 0.68611, 0, 0],
+        "68": [0, 0.68611, 0, 0],
+        "69": [0, 0.68611, 0, 0],
+        "70": [0, 0.68611, 0, 0],
+        "71": [0, 0.68611, 0, 0],
+        "72": [0, 0.68611, 0, 0],
+        "73": [0, 0.68611, 0, 0],
+        "74": [0, 0.68611, 0, 0],
+        "75": [0, 0.68611, 0, 0],
+        "76": [0, 0.68611, 0, 0],
+        "77": [0, 0.68611, 0, 0],
+        "78": [0, 0.68611, 0, 0],
+        "79": [0, 0.68611, 0, 0],
+        "80": [0, 0.68611, 0, 0],
+        "81": [0.19444, 0.68611, 0, 0],
+        "82": [0, 0.68611, 0, 0],
+        "83": [0, 0.68611, 0, 0],
+        "84": [0, 0.68611, 0, 0],
+        "85": [0, 0.68611, 0, 0],
+        "86": [0, 0.68611, 0.01597, 0],
+        "87": [0, 0.68611, 0.01597, 0],
+        "88": [0, 0.68611, 0, 0],
+        "89": [0, 0.68611, 0.02875, 0],
+        "90": [0, 0.68611, 0, 0],
+        "91": [0.25, 0.75, 0, 0],
+        "92": [0.25, 0.75, 0, 0],
+        "93": [0.25, 0.75, 0, 0],
+        "94": [0, 0.69444, 0, 0],
+        "95": [0.31, 0.13444, 0.03194, 0],
+        "96": [0, 0.69444, 0, 0],
+        "97": [0, 0.44444, 0, 0],
+        "98": [0, 0.69444, 0, 0],
+        "99": [0, 0.44444, 0, 0],
+        "100": [0, 0.69444, 0, 0],
+        "101": [0, 0.44444, 0, 0],
+        "102": [0, 0.69444, 0.10903, 0],
+        "103": [0.19444, 0.44444, 0.01597, 0],
+        "104": [0, 0.69444, 0, 0],
+        "105": [0, 0.69444, 0, 0],
+        "106": [0.19444, 0.69444, 0, 0],
+        "107": [0, 0.69444, 0, 0],
+        "108": [0, 0.69444, 0, 0],
+        "109": [0, 0.44444, 0, 0],
+        "110": [0, 0.44444, 0, 0],
+        "111": [0, 0.44444, 0, 0],
+        "112": [0.19444, 0.44444, 0, 0],
+        "113": [0.19444, 0.44444, 0, 0],
+        "114": [0, 0.44444, 0, 0],
+        "115": [0, 0.44444, 0, 0],
+        "116": [0, 0.63492, 0, 0],
+        "117": [0, 0.44444, 0, 0],
+        "118": [0, 0.44444, 0.01597, 0],
+        "119": [0, 0.44444, 0.01597, 0],
+        "120": [0, 0.44444, 0, 0],
+        "121": [0.19444, 0.44444, 0.01597, 0],
+        "122": [0, 0.44444, 0, 0],
+        "123": [0.25, 0.75, 0, 0],
+        "124": [0.25, 0.75, 0, 0],
+        "125": [0.25, 0.75, 0, 0],
+        "126": [0.35, 0.34444, 0, 0],
+        "168": [0, 0.69444, 0, 0],
+        "172": [0, 0.44444, 0, 0],
+        "175": [0, 0.59611, 0, 0],
+        "176": [0, 0.69444, 0, 0],
+        "177": [0.13333, 0.63333, 0, 0],
+        "180": [0, 0.69444, 0, 0],
+        "215": [0.13333, 0.63333, 0, 0],
+        "247": [0.13333, 0.63333, 0, 0],
+        "305": [0, 0.44444, 0, 0],
+        "567": [0.19444, 0.44444, 0, 0],
+        "710": [0, 0.69444, 0, 0],
+        "711": [0, 0.63194, 0, 0],
+        "713": [0, 0.59611, 0, 0],
+        "714": [0, 0.69444, 0, 0],
+        "715": [0, 0.69444, 0, 0],
+        "728": [0, 0.69444, 0, 0],
+        "729": [0, 0.69444, 0, 0],
+        "730": [0, 0.69444, 0, 0],
+        "732": [0, 0.69444, 0, 0],
+        "768": [0, 0.69444, 0, 0],
+        "769": [0, 0.69444, 0, 0],
+        "770": [0, 0.69444, 0, 0],
+        "771": [0, 0.69444, 0, 0],
+        "772": [0, 0.59611, 0, 0],
+        "774": [0, 0.69444, 0, 0],
+        "775": [0, 0.69444, 0, 0],
+        "776": [0, 0.69444, 0, 0],
+        "778": [0, 0.69444, 0, 0],
+        "779": [0, 0.69444, 0, 0],
+        "780": [0, 0.63194, 0, 0],
+        "824": [0.19444, 0.69444, 0, 0],
+        "915": [0, 0.68611, 0, 0],
+        "916": [0, 0.68611, 0, 0],
+        "920": [0, 0.68611, 0, 0],
+        "923": [0, 0.68611, 0, 0],
+        "926": [0, 0.68611, 0, 0],
+        "928": [0, 0.68611, 0, 0],
+        "931": [0, 0.68611, 0, 0],
+        "933": [0, 0.68611, 0, 0],
+        "934": [0, 0.68611, 0, 0],
+        "936": [0, 0.68611, 0, 0],
+        "937": [0, 0.68611, 0, 0],
+        "8211": [0, 0.44444, 0.03194, 0],
+        "8212": [0, 0.44444, 0.03194, 0],
+        "8216": [0, 0.69444, 0, 0],
+        "8217": [0, 0.69444, 0, 0],
+        "8220": [0, 0.69444, 0, 0],
+        "8221": [0, 0.69444, 0, 0],
+        "8224": [0.19444, 0.69444, 0, 0],
+        "8225": [0.19444, 0.69444, 0, 0],
+        "8242": [0, 0.55556, 0, 0],
+        "8407": [0, 0.72444, 0.15486, 0],
+        "8463": [0, 0.69444, 0, 0],
+        "8465": [0, 0.69444, 0, 0],
+        "8467": [0, 0.69444, 0, 0],
+        "8472": [0.19444, 0.44444, 0, 0],
+        "8476": [0, 0.69444, 0, 0],
+        "8501": [0, 0.69444, 0, 0],
+        "8592": [-0.10889, 0.39111, 0, 0],
+        "8593": [0.19444, 0.69444, 0, 0],
+        "8594": [-0.10889, 0.39111, 0, 0],
+        "8595": [0.19444, 0.69444, 0, 0],
+        "8596": [-0.10889, 0.39111, 0, 0],
+        "8597": [0.25, 0.75, 0, 0],
+        "8598": [0.19444, 0.69444, 0, 0],
+        "8599": [0.19444, 0.69444, 0, 0],
+        "8600": [0.19444, 0.69444, 0, 0],
+        "8601": [0.19444, 0.69444, 0, 0],
+        "8636": [-0.10889, 0.39111, 0, 0],
+        "8637": [-0.10889, 0.39111, 0, 0],
+        "8640": [-0.10889, 0.39111, 0, 0],
+        "8641": [-0.10889, 0.39111, 0, 0],
+        "8656": [-0.10889, 0.39111, 0, 0],
+        "8657": [0.19444, 0.69444, 0, 0],
+        "8658": [-0.10889, 0.39111, 0, 0],
+        "8659": [0.19444, 0.69444, 0, 0],
+        "8660": [-0.10889, 0.39111, 0, 0],
+        "8661": [0.25, 0.75, 0, 0],
+        "8704": [0, 0.69444, 0, 0],
+        "8706": [0, 0.69444, 0.06389, 0],
+        "8707": [0, 0.69444, 0, 0],
+        "8709": [0.05556, 0.75, 0, 0],
+        "8711": [0, 0.68611, 0, 0],
+        "8712": [0.08556, 0.58556, 0, 0],
+        "8715": [0.08556, 0.58556, 0, 0],
+        "8722": [0.13333, 0.63333, 0, 0],
+        "8723": [0.13333, 0.63333, 0, 0],
+        "8725": [0.25, 0.75, 0, 0],
+        "8726": [0.25, 0.75, 0, 0],
+        "8727": [-0.02778, 0.47222, 0, 0],
+        "8728": [-0.02639, 0.47361, 0, 0],
+        "8729": [-0.02639, 0.47361, 0, 0],
+        "8730": [0.18, 0.82, 0, 0],
+        "8733": [0, 0.44444, 0, 0],
+        "8734": [0, 0.44444, 0, 0],
+        "8736": [0, 0.69224, 0, 0],
+        "8739": [0.25, 0.75, 0, 0],
+        "8741": [0.25, 0.75, 0, 0],
+        "8743": [0, 0.55556, 0, 0],
+        "8744": [0, 0.55556, 0, 0],
+        "8745": [0, 0.55556, 0, 0],
+        "8746": [0, 0.55556, 0, 0],
+        "8747": [0.19444, 0.69444, 0.12778, 0],
+        "8764": [-0.10889, 0.39111, 0, 0],
+        "8768": [0.19444, 0.69444, 0, 0],
+        "8771": [0.00222, 0.50222, 0, 0],
+        "8776": [0.02444, 0.52444, 0, 0],
+        "8781": [0.00222, 0.50222, 0, 0],
+        "8801": [0.00222, 0.50222, 0, 0],
+        "8804": [0.19667, 0.69667, 0, 0],
+        "8805": [0.19667, 0.69667, 0, 0],
+        "8810": [0.08556, 0.58556, 0, 0],
+        "8811": [0.08556, 0.58556, 0, 0],
+        "8826": [0.08556, 0.58556, 0, 0],
+        "8827": [0.08556, 0.58556, 0, 0],
+        "8834": [0.08556, 0.58556, 0, 0],
+        "8835": [0.08556, 0.58556, 0, 0],
+        "8838": [0.19667, 0.69667, 0, 0],
+        "8839": [0.19667, 0.69667, 0, 0],
+        "8846": [0, 0.55556, 0, 0],
+        "8849": [0.19667, 0.69667, 0, 0],
+        "8850": [0.19667, 0.69667, 0, 0],
+        "8851": [0, 0.55556, 0, 0],
+        "8852": [0, 0.55556, 0, 0],
+        "8853": [0.13333, 0.63333, 0, 0],
+        "8854": [0.13333, 0.63333, 0, 0],
+        "8855": [0.13333, 0.63333, 0, 0],
+        "8856": [0.13333, 0.63333, 0, 0],
+        "8857": [0.13333, 0.63333, 0, 0],
+        "8866": [0, 0.69444, 0, 0],
+        "8867": [0, 0.69444, 0, 0],
+        "8868": [0, 0.69444, 0, 0],
+        "8869": [0, 0.69444, 0, 0],
+        "8900": [-0.02639, 0.47361, 0, 0],
+        "8901": [-0.02639, 0.47361, 0, 0],
+        "8902": [-0.02778, 0.47222, 0, 0],
+        "8968": [0.25, 0.75, 0, 0],
+        "8969": [0.25, 0.75, 0, 0],
+        "8970": [0.25, 0.75, 0, 0],
+        "8971": [0.25, 0.75, 0, 0],
+        "8994": [-0.13889, 0.36111, 0, 0],
+        "8995": [-0.13889, 0.36111, 0, 0],
+        "9651": [0.19444, 0.69444, 0, 0],
+        "9657": [-0.02778, 0.47222, 0, 0],
+        "9661": [0.19444, 0.69444, 0, 0],
+        "9667": [-0.02778, 0.47222, 0, 0],
+        "9711": [0.19444, 0.69444, 0, 0],
+        "9824": [0.12963, 0.69444, 0, 0],
+        "9825": [0.12963, 0.69444, 0, 0],
+        "9826": [0.12963, 0.69444, 0, 0],
+        "9827": [0.12963, 0.69444, 0, 0],
+        "9837": [0, 0.75, 0, 0],
+        "9838": [0.19444, 0.69444, 0, 0],
+        "9839": [0.19444, 0.69444, 0, 0],
+        "10216": [0.25, 0.75, 0, 0],
+        "10217": [0.25, 0.75, 0, 0],
+        "10815": [0, 0.68611, 0, 0],
+        "10927": [0.19667, 0.69667, 0, 0],
+        "10928": [0.19667, 0.69667, 0, 0],
+    },
+    "Main-Italic": {
+        "33": [0, 0.69444, 0.12417, 0],
+        "34": [0, 0.69444, 0.06961, 0],
+        "35": [0.19444, 0.69444, 0.06616, 0],
+        "37": [0.05556, 0.75, 0.13639, 0],
+        "38": [0, 0.69444, 0.09694, 0],
+        "39": [0, 0.69444, 0.12417, 0],
+        "40": [0.25, 0.75, 0.16194, 0],
+        "41": [0.25, 0.75, 0.03694, 0],
+        "42": [0, 0.75, 0.14917, 0],
+        "43": [0.05667, 0.56167, 0.03694, 0],
+        "44": [0.19444, 0.10556, 0, 0],
+        "45": [0, 0.43056, 0.02826, 0],
+        "46": [0, 0.10556, 0, 0],
+        "47": [0.25, 0.75, 0.16194, 0],
+        "48": [0, 0.64444, 0.13556, 0],
+        "49": [0, 0.64444, 0.13556, 0],
+        "50": [0, 0.64444, 0.13556, 0],
+        "51": [0, 0.64444, 0.13556, 0],
+        "52": [0.19444, 0.64444, 0.13556, 0],
+        "53": [0, 0.64444, 0.13556, 0],
+        "54": [0, 0.64444, 0.13556, 0],
+        "55": [0.19444, 0.64444, 0.13556, 0],
+        "56": [0, 0.64444, 0.13556, 0],
+        "57": [0, 0.64444, 0.13556, 0],
+        "58": [0, 0.43056, 0.0582, 0],
+        "59": [0.19444, 0.43056, 0.0582, 0],
+        "61": [-0.13313, 0.36687, 0.06616, 0],
+        "63": [0, 0.69444, 0.1225, 0],
+        "64": [0, 0.69444, 0.09597, 0],
+        "65": [0, 0.68333, 0, 0],
+        "66": [0, 0.68333, 0.10257, 0],
+        "67": [0, 0.68333, 0.14528, 0],
+        "68": [0, 0.68333, 0.09403, 0],
+        "69": [0, 0.68333, 0.12028, 0],
+        "70": [0, 0.68333, 0.13305, 0],
+        "71": [0, 0.68333, 0.08722, 0],
+        "72": [0, 0.68333, 0.16389, 0],
+        "73": [0, 0.68333, 0.15806, 0],
+        "74": [0, 0.68333, 0.14028, 0],
+        "75": [0, 0.68333, 0.14528, 0],
+        "76": [0, 0.68333, 0, 0],
+        "77": [0, 0.68333, 0.16389, 0],
+        "78": [0, 0.68333, 0.16389, 0],
+        "79": [0, 0.68333, 0.09403, 0],
+        "80": [0, 0.68333, 0.10257, 0],
+        "81": [0.19444, 0.68333, 0.09403, 0],
+        "82": [0, 0.68333, 0.03868, 0],
+        "83": [0, 0.68333, 0.11972, 0],
+        "84": [0, 0.68333, 0.13305, 0],
+        "85": [0, 0.68333, 0.16389, 0],
+        "86": [0, 0.68333, 0.18361, 0],
+        "87": [0, 0.68333, 0.18361, 0],
+        "88": [0, 0.68333, 0.15806, 0],
+        "89": [0, 0.68333, 0.19383, 0],
+        "90": [0, 0.68333, 0.14528, 0],
+        "91": [0.25, 0.75, 0.1875, 0],
+        "93": [0.25, 0.75, 0.10528, 0],
+        "94": [0, 0.69444, 0.06646, 0],
+        "95": [0.31, 0.12056, 0.09208, 0],
+        "97": [0, 0.43056, 0.07671, 0],
+        "98": [0, 0.69444, 0.06312, 0],
+        "99": [0, 0.43056, 0.05653, 0],
+        "100": [0, 0.69444, 0.10333, 0],
+        "101": [0, 0.43056, 0.07514, 0],
+        "102": [0.19444, 0.69444, 0.21194, 0],
+        "103": [0.19444, 0.43056, 0.08847, 0],
+        "104": [0, 0.69444, 0.07671, 0],
+        "105": [0, 0.65536, 0.1019, 0],
+        "106": [0.19444, 0.65536, 0.14467, 0],
+        "107": [0, 0.69444, 0.10764, 0],
+        "108": [0, 0.69444, 0.10333, 0],
+        "109": [0, 0.43056, 0.07671, 0],
+        "110": [0, 0.43056, 0.07671, 0],
+        "111": [0, 0.43056, 0.06312, 0],
+        "112": [0.19444, 0.43056, 0.06312, 0],
+        "113": [0.19444, 0.43056, 0.08847, 0],
+        "114": [0, 0.43056, 0.10764, 0],
+        "115": [0, 0.43056, 0.08208, 0],
+        "116": [0, 0.61508, 0.09486, 0],
+        "117": [0, 0.43056, 0.07671, 0],
+        "118": [0, 0.43056, 0.10764, 0],
+        "119": [0, 0.43056, 0.10764, 0],
+        "120": [0, 0.43056, 0.12042, 0],
+        "121": [0.19444, 0.43056, 0.08847, 0],
+        "122": [0, 0.43056, 0.12292, 0],
+        "126": [0.35, 0.31786, 0.11585, 0],
+        "163": [0, 0.69444, 0, 0],
+        "305": [0, 0.43056, 0, 0.02778],
+        "567": [0.19444, 0.43056, 0, 0.08334],
+        "768": [0, 0.69444, 0, 0],
+        "769": [0, 0.69444, 0.09694, 0],
+        "770": [0, 0.69444, 0.06646, 0],
+        "771": [0, 0.66786, 0.11585, 0],
+        "772": [0, 0.56167, 0.10333, 0],
+        "774": [0, 0.69444, 0.10806, 0],
+        "775": [0, 0.66786, 0.11752, 0],
+        "776": [0, 0.66786, 0.10474, 0],
+        "778": [0, 0.69444, 0, 0],
+        "779": [0, 0.69444, 0.1225, 0],
+        "780": [0, 0.62847, 0.08295, 0],
+        "915": [0, 0.68333, 0.13305, 0],
+        "916": [0, 0.68333, 0, 0],
+        "920": [0, 0.68333, 0.09403, 0],
+        "923": [0, 0.68333, 0, 0],
+        "926": [0, 0.68333, 0.15294, 0],
+        "928": [0, 0.68333, 0.16389, 0],
+        "931": [0, 0.68333, 0.12028, 0],
+        "933": [0, 0.68333, 0.11111, 0],
+        "934": [0, 0.68333, 0.05986, 0],
+        "936": [0, 0.68333, 0.11111, 0],
+        "937": [0, 0.68333, 0.10257, 0],
+        "8211": [0, 0.43056, 0.09208, 0],
+        "8212": [0, 0.43056, 0.09208, 0],
+        "8216": [0, 0.69444, 0.12417, 0],
+        "8217": [0, 0.69444, 0.12417, 0],
+        "8220": [0, 0.69444, 0.1685, 0],
+        "8221": [0, 0.69444, 0.06961, 0],
+        "8463": [0, 0.68889, 0, 0],
+    },
+    "Main-Regular": {
+        "32": [0, 0, 0, 0],
+        "33": [0, 0.69444, 0, 0],
+        "34": [0, 0.69444, 0, 0],
+        "35": [0.19444, 0.69444, 0, 0],
+        "36": [0.05556, 0.75, 0, 0],
+        "37": [0.05556, 0.75, 0, 0],
+        "38": [0, 0.69444, 0, 0],
+        "39": [0, 0.69444, 0, 0],
+        "40": [0.25, 0.75, 0, 0],
+        "41": [0.25, 0.75, 0, 0],
+        "42": [0, 0.75, 0, 0],
+        "43": [0.08333, 0.58333, 0, 0],
+        "44": [0.19444, 0.10556, 0, 0],
+        "45": [0, 0.43056, 0, 0],
+        "46": [0, 0.10556, 0, 0],
+        "47": [0.25, 0.75, 0, 0],
+        "48": [0, 0.64444, 0, 0],
+        "49": [0, 0.64444, 0, 0],
+        "50": [0, 0.64444, 0, 0],
+        "51": [0, 0.64444, 0, 0],
+        "52": [0, 0.64444, 0, 0],
+        "53": [0, 0.64444, 0, 0],
+        "54": [0, 0.64444, 0, 0],
+        "55": [0, 0.64444, 0, 0],
+        "56": [0, 0.64444, 0, 0],
+        "57": [0, 0.64444, 0, 0],
+        "58": [0, 0.43056, 0, 0],
+        "59": [0.19444, 0.43056, 0, 0],
+        "60": [0.0391, 0.5391, 0, 0],
+        "61": [-0.13313, 0.36687, 0, 0],
+        "62": [0.0391, 0.5391, 0, 0],
+        "63": [0, 0.69444, 0, 0],
+        "64": [0, 0.69444, 0, 0],
+        "65": [0, 0.68333, 0, 0],
+        "66": [0, 0.68333, 0, 0],
+        "67": [0, 0.68333, 0, 0],
+        "68": [0, 0.68333, 0, 0],
+        "69": [0, 0.68333, 0, 0],
+        "70": [0, 0.68333, 0, 0],
+        "71": [0, 0.68333, 0, 0],
+        "72": [0, 0.68333, 0, 0],
+        "73": [0, 0.68333, 0, 0],
+        "74": [0, 0.68333, 0, 0],
+        "75": [0, 0.68333, 0, 0],
+        "76": [0, 0.68333, 0, 0],
+        "77": [0, 0.68333, 0, 0],
+        "78": [0, 0.68333, 0, 0],
+        "79": [0, 0.68333, 0, 0],
+        "80": [0, 0.68333, 0, 0],
+        "81": [0.19444, 0.68333, 0, 0],
+        "82": [0, 0.68333, 0, 0],
+        "83": [0, 0.68333, 0, 0],
+        "84": [0, 0.68333, 0, 0],
+        "85": [0, 0.68333, 0, 0],
+        "86": [0, 0.68333, 0.01389, 0],
+        "87": [0, 0.68333, 0.01389, 0],
+        "88": [0, 0.68333, 0, 0],
+        "89": [0, 0.68333, 0.025, 0],
+        "90": [0, 0.68333, 0, 0],
+        "91": [0.25, 0.75, 0, 0],
+        "92": [0.25, 0.75, 0, 0],
+        "93": [0.25, 0.75, 0, 0],
+        "94": [0, 0.69444, 0, 0],
+        "95": [0.31, 0.12056, 0.02778, 0],
+        "96": [0, 0.69444, 0, 0],
+        "97": [0, 0.43056, 0, 0],
+        "98": [0, 0.69444, 0, 0],
+        "99": [0, 0.43056, 0, 0],
+        "100": [0, 0.69444, 0, 0],
+        "101": [0, 0.43056, 0, 0],
+        "102": [0, 0.69444, 0.07778, 0],
+        "103": [0.19444, 0.43056, 0.01389, 0],
+        "104": [0, 0.69444, 0, 0],
+        "105": [0, 0.66786, 0, 0],
+        "106": [0.19444, 0.66786, 0, 0],
+        "107": [0, 0.69444, 0, 0],
+        "108": [0, 0.69444, 0, 0],
+        "109": [0, 0.43056, 0, 0],
+        "110": [0, 0.43056, 0, 0],
+        "111": [0, 0.43056, 0, 0],
+        "112": [0.19444, 0.43056, 0, 0],
+        "113": [0.19444, 0.43056, 0, 0],
+        "114": [0, 0.43056, 0, 0],
+        "115": [0, 0.43056, 0, 0],
+        "116": [0, 0.61508, 0, 0],
+        "117": [0, 0.43056, 0, 0],
+        "118": [0, 0.43056, 0.01389, 0],
+        "119": [0, 0.43056, 0.01389, 0],
+        "120": [0, 0.43056, 0, 0],
+        "121": [0.19444, 0.43056, 0.01389, 0],
+        "122": [0, 0.43056, 0, 0],
+        "123": [0.25, 0.75, 0, 0],
+        "124": [0.25, 0.75, 0, 0],
+        "125": [0.25, 0.75, 0, 0],
+        "126": [0.35, 0.31786, 0, 0],
+        "160": [0, 0, 0, 0],
+        "168": [0, 0.66786, 0, 0],
+        "172": [0, 0.43056, 0, 0],
+        "175": [0, 0.56778, 0, 0],
+        "176": [0, 0.69444, 0, 0],
+        "177": [0.08333, 0.58333, 0, 0],
+        "180": [0, 0.69444, 0, 0],
+        "215": [0.08333, 0.58333, 0, 0],
+        "247": [0.08333, 0.58333, 0, 0],
+        "305": [0, 0.43056, 0, 0],
+        "567": [0.19444, 0.43056, 0, 0],
+        "710": [0, 0.69444, 0, 0],
+        "711": [0, 0.62847, 0, 0],
+        "713": [0, 0.56778, 0, 0],
+        "714": [0, 0.69444, 0, 0],
+        "715": [0, 0.69444, 0, 0],
+        "728": [0, 0.69444, 0, 0],
+        "729": [0, 0.66786, 0, 0],
+        "730": [0, 0.69444, 0, 0],
+        "732": [0, 0.66786, 0, 0],
+        "768": [0, 0.69444, 0, 0],
+        "769": [0, 0.69444, 0, 0],
+        "770": [0, 0.69444, 0, 0],
+        "771": [0, 0.66786, 0, 0],
+        "772": [0, 0.56778, 0, 0],
+        "774": [0, 0.69444, 0, 0],
+        "775": [0, 0.66786, 0, 0],
+        "776": [0, 0.66786, 0, 0],
+        "778": [0, 0.69444, 0, 0],
+        "779": [0, 0.69444, 0, 0],
+        "780": [0, 0.62847, 0, 0],
+        "824": [0.19444, 0.69444, 0, 0],
+        "915": [0, 0.68333, 0, 0],
+        "916": [0, 0.68333, 0, 0],
+        "920": [0, 0.68333, 0, 0],
+        "923": [0, 0.68333, 0, 0],
+        "926": [0, 0.68333, 0, 0],
+        "928": [0, 0.68333, 0, 0],
+        "931": [0, 0.68333, 0, 0],
+        "933": [0, 0.68333, 0, 0],
+        "934": [0, 0.68333, 0, 0],
+        "936": [0, 0.68333, 0, 0],
+        "937": [0, 0.68333, 0, 0],
+        "8211": [0, 0.43056, 0.02778, 0],
+        "8212": [0, 0.43056, 0.02778, 0],
+        "8216": [0, 0.69444, 0, 0],
+        "8217": [0, 0.69444, 0, 0],
+        "8220": [0, 0.69444, 0, 0],
+        "8221": [0, 0.69444, 0, 0],
+        "8224": [0.19444, 0.69444, 0, 0],
+        "8225": [0.19444, 0.69444, 0, 0],
+        "8230": [0, 0.12, 0, 0],
+        "8242": [0, 0.55556, 0, 0],
+        "8407": [0, 0.71444, 0.15382, 0],
+        "8463": [0, 0.68889, 0, 0],
+        "8465": [0, 0.69444, 0, 0],
+        "8467": [0, 0.69444, 0, 0.11111],
+        "8472": [0.19444, 0.43056, 0, 0.11111],
+        "8476": [0, 0.69444, 0, 0],
+        "8501": [0, 0.69444, 0, 0],
+        "8592": [-0.13313, 0.36687, 0, 0],
+        "8593": [0.19444, 0.69444, 0, 0],
+        "8594": [-0.13313, 0.36687, 0, 0],
+        "8595": [0.19444, 0.69444, 0, 0],
+        "8596": [-0.13313, 0.36687, 0, 0],
+        "8597": [0.25, 0.75, 0, 0],
+        "8598": [0.19444, 0.69444, 0, 0],
+        "8599": [0.19444, 0.69444, 0, 0],
+        "8600": [0.19444, 0.69444, 0, 0],
+        "8601": [0.19444, 0.69444, 0, 0],
+        "8614": [0.011, 0.511, 0, 0],
+        "8617": [0.011, 0.511, 0, 0],
+        "8618": [0.011, 0.511, 0, 0],
+        "8636": [-0.13313, 0.36687, 0, 0],
+        "8637": [-0.13313, 0.36687, 0, 0],
+        "8640": [-0.13313, 0.36687, 0, 0],
+        "8641": [-0.13313, 0.36687, 0, 0],
+        "8652": [0.011, 0.671, 0, 0],
+        "8656": [-0.13313, 0.36687, 0, 0],
+        "8657": [0.19444, 0.69444, 0, 0],
+        "8658": [-0.13313, 0.36687, 0, 0],
+        "8659": [0.19444, 0.69444, 0, 0],
+        "8660": [-0.13313, 0.36687, 0, 0],
+        "8661": [0.25, 0.75, 0, 0],
+        "8704": [0, 0.69444, 0, 0],
+        "8706": [0, 0.69444, 0.05556, 0.08334],
+        "8707": [0, 0.69444, 0, 0],
+        "8709": [0.05556, 0.75, 0, 0],
+        "8711": [0, 0.68333, 0, 0],
+        "8712": [0.0391, 0.5391, 0, 0],
+        "8715": [0.0391, 0.5391, 0, 0],
+        "8722": [0.08333, 0.58333, 0, 0],
+        "8723": [0.08333, 0.58333, 0, 0],
+        "8725": [0.25, 0.75, 0, 0],
+        "8726": [0.25, 0.75, 0, 0],
+        "8727": [-0.03472, 0.46528, 0, 0],
+        "8728": [-0.05555, 0.44445, 0, 0],
+        "8729": [-0.05555, 0.44445, 0, 0],
+        "8730": [0.2, 0.8, 0, 0],
+        "8733": [0, 0.43056, 0, 0],
+        "8734": [0, 0.43056, 0, 0],
+        "8736": [0, 0.69224, 0, 0],
+        "8739": [0.25, 0.75, 0, 0],
+        "8741": [0.25, 0.75, 0, 0],
+        "8743": [0, 0.55556, 0, 0],
+        "8744": [0, 0.55556, 0, 0],
+        "8745": [0, 0.55556, 0, 0],
+        "8746": [0, 0.55556, 0, 0],
+        "8747": [0.19444, 0.69444, 0.11111, 0],
+        "8764": [-0.13313, 0.36687, 0, 0],
+        "8768": [0.19444, 0.69444, 0, 0],
+        "8771": [-0.03625, 0.46375, 0, 0],
+        "8773": [-0.022, 0.589, 0, 0],
+        "8776": [-0.01688, 0.48312, 0, 0],
+        "8781": [-0.03625, 0.46375, 0, 0],
+        "8784": [-0.133, 0.67, 0, 0],
+        "8800": [0.215, 0.716, 0, 0],
+        "8801": [-0.03625, 0.46375, 0, 0],
+        "8804": [0.13597, 0.63597, 0, 0],
+        "8805": [0.13597, 0.63597, 0, 0],
+        "8810": [0.0391, 0.5391, 0, 0],
+        "8811": [0.0391, 0.5391, 0, 0],
+        "8826": [0.0391, 0.5391, 0, 0],
+        "8827": [0.0391, 0.5391, 0, 0],
+        "8834": [0.0391, 0.5391, 0, 0],
+        "8835": [0.0391, 0.5391, 0, 0],
+        "8838": [0.13597, 0.63597, 0, 0],
+        "8839": [0.13597, 0.63597, 0, 0],
+        "8846": [0, 0.55556, 0, 0],
+        "8849": [0.13597, 0.63597, 0, 0],
+        "8850": [0.13597, 0.63597, 0, 0],
+        "8851": [0, 0.55556, 0, 0],
+        "8852": [0, 0.55556, 0, 0],
+        "8853": [0.08333, 0.58333, 0, 0],
+        "8854": [0.08333, 0.58333, 0, 0],
+        "8855": [0.08333, 0.58333, 0, 0],
+        "8856": [0.08333, 0.58333, 0, 0],
+        "8857": [0.08333, 0.58333, 0, 0],
+        "8866": [0, 0.69444, 0, 0],
+        "8867": [0, 0.69444, 0, 0],
+        "8868": [0, 0.69444, 0, 0],
+        "8869": [0, 0.69444, 0, 0],
+        "8872": [0.249, 0.75, 0, 0],
+        "8900": [-0.05555, 0.44445, 0, 0],
+        "8901": [-0.05555, 0.44445, 0, 0],
+        "8902": [-0.03472, 0.46528, 0, 0],
+        "8904": [0.005, 0.505, 0, 0],
+        "8942": [0.03, 0.9, 0, 0],
+        "8943": [-0.19, 0.31, 0, 0],
+        "8945": [-0.1, 0.82, 0, 0],
+        "8968": [0.25, 0.75, 0, 0],
+        "8969": [0.25, 0.75, 0, 0],
+        "8970": [0.25, 0.75, 0, 0],
+        "8971": [0.25, 0.75, 0, 0],
+        "8994": [-0.14236, 0.35764, 0, 0],
+        "8995": [-0.14236, 0.35764, 0, 0],
+        "9136": [0.244, 0.744, 0, 0],
+        "9137": [0.244, 0.744, 0, 0],
+        "9651": [0.19444, 0.69444, 0, 0],
+        "9657": [-0.03472, 0.46528, 0, 0],
+        "9661": [0.19444, 0.69444, 0, 0],
+        "9667": [-0.03472, 0.46528, 0, 0],
+        "9711": [0.19444, 0.69444, 0, 0],
+        "9824": [0.12963, 0.69444, 0, 0],
+        "9825": [0.12963, 0.69444, 0, 0],
+        "9826": [0.12963, 0.69444, 0, 0],
+        "9827": [0.12963, 0.69444, 0, 0],
+        "9837": [0, 0.75, 0, 0],
+        "9838": [0.19444, 0.69444, 0, 0],
+        "9839": [0.19444, 0.69444, 0, 0],
+        "10216": [0.25, 0.75, 0, 0],
+        "10217": [0.25, 0.75, 0, 0],
+        "10222": [0.244, 0.744, 0, 0],
+        "10223": [0.244, 0.744, 0, 0],
+        "10229": [0.011, 0.511, 0, 0],
+        "10230": [0.011, 0.511, 0, 0],
+        "10231": [0.011, 0.511, 0, 0],
+        "10232": [0.024, 0.525, 0, 0],
+        "10233": [0.024, 0.525, 0, 0],
+        "10234": [0.024, 0.525, 0, 0],
+        "10236": [0.011, 0.511, 0, 0],
+        "10815": [0, 0.68333, 0, 0],
+        "10927": [0.13597, 0.63597, 0, 0],
+        "10928": [0.13597, 0.63597, 0, 0],
+    },
+    "Math-BoldItalic": {
+        "47": [0.19444, 0.69444, 0, 0],
+        "65": [0, 0.68611, 0, 0],
+        "66": [0, 0.68611, 0.04835, 0],
+        "67": [0, 0.68611, 0.06979, 0],
+        "68": [0, 0.68611, 0.03194, 0],
+        "69": [0, 0.68611, 0.05451, 0],
+        "70": [0, 0.68611, 0.15972, 0],
+        "71": [0, 0.68611, 0, 0],
+        "72": [0, 0.68611, 0.08229, 0],
+        "73": [0, 0.68611, 0.07778, 0],
+        "74": [0, 0.68611, 0.10069, 0],
+        "75": [0, 0.68611, 0.06979, 0],
+        "76": [0, 0.68611, 0, 0],
+        "77": [0, 0.68611, 0.11424, 0],
+        "78": [0, 0.68611, 0.11424, 0],
+        "79": [0, 0.68611, 0.03194, 0],
+        "80": [0, 0.68611, 0.15972, 0],
+        "81": [0.19444, 0.68611, 0, 0],
+        "82": [0, 0.68611, 0.00421, 0],
+        "83": [0, 0.68611, 0.05382, 0],
+        "84": [0, 0.68611, 0.15972, 0],
+        "85": [0, 0.68611, 0.11424, 0],
+        "86": [0, 0.68611, 0.25555, 0],
+        "87": [0, 0.68611, 0.15972, 0],
+        "88": [0, 0.68611, 0.07778, 0],
+        "89": [0, 0.68611, 0.25555, 0],
+        "90": [0, 0.68611, 0.06979, 0],
+        "97": [0, 0.44444, 0, 0],
+        "98": [0, 0.69444, 0, 0],
+        "99": [0, 0.44444, 0, 0],
+        "100": [0, 0.69444, 0, 0],
+        "101": [0, 0.44444, 0, 0],
+        "102": [0.19444, 0.69444, 0.11042, 0],
+        "103": [0.19444, 0.44444, 0.03704, 0],
+        "104": [0, 0.69444, 0, 0],
+        "105": [0, 0.69326, 0, 0],
+        "106": [0.19444, 0.69326, 0.0622, 0],
+        "107": [0, 0.69444, 0.01852, 0],
+        "108": [0, 0.69444, 0.0088, 0],
+        "109": [0, 0.44444, 0, 0],
+        "110": [0, 0.44444, 0, 0],
+        "111": [0, 0.44444, 0, 0],
+        "112": [0.19444, 0.44444, 0, 0],
+        "113": [0.19444, 0.44444, 0.03704, 0],
+        "114": [0, 0.44444, 0.03194, 0],
+        "115": [0, 0.44444, 0, 0],
+        "116": [0, 0.63492, 0, 0],
+        "117": [0, 0.44444, 0, 0],
+        "118": [0, 0.44444, 0.03704, 0],
+        "119": [0, 0.44444, 0.02778, 0],
+        "120": [0, 0.44444, 0, 0],
+        "121": [0.19444, 0.44444, 0.03704, 0],
+        "122": [0, 0.44444, 0.04213, 0],
+        "915": [0, 0.68611, 0.15972, 0],
+        "916": [0, 0.68611, 0, 0],
+        "920": [0, 0.68611, 0.03194, 0],
+        "923": [0, 0.68611, 0, 0],
+        "926": [0, 0.68611, 0.07458, 0],
+        "928": [0, 0.68611, 0.08229, 0],
+        "931": [0, 0.68611, 0.05451, 0],
+        "933": [0, 0.68611, 0.15972, 0],
+        "934": [0, 0.68611, 0, 0],
+        "936": [0, 0.68611, 0.11653, 0],
+        "937": [0, 0.68611, 0.04835, 0],
+        "945": [0, 0.44444, 0, 0],
+        "946": [0.19444, 0.69444, 0.03403, 0],
+        "947": [0.19444, 0.44444, 0.06389, 0],
+        "948": [0, 0.69444, 0.03819, 0],
+        "949": [0, 0.44444, 0, 0],
+        "950": [0.19444, 0.69444, 0.06215, 0],
+        "951": [0.19444, 0.44444, 0.03704, 0],
+        "952": [0, 0.69444, 0.03194, 0],
+        "953": [0, 0.44444, 0, 0],
+        "954": [0, 0.44444, 0, 0],
+        "955": [0, 0.69444, 0, 0],
+        "956": [0.19444, 0.44444, 0, 0],
+        "957": [0, 0.44444, 0.06898, 0],
+        "958": [0.19444, 0.69444, 0.03021, 0],
+        "959": [0, 0.44444, 0, 0],
+        "960": [0, 0.44444, 0.03704, 0],
+        "961": [0.19444, 0.44444, 0, 0],
+        "962": [0.09722, 0.44444, 0.07917, 0],
+        "963": [0, 0.44444, 0.03704, 0],
+        "964": [0, 0.44444, 0.13472, 0],
+        "965": [0, 0.44444, 0.03704, 0],
+        "966": [0.19444, 0.44444, 0, 0],
+        "967": [0.19444, 0.44444, 0, 0],
+        "968": [0.19444, 0.69444, 0.03704, 0],
+        "969": [0, 0.44444, 0.03704, 0],
+        "977": [0, 0.69444, 0, 0],
+        "981": [0.19444, 0.69444, 0, 0],
+        "982": [0, 0.44444, 0.03194, 0],
+        "1009": [0.19444, 0.44444, 0, 0],
+        "1013": [0, 0.44444, 0, 0],
+    },
+    "Math-Italic": {
+        "47": [0.19444, 0.69444, 0, 0],
+        "65": [0, 0.68333, 0, 0.13889],
+        "66": [0, 0.68333, 0.05017, 0.08334],
+        "67": [0, 0.68333, 0.07153, 0.08334],
+        "68": [0, 0.68333, 0.02778, 0.05556],
+        "69": [0, 0.68333, 0.05764, 0.08334],
+        "70": [0, 0.68333, 0.13889, 0.08334],
+        "71": [0, 0.68333, 0, 0.08334],
+        "72": [0, 0.68333, 0.08125, 0.05556],
+        "73": [0, 0.68333, 0.07847, 0.11111],
+        "74": [0, 0.68333, 0.09618, 0.16667],
+        "75": [0, 0.68333, 0.07153, 0.05556],
+        "76": [0, 0.68333, 0, 0.02778],
+        "77": [0, 0.68333, 0.10903, 0.08334],
+        "78": [0, 0.68333, 0.10903, 0.08334],
+        "79": [0, 0.68333, 0.02778, 0.08334],
+        "80": [0, 0.68333, 0.13889, 0.08334],
+        "81": [0.19444, 0.68333, 0, 0.08334],
+        "82": [0, 0.68333, 0.00773, 0.08334],
+        "83": [0, 0.68333, 0.05764, 0.08334],
+        "84": [0, 0.68333, 0.13889, 0.08334],
+        "85": [0, 0.68333, 0.10903, 0.02778],
+        "86": [0, 0.68333, 0.22222, 0],
+        "87": [0, 0.68333, 0.13889, 0],
+        "88": [0, 0.68333, 0.07847, 0.08334],
+        "89": [0, 0.68333, 0.22222, 0],
+        "90": [0, 0.68333, 0.07153, 0.08334],
+        "97": [0, 0.43056, 0, 0],
+        "98": [0, 0.69444, 0, 0],
+        "99": [0, 0.43056, 0, 0.05556],
+        "100": [0, 0.69444, 0, 0.16667],
+        "101": [0, 0.43056, 0, 0.05556],
+        "102": [0.19444, 0.69444, 0.10764, 0.16667],
+        "103": [0.19444, 0.43056, 0.03588, 0.02778],
+        "104": [0, 0.69444, 0, 0],
+        "105": [0, 0.65952, 0, 0],
+        "106": [0.19444, 0.65952, 0.05724, 0],
+        "107": [0, 0.69444, 0.03148, 0],
+        "108": [0, 0.69444, 0.01968, 0.08334],
+        "109": [0, 0.43056, 0, 0],
+        "110": [0, 0.43056, 0, 0],
+        "111": [0, 0.43056, 0, 0.05556],
+        "112": [0.19444, 0.43056, 0, 0.08334],
+        "113": [0.19444, 0.43056, 0.03588, 0.08334],
+        "114": [0, 0.43056, 0.02778, 0.05556],
+        "115": [0, 0.43056, 0, 0.05556],
+        "116": [0, 0.61508, 0, 0.08334],
+        "117": [0, 0.43056, 0, 0.02778],
+        "118": [0, 0.43056, 0.03588, 0.02778],
+        "119": [0, 0.43056, 0.02691, 0.08334],
+        "120": [0, 0.43056, 0, 0.02778],
+        "121": [0.19444, 0.43056, 0.03588, 0.05556],
+        "122": [0, 0.43056, 0.04398, 0.05556],
+        "915": [0, 0.68333, 0.13889, 0.08334],
+        "916": [0, 0.68333, 0, 0.16667],
+        "920": [0, 0.68333, 0.02778, 0.08334],
+        "923": [0, 0.68333, 0, 0.16667],
+        "926": [0, 0.68333, 0.07569, 0.08334],
+        "928": [0, 0.68333, 0.08125, 0.05556],
+        "931": [0, 0.68333, 0.05764, 0.08334],
+        "933": [0, 0.68333, 0.13889, 0.05556],
+        "934": [0, 0.68333, 0, 0.08334],
+        "936": [0, 0.68333, 0.11, 0.05556],
+        "937": [0, 0.68333, 0.05017, 0.08334],
+        "945": [0, 0.43056, 0.0037, 0.02778],
+        "946": [0.19444, 0.69444, 0.05278, 0.08334],
+        "947": [0.19444, 0.43056, 0.05556, 0],
+        "948": [0, 0.69444, 0.03785, 0.05556],
+        "949": [0, 0.43056, 0, 0.08334],
+        "950": [0.19444, 0.69444, 0.07378, 0.08334],
+        "951": [0.19444, 0.43056, 0.03588, 0.05556],
+        "952": [0, 0.69444, 0.02778, 0.08334],
+        "953": [0, 0.43056, 0, 0.05556],
+        "954": [0, 0.43056, 0, 0],
+        "955": [0, 0.69444, 0, 0],
+        "956": [0.19444, 0.43056, 0, 0.02778],
+        "957": [0, 0.43056, 0.06366, 0.02778],
+        "958": [0.19444, 0.69444, 0.04601, 0.11111],
+        "959": [0, 0.43056, 0, 0.05556],
+        "960": [0, 0.43056, 0.03588, 0],
+        "961": [0.19444, 0.43056, 0, 0.08334],
+        "962": [0.09722, 0.43056, 0.07986, 0.08334],
+        "963": [0, 0.43056, 0.03588, 0],
+        "964": [0, 0.43056, 0.1132, 0.02778],
+        "965": [0, 0.43056, 0.03588, 0.02778],
+        "966": [0.19444, 0.43056, 0, 0.08334],
+        "967": [0.19444, 0.43056, 0, 0.05556],
+        "968": [0.19444, 0.69444, 0.03588, 0.11111],
+        "969": [0, 0.43056, 0.03588, 0],
+        "977": [0, 0.69444, 0, 0.08334],
+        "981": [0.19444, 0.69444, 0, 0.08334],
+        "982": [0, 0.43056, 0.02778, 0],
+        "1009": [0.19444, 0.43056, 0, 0.08334],
+        "1013": [0, 0.43056, 0, 0.05556],
+    },
+    "Math-Regular": {
+        "65": [0, 0.68333, 0, 0.13889],
+        "66": [0, 0.68333, 0.05017, 0.08334],
+        "67": [0, 0.68333, 0.07153, 0.08334],
+        "68": [0, 0.68333, 0.02778, 0.05556],
+        "69": [0, 0.68333, 0.05764, 0.08334],
+        "70": [0, 0.68333, 0.13889, 0.08334],
+        "71": [0, 0.68333, 0, 0.08334],
+        "72": [0, 0.68333, 0.08125, 0.05556],
+        "73": [0, 0.68333, 0.07847, 0.11111],
+        "74": [0, 0.68333, 0.09618, 0.16667],
+        "75": [0, 0.68333, 0.07153, 0.05556],
+        "76": [0, 0.68333, 0, 0.02778],
+        "77": [0, 0.68333, 0.10903, 0.08334],
+        "78": [0, 0.68333, 0.10903, 0.08334],
+        "79": [0, 0.68333, 0.02778, 0.08334],
+        "80": [0, 0.68333, 0.13889, 0.08334],
+        "81": [0.19444, 0.68333, 0, 0.08334],
+        "82": [0, 0.68333, 0.00773, 0.08334],
+        "83": [0, 0.68333, 0.05764, 0.08334],
+        "84": [0, 0.68333, 0.13889, 0.08334],
+        "85": [0, 0.68333, 0.10903, 0.02778],
+        "86": [0, 0.68333, 0.22222, 0],
+        "87": [0, 0.68333, 0.13889, 0],
+        "88": [0, 0.68333, 0.07847, 0.08334],
+        "89": [0, 0.68333, 0.22222, 0],
+        "90": [0, 0.68333, 0.07153, 0.08334],
+        "97": [0, 0.43056, 0, 0],
+        "98": [0, 0.69444, 0, 0],
+        "99": [0, 0.43056, 0, 0.05556],
+        "100": [0, 0.69444, 0, 0.16667],
+        "101": [0, 0.43056, 0, 0.05556],
+        "102": [0.19444, 0.69444, 0.10764, 0.16667],
+        "103": [0.19444, 0.43056, 0.03588, 0.02778],
+        "104": [0, 0.69444, 0, 0],
+        "105": [0, 0.65952, 0, 0],
+        "106": [0.19444, 0.65952, 0.05724, 0],
+        "107": [0, 0.69444, 0.03148, 0],
+        "108": [0, 0.69444, 0.01968, 0.08334],
+        "109": [0, 0.43056, 0, 0],
+        "110": [0, 0.43056, 0, 0],
+        "111": [0, 0.43056, 0, 0.05556],
+        "112": [0.19444, 0.43056, 0, 0.08334],
+        "113": [0.19444, 0.43056, 0.03588, 0.08334],
+        "114": [0, 0.43056, 0.02778, 0.05556],
+        "115": [0, 0.43056, 0, 0.05556],
+        "116": [0, 0.61508, 0, 0.08334],
+        "117": [0, 0.43056, 0, 0.02778],
+        "118": [0, 0.43056, 0.03588, 0.02778],
+        "119": [0, 0.43056, 0.02691, 0.08334],
+        "120": [0, 0.43056, 0, 0.02778],
+        "121": [0.19444, 0.43056, 0.03588, 0.05556],
+        "122": [0, 0.43056, 0.04398, 0.05556],
+        "915": [0, 0.68333, 0.13889, 0.08334],
+        "916": [0, 0.68333, 0, 0.16667],
+        "920": [0, 0.68333, 0.02778, 0.08334],
+        "923": [0, 0.68333, 0, 0.16667],
+        "926": [0, 0.68333, 0.07569, 0.08334],
+        "928": [0, 0.68333, 0.08125, 0.05556],
+        "931": [0, 0.68333, 0.05764, 0.08334],
+        "933": [0, 0.68333, 0.13889, 0.05556],
+        "934": [0, 0.68333, 0, 0.08334],
+        "936": [0, 0.68333, 0.11, 0.05556],
+        "937": [0, 0.68333, 0.05017, 0.08334],
+        "945": [0, 0.43056, 0.0037, 0.02778],
+        "946": [0.19444, 0.69444, 0.05278, 0.08334],
+        "947": [0.19444, 0.43056, 0.05556, 0],
+        "948": [0, 0.69444, 0.03785, 0.05556],
+        "949": [0, 0.43056, 0, 0.08334],
+        "950": [0.19444, 0.69444, 0.07378, 0.08334],
+        "951": [0.19444, 0.43056, 0.03588, 0.05556],
+        "952": [0, 0.69444, 0.02778, 0.08334],
+        "953": [0, 0.43056, 0, 0.05556],
+        "954": [0, 0.43056, 0, 0],
+        "955": [0, 0.69444, 0, 0],
+        "956": [0.19444, 0.43056, 0, 0.02778],
+        "957": [0, 0.43056, 0.06366, 0.02778],
+        "958": [0.19444, 0.69444, 0.04601, 0.11111],
+        "959": [0, 0.43056, 0, 0.05556],
+        "960": [0, 0.43056, 0.03588, 0],
+        "961": [0.19444, 0.43056, 0, 0.08334],
+        "962": [0.09722, 0.43056, 0.07986, 0.08334],
+        "963": [0, 0.43056, 0.03588, 0],
+        "964": [0, 0.43056, 0.1132, 0.02778],
+        "965": [0, 0.43056, 0.03588, 0.02778],
+        "966": [0.19444, 0.43056, 0, 0.08334],
+        "967": [0.19444, 0.43056, 0, 0.05556],
+        "968": [0.19444, 0.69444, 0.03588, 0.11111],
+        "969": [0, 0.43056, 0.03588, 0],
+        "977": [0, 0.69444, 0, 0.08334],
+        "981": [0.19444, 0.69444, 0, 0.08334],
+        "982": [0, 0.43056, 0.02778, 0],
+        "1009": [0.19444, 0.43056, 0, 0.08334],
+        "1013": [0, 0.43056, 0, 0.05556],
+    },
+    "SansSerif-Regular": {
+        "33": [0, 0.69444, 0, 0],
+        "34": [0, 0.69444, 0, 0],
+        "35": [0.19444, 0.69444, 0, 0],
+        "36": [0.05556, 0.75, 0, 0],
+        "37": [0.05556, 0.75, 0, 0],
+        "38": [0, 0.69444, 0, 0],
+        "39": [0, 0.69444, 0, 0],
+        "40": [0.25, 0.75, 0, 0],
+        "41": [0.25, 0.75, 0, 0],
+        "42": [0, 0.75, 0, 0],
+        "43": [0.08333, 0.58333, 0, 0],
+        "44": [0.125, 0.08333, 0, 0],
+        "45": [0, 0.44444, 0, 0],
+        "46": [0, 0.08333, 0, 0],
+        "47": [0.25, 0.75, 0, 0],
+        "48": [0, 0.65556, 0, 0],
+        "49": [0, 0.65556, 0, 0],
+        "50": [0, 0.65556, 0, 0],
+        "51": [0, 0.65556, 0, 0],
+        "52": [0, 0.65556, 0, 0],
+        "53": [0, 0.65556, 0, 0],
+        "54": [0, 0.65556, 0, 0],
+        "55": [0, 0.65556, 0, 0],
+        "56": [0, 0.65556, 0, 0],
+        "57": [0, 0.65556, 0, 0],
+        "58": [0, 0.44444, 0, 0],
+        "59": [0.125, 0.44444, 0, 0],
+        "61": [-0.13, 0.37, 0, 0],
+        "63": [0, 0.69444, 0, 0],
+        "64": [0, 0.69444, 0, 0],
+        "65": [0, 0.69444, 0, 0],
+        "66": [0, 0.69444, 0, 0],
+        "67": [0, 0.69444, 0, 0],
+        "68": [0, 0.69444, 0, 0],
+        "69": [0, 0.69444, 0, 0],
+        "70": [0, 0.69444, 0, 0],
+        "71": [0, 0.69444, 0, 0],
+        "72": [0, 0.69444, 0, 0],
+        "73": [0, 0.69444, 0, 0],
+        "74": [0, 0.69444, 0, 0],
+        "75": [0, 0.69444, 0, 0],
+        "76": [0, 0.69444, 0, 0],
+        "77": [0, 0.69444, 0, 0],
+        "78": [0, 0.69444, 0, 0],
+        "79": [0, 0.69444, 0, 0],
+        "80": [0, 0.69444, 0, 0],
+        "81": [0.125, 0.69444, 0, 0],
+        "82": [0, 0.69444, 0, 0],
+        "83": [0, 0.69444, 0, 0],
+        "84": [0, 0.69444, 0, 0],
+        "85": [0, 0.69444, 0, 0],
+        "86": [0, 0.69444, 0.01389, 0],
+        "87": [0, 0.69444, 0.01389, 0],
+        "88": [0, 0.69444, 0, 0],
+        "89": [0, 0.69444, 0.025, 0],
+        "90": [0, 0.69444, 0, 0],
+        "91": [0.25, 0.75, 0, 0],
+        "93": [0.25, 0.75, 0, 0],
+        "94": [0, 0.69444, 0, 0],
+        "95": [0.35, 0.09444, 0.02778, 0],
+        "97": [0, 0.44444, 0, 0],
+        "98": [0, 0.69444, 0, 0],
+        "99": [0, 0.44444, 0, 0],
+        "100": [0, 0.69444, 0, 0],
+        "101": [0, 0.44444, 0, 0],
+        "102": [0, 0.69444, 0.06944, 0],
+        "103": [0.19444, 0.44444, 0.01389, 0],
+        "104": [0, 0.69444, 0, 0],
+        "105": [0, 0.67937, 0, 0],
+        "106": [0.19444, 0.67937, 0, 0],
+        "107": [0, 0.69444, 0, 0],
+        "108": [0, 0.69444, 0, 0],
+        "109": [0, 0.44444, 0, 0],
+        "110": [0, 0.44444, 0, 0],
+        "111": [0, 0.44444, 0, 0],
+        "112": [0.19444, 0.44444, 0, 0],
+        "113": [0.19444, 0.44444, 0, 0],
+        "114": [0, 0.44444, 0.01389, 0],
+        "115": [0, 0.44444, 0, 0],
+        "116": [0, 0.57143, 0, 0],
+        "117": [0, 0.44444, 0, 0],
+        "118": [0, 0.44444, 0.01389, 0],
+        "119": [0, 0.44444, 0.01389, 0],
+        "120": [0, 0.44444, 0, 0],
+        "121": [0.19444, 0.44444, 0.01389, 0],
+        "122": [0, 0.44444, 0, 0],
+        "126": [0.35, 0.32659, 0, 0],
+        "305": [0, 0.44444, 0, 0],
+        "567": [0.19444, 0.44444, 0, 0],
+        "768": [0, 0.69444, 0, 0],
+        "769": [0, 0.69444, 0, 0],
+        "770": [0, 0.69444, 0, 0],
+        "771": [0, 0.67659, 0, 0],
+        "772": [0, 0.60889, 0, 0],
+        "774": [0, 0.69444, 0, 0],
+        "775": [0, 0.67937, 0, 0],
+        "776": [0, 0.67937, 0, 0],
+        "778": [0, 0.69444, 0, 0],
+        "779": [0, 0.69444, 0, 0],
+        "780": [0, 0.63194, 0, 0],
+        "915": [0, 0.69444, 0, 0],
+        "916": [0, 0.69444, 0, 0],
+        "920": [0, 0.69444, 0, 0],
+        "923": [0, 0.69444, 0, 0],
+        "926": [0, 0.69444, 0, 0],
+        "928": [0, 0.69444, 0, 0],
+        "931": [0, 0.69444, 0, 0],
+        "933": [0, 0.69444, 0, 0],
+        "934": [0, 0.69444, 0, 0],
+        "936": [0, 0.69444, 0, 0],
+        "937": [0, 0.69444, 0, 0],
+        "8211": [0, 0.44444, 0.02778, 0],
+        "8212": [0, 0.44444, 0.02778, 0],
+        "8216": [0, 0.69444, 0, 0],
+        "8217": [0, 0.69444, 0, 0],
+        "8220": [0, 0.69444, 0, 0],
+        "8221": [0, 0.69444, 0, 0],
+    },
+    "Script-Regular": {
+        "65": [0, 0.7, 0.22925, 0],
+        "66": [0, 0.7, 0.04087, 0],
+        "67": [0, 0.7, 0.1689, 0],
+        "68": [0, 0.7, 0.09371, 0],
+        "69": [0, 0.7, 0.18583, 0],
+        "70": [0, 0.7, 0.13634, 0],
+        "71": [0, 0.7, 0.17322, 0],
+        "72": [0, 0.7, 0.29694, 0],
+        "73": [0, 0.7, 0.19189, 0],
+        "74": [0.27778, 0.7, 0.19189, 0],
+        "75": [0, 0.7, 0.31259, 0],
+        "76": [0, 0.7, 0.19189, 0],
+        "77": [0, 0.7, 0.15981, 0],
+        "78": [0, 0.7, 0.3525, 0],
+        "79": [0, 0.7, 0.08078, 0],
+        "80": [0, 0.7, 0.08078, 0],
+        "81": [0, 0.7, 0.03305, 0],
+        "82": [0, 0.7, 0.06259, 0],
+        "83": [0, 0.7, 0.19189, 0],
+        "84": [0, 0.7, 0.29087, 0],
+        "85": [0, 0.7, 0.25815, 0],
+        "86": [0, 0.7, 0.27523, 0],
+        "87": [0, 0.7, 0.27523, 0],
+        "88": [0, 0.7, 0.26006, 0],
+        "89": [0, 0.7, 0.2939, 0],
+        "90": [0, 0.7, 0.24037, 0],
+    },
+    "Size1-Regular": {
+        "40": [0.35001, 0.85, 0, 0],
+        "41": [0.35001, 0.85, 0, 0],
+        "47": [0.35001, 0.85, 0, 0],
+        "91": [0.35001, 0.85, 0, 0],
+        "92": [0.35001, 0.85, 0, 0],
+        "93": [0.35001, 0.85, 0, 0],
+        "123": [0.35001, 0.85, 0, 0],
+        "125": [0.35001, 0.85, 0, 0],
+        "710": [0, 0.72222, 0, 0],
+        "732": [0, 0.72222, 0, 0],
+        "770": [0, 0.72222, 0, 0],
+        "771": [0, 0.72222, 0, 0],
+        "8214": [-0.00099, 0.601, 0, 0],
+        "8593": [1e-05, 0.6, 0, 0],
+        "8595": [1e-05, 0.6, 0, 0],
+        "8657": [1e-05, 0.6, 0, 0],
+        "8659": [1e-05, 0.6, 0, 0],
+        "8719": [0.25001, 0.75, 0, 0],
+        "8720": [0.25001, 0.75, 0, 0],
+        "8721": [0.25001, 0.75, 0, 0],
+        "8730": [0.35001, 0.85, 0, 0],
+        "8739": [-0.00599, 0.606, 0, 0],
+        "8741": [-0.00599, 0.606, 0, 0],
+        "8747": [0.30612, 0.805, 0.19445, 0],
+        "8748": [0.306, 0.805, 0.19445, 0],
+        "8749": [0.306, 0.805, 0.19445, 0],
+        "8750": [0.30612, 0.805, 0.19445, 0],
+        "8896": [0.25001, 0.75, 0, 0],
+        "8897": [0.25001, 0.75, 0, 0],
+        "8898": [0.25001, 0.75, 0, 0],
+        "8899": [0.25001, 0.75, 0, 0],
+        "8968": [0.35001, 0.85, 0, 0],
+        "8969": [0.35001, 0.85, 0, 0],
+        "8970": [0.35001, 0.85, 0, 0],
+        "8971": [0.35001, 0.85, 0, 0],
+        "9168": [-0.00099, 0.601, 0, 0],
+        "10216": [0.35001, 0.85, 0, 0],
+        "10217": [0.35001, 0.85, 0, 0],
+        "10752": [0.25001, 0.75, 0, 0],
+        "10753": [0.25001, 0.75, 0, 0],
+        "10754": [0.25001, 0.75, 0, 0],
+        "10756": [0.25001, 0.75, 0, 0],
+        "10758": [0.25001, 0.75, 0, 0],
+    },
+    "Size2-Regular": {
+        "40": [0.65002, 1.15, 0, 0],
+        "41": [0.65002, 1.15, 0, 0],
+        "47": [0.65002, 1.15, 0, 0],
+        "91": [0.65002, 1.15, 0, 0],
+        "92": [0.65002, 1.15, 0, 0],
+        "93": [0.65002, 1.15, 0, 0],
+        "123": [0.65002, 1.15, 0, 0],
+        "125": [0.65002, 1.15, 0, 0],
+        "710": [0, 0.75, 0, 0],
+        "732": [0, 0.75, 0, 0],
+        "770": [0, 0.75, 0, 0],
+        "771": [0, 0.75, 0, 0],
+        "8719": [0.55001, 1.05, 0, 0],
+        "8720": [0.55001, 1.05, 0, 0],
+        "8721": [0.55001, 1.05, 0, 0],
+        "8730": [0.65002, 1.15, 0, 0],
+        "8747": [0.86225, 1.36, 0.44445, 0],
+        "8748": [0.862, 1.36, 0.44445, 0],
+        "8749": [0.862, 1.36, 0.44445, 0],
+        "8750": [0.86225, 1.36, 0.44445, 0],
+        "8896": [0.55001, 1.05, 0, 0],
+        "8897": [0.55001, 1.05, 0, 0],
+        "8898": [0.55001, 1.05, 0, 0],
+        "8899": [0.55001, 1.05, 0, 0],
+        "8968": [0.65002, 1.15, 0, 0],
+        "8969": [0.65002, 1.15, 0, 0],
+        "8970": [0.65002, 1.15, 0, 0],
+        "8971": [0.65002, 1.15, 0, 0],
+        "10216": [0.65002, 1.15, 0, 0],
+        "10217": [0.65002, 1.15, 0, 0],
+        "10752": [0.55001, 1.05, 0, 0],
+        "10753": [0.55001, 1.05, 0, 0],
+        "10754": [0.55001, 1.05, 0, 0],
+        "10756": [0.55001, 1.05, 0, 0],
+        "10758": [0.55001, 1.05, 0, 0],
+    },
+    "Size3-Regular": {
+        "40": [0.95003, 1.45, 0, 0],
+        "41": [0.95003, 1.45, 0, 0],
+        "47": [0.95003, 1.45, 0, 0],
+        "91": [0.95003, 1.45, 0, 0],
+        "92": [0.95003, 1.45, 0, 0],
+        "93": [0.95003, 1.45, 0, 0],
+        "123": [0.95003, 1.45, 0, 0],
+        "125": [0.95003, 1.45, 0, 0],
+        "710": [0, 0.75, 0, 0],
+        "732": [0, 0.75, 0, 0],
+        "770": [0, 0.75, 0, 0],
+        "771": [0, 0.75, 0, 0],
+        "8730": [0.95003, 1.45, 0, 0],
+        "8968": [0.95003, 1.45, 0, 0],
+        "8969": [0.95003, 1.45, 0, 0],
+        "8970": [0.95003, 1.45, 0, 0],
+        "8971": [0.95003, 1.45, 0, 0],
+        "10216": [0.95003, 1.45, 0, 0],
+        "10217": [0.95003, 1.45, 0, 0],
+    },
+    "Size4-Regular": {
+        "40": [1.25003, 1.75, 0, 0],
+        "41": [1.25003, 1.75, 0, 0],
+        "47": [1.25003, 1.75, 0, 0],
+        "91": [1.25003, 1.75, 0, 0],
+        "92": [1.25003, 1.75, 0, 0],
+        "93": [1.25003, 1.75, 0, 0],
+        "123": [1.25003, 1.75, 0, 0],
+        "125": [1.25003, 1.75, 0, 0],
+        "710": [0, 0.825, 0, 0],
+        "732": [0, 0.825, 0, 0],
+        "770": [0, 0.825, 0, 0],
+        "771": [0, 0.825, 0, 0],
+        "8730": [1.25003, 1.75, 0, 0],
+        "8968": [1.25003, 1.75, 0, 0],
+        "8969": [1.25003, 1.75, 0, 0],
+        "8970": [1.25003, 1.75, 0, 0],
+        "8971": [1.25003, 1.75, 0, 0],
+        "9115": [0.64502, 1.155, 0, 0],
+        "9116": [1e-05, 0.6, 0, 0],
+        "9117": [0.64502, 1.155, 0, 0],
+        "9118": [0.64502, 1.155, 0, 0],
+        "9119": [1e-05, 0.6, 0, 0],
+        "9120": [0.64502, 1.155, 0, 0],
+        "9121": [0.64502, 1.155, 0, 0],
+        "9122": [-0.00099, 0.601, 0, 0],
+        "9123": [0.64502, 1.155, 0, 0],
+        "9124": [0.64502, 1.155, 0, 0],
+        "9125": [-0.00099, 0.601, 0, 0],
+        "9126": [0.64502, 1.155, 0, 0],
+        "9127": [1e-05, 0.9, 0, 0],
+        "9128": [0.65002, 1.15, 0, 0],
+        "9129": [0.90001, 0, 0, 0],
+        "9130": [0, 0.3, 0, 0],
+        "9131": [1e-05, 0.9, 0, 0],
+        "9132": [0.65002, 1.15, 0, 0],
+        "9133": [0.90001, 0, 0, 0],
+        "9143": [0.88502, 0.915, 0, 0],
+        "10216": [1.25003, 1.75, 0, 0],
+        "10217": [1.25003, 1.75, 0, 0],
+        "57344": [-0.00499, 0.605, 0, 0],
+        "57345": [-0.00499, 0.605, 0, 0],
+        "57680": [0, 0.12, 0, 0],
+        "57681": [0, 0.12, 0, 0],
+        "57682": [0, 0.12, 0, 0],
+        "57683": [0, 0.12, 0, 0],
+    },
+    "Typewriter-Regular": {
+        "33": [0, 0.61111, 0, 0],
+        "34": [0, 0.61111, 0, 0],
+        "35": [0, 0.61111, 0, 0],
+        "36": [0.08333, 0.69444, 0, 0],
+        "37": [0.08333, 0.69444, 0, 0],
+        "38": [0, 0.61111, 0, 0],
+        "39": [0, 0.61111, 0, 0],
+        "40": [0.08333, 0.69444, 0, 0],
+        "41": [0.08333, 0.69444, 0, 0],
+        "42": [0, 0.52083, 0, 0],
+        "43": [-0.08056, 0.53055, 0, 0],
+        "44": [0.13889, 0.125, 0, 0],
+        "45": [-0.08056, 0.53055, 0, 0],
+        "46": [0, 0.125, 0, 0],
+        "47": [0.08333, 0.69444, 0, 0],
+        "48": [0, 0.61111, 0, 0],
+        "49": [0, 0.61111, 0, 0],
+        "50": [0, 0.61111, 0, 0],
+        "51": [0, 0.61111, 0, 0],
+        "52": [0, 0.61111, 0, 0],
+        "53": [0, 0.61111, 0, 0],
+        "54": [0, 0.61111, 0, 0],
+        "55": [0, 0.61111, 0, 0],
+        "56": [0, 0.61111, 0, 0],
+        "57": [0, 0.61111, 0, 0],
+        "58": [0, 0.43056, 0, 0],
+        "59": [0.13889, 0.43056, 0, 0],
+        "60": [-0.05556, 0.55556, 0, 0],
+        "61": [-0.19549, 0.41562, 0, 0],
+        "62": [-0.05556, 0.55556, 0, 0],
+        "63": [0, 0.61111, 0, 0],
+        "64": [0, 0.61111, 0, 0],
+        "65": [0, 0.61111, 0, 0],
+        "66": [0, 0.61111, 0, 0],
+        "67": [0, 0.61111, 0, 0],
+        "68": [0, 0.61111, 0, 0],
+        "69": [0, 0.61111, 0, 0],
+        "70": [0, 0.61111, 0, 0],
+        "71": [0, 0.61111, 0, 0],
+        "72": [0, 0.61111, 0, 0],
+        "73": [0, 0.61111, 0, 0],
+        "74": [0, 0.61111, 0, 0],
+        "75": [0, 0.61111, 0, 0],
+        "76": [0, 0.61111, 0, 0],
+        "77": [0, 0.61111, 0, 0],
+        "78": [0, 0.61111, 0, 0],
+        "79": [0, 0.61111, 0, 0],
+        "80": [0, 0.61111, 0, 0],
+        "81": [0.13889, 0.61111, 0, 0],
+        "82": [0, 0.61111, 0, 0],
+        "83": [0, 0.61111, 0, 0],
+        "84": [0, 0.61111, 0, 0],
+        "85": [0, 0.61111, 0, 0],
+        "86": [0, 0.61111, 0, 0],
+        "87": [0, 0.61111, 0, 0],
+        "88": [0, 0.61111, 0, 0],
+        "89": [0, 0.61111, 0, 0],
+        "90": [0, 0.61111, 0, 0],
+        "91": [0.08333, 0.69444, 0, 0],
+        "92": [0.08333, 0.69444, 0, 0],
+        "93": [0.08333, 0.69444, 0, 0],
+        "94": [0, 0.61111, 0, 0],
+        "95": [0.09514, 0, 0, 0],
+        "96": [0, 0.61111, 0, 0],
+        "97": [0, 0.43056, 0, 0],
+        "98": [0, 0.61111, 0, 0],
+        "99": [0, 0.43056, 0, 0],
+        "100": [0, 0.61111, 0, 0],
+        "101": [0, 0.43056, 0, 0],
+        "102": [0, 0.61111, 0, 0],
+        "103": [0.22222, 0.43056, 0, 0],
+        "104": [0, 0.61111, 0, 0],
+        "105": [0, 0.61111, 0, 0],
+        "106": [0.22222, 0.61111, 0, 0],
+        "107": [0, 0.61111, 0, 0],
+        "108": [0, 0.61111, 0, 0],
+        "109": [0, 0.43056, 0, 0],
+        "110": [0, 0.43056, 0, 0],
+        "111": [0, 0.43056, 0, 0],
+        "112": [0.22222, 0.43056, 0, 0],
+        "113": [0.22222, 0.43056, 0, 0],
+        "114": [0, 0.43056, 0, 0],
+        "115": [0, 0.43056, 0, 0],
+        "116": [0, 0.55358, 0, 0],
+        "117": [0, 0.43056, 0, 0],
+        "118": [0, 0.43056, 0, 0],
+        "119": [0, 0.43056, 0, 0],
+        "120": [0, 0.43056, 0, 0],
+        "121": [0.22222, 0.43056, 0, 0],
+        "122": [0, 0.43056, 0, 0],
+        "123": [0.08333, 0.69444, 0, 0],
+        "124": [0.08333, 0.69444, 0, 0],
+        "125": [0.08333, 0.69444, 0, 0],
+        "126": [0, 0.61111, 0, 0],
+        "127": [0, 0.61111, 0, 0],
+        "305": [0, 0.43056, 0, 0],
+        "567": [0.22222, 0.43056, 0, 0],
+        "768": [0, 0.61111, 0, 0],
+        "769": [0, 0.61111, 0, 0],
+        "770": [0, 0.61111, 0, 0],
+        "771": [0, 0.61111, 0, 0],
+        "772": [0, 0.56555, 0, 0],
+        "774": [0, 0.61111, 0, 0],
+        "776": [0, 0.61111, 0, 0],
+        "778": [0, 0.61111, 0, 0],
+        "780": [0, 0.56597, 0, 0],
+        "915": [0, 0.61111, 0, 0],
+        "916": [0, 0.61111, 0, 0],
+        "920": [0, 0.61111, 0, 0],
+        "923": [0, 0.61111, 0, 0],
+        "926": [0, 0.61111, 0, 0],
+        "928": [0, 0.61111, 0, 0],
+        "931": [0, 0.61111, 0, 0],
+        "933": [0, 0.61111, 0, 0],
+        "934": [0, 0.61111, 0, 0],
+        "936": [0, 0.61111, 0, 0],
+        "937": [0, 0.61111, 0, 0],
+        "2018": [0, 0.61111, 0, 0],
+        "2019": [0, 0.61111, 0, 0],
+        "8242": [0, 0.61111, 0, 0],
+    },
+};
+
+},{}],19:[function(require,module,exports){
+var utils = require("./utils");
+var ParseError = require("./ParseError");
+
+/* This file contains a list of functions that we parse, identified by
+ * the calls to defineFunction.
+ *
+ * The first argument to defineFunction is a single name or a list of names.
+ * All functions named in such a list will share a single implementation.
+ *
+ * Each declared function can have associated properties, which
+ * include the following:
+ *
+ *  - numArgs: The number of arguments the function takes.
+ *             If this is the only property, it can be passed as a number
+ *             instead of an element of a properties object.
+ *  - argTypes: (optional) An array corresponding to each argument of the
+ *              function, giving the type of argument that should be parsed. Its
+ *              length should be equal to `numArgs + numOptionalArgs`. Valid
+ *              types:
+ *               - "size": A size-like thing, such as "1em" or "5ex"
+ *               - "color": An html color, like "#abc" or "blue"
+ *               - "original": The same type as the environment that the
+ *                             function being parsed is in (e.g. used for the
+ *                             bodies of functions like \color where the first
+ *                             argument is special and the second argument is
+ *                             parsed normally)
+ *              Other possible types (probably shouldn't be used)
+ *               - "text": Text-like (e.g. \text)
+ *               - "math": Normal math
+ *              If undefined, this will be treated as an appropriate length
+ *              array of "original" strings
+ *  - greediness: (optional) The greediness of the function to use ungrouped
+ *                arguments.
+ *
+ *                E.g. if you have an expression
+ *                  \sqrt \frac 1 2
+ *                since \frac has greediness=2 vs \sqrt's greediness=1, \frac
+ *                will use the two arguments '1' and '2' as its two arguments,
+ *                then that whole function will be used as the argument to
+ *                \sqrt. On the other hand, the expressions
+ *                  \frac \frac 1 2 3
+ *                and
+ *                  \frac \sqrt 1 2
+ *                will fail because \frac and \frac have equal greediness
+ *                and \sqrt has a lower greediness than \frac respectively. To
+ *                make these parse, we would have to change them to:
+ *                  \frac {\frac 1 2} 3
+ *                and
+ *                  \frac {\sqrt 1} 2
+ *
+ *                The default value is `1`
+ *  - allowedInText: (optional) Whether or not the function is allowed inside
+ *                   text mode (default false)
+ *  - numOptionalArgs: (optional) The number of optional arguments the function
+ *                     should parse. If the optional arguments aren't found,
+ *                     `null` will be passed to the handler in their place.
+ *                     (default 0)
+ *  - infix: (optional) Must be true if the function is an infix operator.
+ *
+ * The last argument is that implementation, the handler for the function(s).
+ * It is called to handle these functions and their arguments.
+ * It receives two arguments:
+ *  - context contains information and references provided by the parser
+ *  - args is an array of arguments obtained from TeX input
+ * The context contains the following properties:
+ *  - funcName: the text (i.e. name) of the function, including \
+ *  - parser: the parser object
+ *  - lexer: the lexer object
+ *  - positions: the positions in the overall string of the function
+ *               and the arguments.
+ * The latter three should only be used to produce error messages.
+ *
+ * The function should return an object with the following keys:
+ *  - type: The type of element that this is. This is then used in
+ *          buildHTML/buildMathML to determine which function
+ *          should be called to build this node into a DOM node
+ * Any other data can be added to the object, which will be passed
+ * in to the function in buildHTML/buildMathML as `group.value`.
+ */
+
+function defineFunction(names, props, handler) {
+    if (typeof names === "string") {
+        names = [names];
+    }
+    if (typeof props === "number") {
+        props = { numArgs: props };
+    }
+    // Set default values of functions
+    var data = {
+        numArgs: props.numArgs,
+        argTypes: props.argTypes,
+        greediness: (props.greediness === undefined) ? 1 : props.greediness,
+        allowedInText: !!props.allowedInText,
+        numOptionalArgs: props.numOptionalArgs || 0,
+        infix: !!props.infix,
+        handler: handler,
+    };
+    for (var i = 0; i < names.length; ++i) {
+        module.exports[names[i]] = data;
+    }
+}
+
+// A normal square root
+defineFunction("\\sqrt", {
+    numArgs: 1,
+    numOptionalArgs: 1,
+}, function(context, args) {
+    var index = args[0];
+    var body = args[1];
+    return {
+        type: "sqrt",
+        body: body,
+        index: index,
+    };
+});
+
+// Some non-mathy text
+defineFunction("\\text", {
+    numArgs: 1,
+    argTypes: ["text"],
+    greediness: 2,
+}, function(context, args) {
+    var body = args[0];
+    // Since the corresponding buildHTML/buildMathML function expects a
+    // list of elements, we normalize for different kinds of arguments
+    // TODO(emily): maybe this should be done somewhere else
+    var inner;
+    if (body.type === "ordgroup") {
+        inner = body.value;
+    } else {
+        inner = [body];
+    }
+
+    return {
+        type: "text",
+        body: inner,
+    };
+});
+
+// A two-argument custom color
+defineFunction("\\color", {
+    numArgs: 2,
+    allowedInText: true,
+    greediness: 3,
+    argTypes: ["color", "original"],
+}, function(context, args) {
+    var color = args[0];
+    var body = args[1];
+    // Normalize the different kinds of bodies (see \text above)
+    var inner;
+    if (body.type === "ordgroup") {
+        inner = body.value;
+    } else {
+        inner = [body];
+    }
+
+    return {
+        type: "color",
+        color: color.value,
+        value: inner,
+    };
+});
+
+// An overline
+defineFunction("\\overline", {
+    numArgs: 1,
+}, function(context, args) {
+    var body = args[0];
+    return {
+        type: "overline",
+        body: body,
+    };
+});
+
+// An underline
+defineFunction("\\underline", {
+    numArgs: 1,
+}, function(context, args) {
+    var body = args[0];
+    return {
+        type: "underline",
+        body: body,
+    };
+});
+
+// A box of the width and height
+defineFunction("\\rule", {
+    numArgs: 2,
+    numOptionalArgs: 1,
+    argTypes: ["size", "size", "size"],
+}, function(context, args) {
+    var shift = args[0];
+    var width = args[1];
+    var height = args[2];
+    return {
+        type: "rule",
+        shift: shift && shift.value,
+        width: width.value,
+        height: height.value,
+    };
+});
+
+defineFunction("\\kern", {
+    numArgs: 1,
+    argTypes: ["size"],
+}, function(context, args) {
+    return {
+        type: "kern",
+        dimension: args[0].value,
+    };
+});
+
+// A KaTeX logo
+defineFunction("\\KaTeX", {
+    numArgs: 0,
+}, function(context) {
+    return {
+        type: "katex",
+    };
+});
+
+defineFunction("\\phantom", {
+    numArgs: 1,
+}, function(context, args) {
+    var body = args[0];
+    var inner;
+    if (body.type === "ordgroup") {
+        inner = body.value;
+    } else {
+        inner = [body];
+    }
+
+    return {
+        type: "phantom",
+        value: inner,
+    };
+});
+
+// Extra data needed for the delimiter handler down below
+var delimiterSizes = {
+    "\\bigl" : {type: "open",    size: 1},
+    "\\Bigl" : {type: "open",    size: 2},
+    "\\biggl": {type: "open",    size: 3},
+    "\\Biggl": {type: "open",    size: 4},
+    "\\bigr" : {type: "close",   size: 1},
+    "\\Bigr" : {type: "close",   size: 2},
+    "\\biggr": {type: "close",   size: 3},
+    "\\Biggr": {type: "close",   size: 4},
+    "\\bigm" : {type: "rel",     size: 1},
+    "\\Bigm" : {type: "rel",     size: 2},
+    "\\biggm": {type: "rel",     size: 3},
+    "\\Biggm": {type: "rel",     size: 4},
+    "\\big"  : {type: "textord", size: 1},
+    "\\Big"  : {type: "textord", size: 2},
+    "\\bigg" : {type: "textord", size: 3},
+    "\\Bigg" : {type: "textord", size: 4},
+};
+
+var delimiters = [
+    "(", ")", "[", "\\lbrack", "]", "\\rbrack",
+    "\\{", "\\lbrace", "\\}", "\\rbrace",
+    "\\lfloor", "\\rfloor", "\\lceil", "\\rceil",
+    "<", ">", "\\langle", "\\rangle", "\\lt", "\\gt",
+    "\\lvert", "\\rvert", "\\lVert", "\\rVert",
+    "\\lgroup", "\\rgroup", "\\lmoustache", "\\rmoustache",
+    "/", "\\backslash",
+    "|", "\\vert", "\\|", "\\Vert",
+    "\\uparrow", "\\Uparrow",
+    "\\downarrow", "\\Downarrow",
+    "\\updownarrow", "\\Updownarrow",
+    ".",
+];
+
+var fontAliases = {
+    "\\Bbb": "\\mathbb",
+    "\\bold": "\\mathbf",
+    "\\frak": "\\mathfrak",
+};
+
+// Single-argument color functions
+defineFunction([
+    "\\blue", "\\orange", "\\pink", "\\red",
+    "\\green", "\\gray", "\\purple",
+    "\\blueA", "\\blueB", "\\blueC", "\\blueD", "\\blueE",
+    "\\tealA", "\\tealB", "\\tealC", "\\tealD", "\\tealE",
+    "\\greenA", "\\greenB", "\\greenC", "\\greenD", "\\greenE",
+    "\\goldA", "\\goldB", "\\goldC", "\\goldD", "\\goldE",
+    "\\redA", "\\redB", "\\redC", "\\redD", "\\redE",
+    "\\maroonA", "\\maroonB", "\\maroonC", "\\maroonD", "\\maroonE",
+    "\\purpleA", "\\purpleB", "\\purpleC", "\\purpleD", "\\purpleE",
+    "\\mintA", "\\mintB", "\\mintC",
+    "\\grayA", "\\grayB", "\\grayC", "\\grayD", "\\grayE",
+    "\\grayF", "\\grayG", "\\grayH", "\\grayI",
+    "\\kaBlue", "\\kaGreen",
+], {
+    numArgs: 1,
+    allowedInText: true,
+    greediness: 3,
+}, function(context, args) {
+    var body = args[0];
+    var atoms;
+    if (body.type === "ordgroup") {
+        atoms = body.value;
+    } else {
+        atoms = [body];
+    }
+
+    return {
+        type: "color",
+        color: "katex-" + context.funcName.slice(1),
+        value: atoms,
+    };
+});
+
+// There are 2 flags for operators; whether they produce limits in
+// displaystyle, and whether they are symbols and should grow in
+// displaystyle. These four groups cover the four possible choices.
+
+// No limits, not symbols
+defineFunction([
+    "\\arcsin", "\\arccos", "\\arctan", "\\arg", "\\cos", "\\cosh",
+    "\\cot", "\\coth", "\\csc", "\\deg", "\\dim", "\\exp", "\\hom",
+    "\\ker", "\\lg", "\\ln", "\\log", "\\sec", "\\sin", "\\sinh",
+    "\\tan", "\\tanh",
+], {
+    numArgs: 0,
+}, function(context) {
+    return {
+        type: "op",
+        limits: false,
+        symbol: false,
+        body: context.funcName,
+    };
+});
+
+// Limits, not symbols
+defineFunction([
+    "\\det", "\\gcd", "\\inf", "\\lim", "\\liminf", "\\limsup", "\\max",
+    "\\min", "\\Pr", "\\sup",
+], {
+    numArgs: 0,
+}, function(context) {
+    return {
+        type: "op",
+        limits: true,
+        symbol: false,
+        body: context.funcName,
+    };
+});
+
+// No limits, symbols
+defineFunction([
+    "\\int", "\\iint", "\\iiint", "\\oint",
+], {
+    numArgs: 0,
+}, function(context) {
+    return {
+        type: "op",
+        limits: false,
+        symbol: true,
+        body: context.funcName,
+    };
+});
+
+// Limits, symbols
+defineFunction([
+    "\\coprod", "\\bigvee", "\\bigwedge", "\\biguplus", "\\bigcap",
+    "\\bigcup", "\\intop", "\\prod", "\\sum", "\\bigotimes",
+    "\\bigoplus", "\\bigodot", "\\bigsqcup", "\\smallint",
+], {
+    numArgs: 0,
+}, function(context) {
+    return {
+        type: "op",
+        limits: true,
+        symbol: true,
+        body: context.funcName,
+    };
+});
+
+// Fractions
+defineFunction([
+    "\\dfrac", "\\frac", "\\tfrac",
+    "\\dbinom", "\\binom", "\\tbinom",
+], {
+    numArgs: 2,
+    greediness: 2,
+}, function(context, args) {
+    var numer = args[0];
+    var denom = args[1];
+    var hasBarLine;
+    var leftDelim = null;
+    var rightDelim = null;
+    var size = "auto";
+
+    switch (context.funcName) {
+        case "\\dfrac":
+        case "\\frac":
+        case "\\tfrac":
+            hasBarLine = true;
+            break;
+        case "\\dbinom":
+        case "\\binom":
+        case "\\tbinom":
+            hasBarLine = false;
+            leftDelim = "(";
+            rightDelim = ")";
+            break;
+        default:
+            throw new Error("Unrecognized genfrac command");
+    }
+
+    switch (context.funcName) {
+        case "\\dfrac":
+        case "\\dbinom":
+            size = "display";
+            break;
+        case "\\tfrac":
+        case "\\tbinom":
+            size = "text";
+            break;
+    }
+
+    return {
+        type: "genfrac",
+        numer: numer,
+        denom: denom,
+        hasBarLine: hasBarLine,
+        leftDelim: leftDelim,
+        rightDelim: rightDelim,
+        size: size,
+    };
+});
+
+// Left and right overlap functions
+defineFunction(["\\llap", "\\rlap"], {
+    numArgs: 1,
+    allowedInText: true,
+}, function(context, args) {
+    var body = args[0];
+    return {
+        type: context.funcName.slice(1),
+        body: body,
+    };
+});
+
+// Delimiter functions
+defineFunction([
+    "\\bigl", "\\Bigl", "\\biggl", "\\Biggl",
+    "\\bigr", "\\Bigr", "\\biggr", "\\Biggr",
+    "\\bigm", "\\Bigm", "\\biggm", "\\Biggm",
+    "\\big",  "\\Big",  "\\bigg",  "\\Bigg",
+    "\\left", "\\right",
+], {
+    numArgs: 1,
+}, function(context, args) {
+    var delim = args[0];
+    if (!utils.contains(delimiters, delim.value)) {
+        throw new ParseError(
+            "Invalid delimiter: '" + delim.value + "' after '" +
+            context.funcName + "'", delim);
+    }
+
+    // \left and \right are caught somewhere in Parser.js, which is
+    // why this data doesn't match what is in buildHTML.
+    if (context.funcName === "\\left" || context.funcName === "\\right") {
+        return {
+            type: "leftright",
+            value: delim.value,
+        };
+    } else {
+        return {
+            type: "delimsizing",
+            size: delimiterSizes[context.funcName].size,
+            delimType: delimiterSizes[context.funcName].type,
+            value: delim.value,
+        };
+    }
+});
+
+// Sizing functions (handled in Parser.js explicitly, hence no handler)
+defineFunction([
+    "\\tiny", "\\scriptsize", "\\footnotesize", "\\small",
+    "\\normalsize", "\\large", "\\Large", "\\LARGE", "\\huge", "\\Huge",
+], 0, null);
+
+// Style changing functions (handled in Parser.js explicitly, hence no
+// handler)
+defineFunction([
+    "\\displaystyle", "\\textstyle", "\\scriptstyle",
+    "\\scriptscriptstyle",
+], 0, null);
+
+defineFunction([
+    // styles
+    "\\mathrm", "\\mathit", "\\mathbf",
+
+    // families
+    "\\mathbb", "\\mathcal", "\\mathfrak", "\\mathscr", "\\mathsf",
+    "\\mathtt",
+
+    // aliases
+    "\\Bbb", "\\bold", "\\frak",
+], {
+    numArgs: 1,
+    greediness: 2,
+}, function(context, args) {
+    var body = args[0];
+    var func = context.funcName;
+    if (func in fontAliases) {
+        func = fontAliases[func];
+    }
+    return {
+        type: "font",
+        font: func.slice(1),
+        body: body,
+    };
+});
+
+// Accents
+defineFunction([
+    "\\acute", "\\grave", "\\ddot", "\\tilde", "\\bar", "\\breve",
+    "\\check", "\\hat", "\\vec", "\\dot",
+    // We don't support expanding accents yet
+    // "\\widetilde", "\\widehat"
+], {
+    numArgs: 1,
+}, function(context, args) {
+    var base = args[0];
+    return {
+        type: "accent",
+        accent: context.funcName,
+        base: base,
+    };
+});
+
+// Infix generalized fractions
+defineFunction(["\\over", "\\choose"], {
+    numArgs: 0,
+    infix: true,
+}, function(context) {
+    var replaceWith;
+    switch (context.funcName) {
+        case "\\over":
+            replaceWith = "\\frac";
+            break;
+        case "\\choose":
+            replaceWith = "\\binom";
+            break;
+        default:
+            throw new Error("Unrecognized infix genfrac command");
+    }
+    return {
+        type: "infix",
+        replaceWith: replaceWith,
+        token: context.token,
+    };
+});
+
+// Row breaks for aligned data
+defineFunction(["\\\\", "\\cr"], {
+    numArgs: 0,
+    numOptionalArgs: 1,
+    argTypes: ["size"],
+}, function(context, args) {
+    var size = args[0];
+    return {
+        type: "cr",
+        size: size,
+    };
+});
+
+// Environment delimiters
+defineFunction(["\\begin", "\\end"], {
+    numArgs: 1,
+    argTypes: ["text"],
+}, function(context, args) {
+    var nameGroup = args[0];
+    if (nameGroup.type !== "ordgroup") {
+        throw new ParseError("Invalid environment name", nameGroup);
+    }
+    var name = "";
+    for (var i = 0; i < nameGroup.value.length; ++i) {
+        name += nameGroup.value[i].value;
+    }
+    return {
+        type: "environment",
+        name: name,
+        nameGroup: nameGroup,
+    };
+});
+
+},{"./ParseError":6,"./utils":25}],20:[function(require,module,exports){
+/**
+ * These objects store data about MathML nodes. This is the MathML equivalent
+ * of the types in domTree.js. Since MathML handles its own rendering, and
+ * since we're mainly using MathML to improve accessibility, we don't manage
+ * any of the styling state that the plain DOM nodes do.
+ *
+ * The `toNode` and `toMarkup` functions work simlarly to how they do in
+ * domTree.js, creating namespaced DOM nodes and HTML text markup respectively.
+ */
+
+var utils = require("./utils");
+
+/**
+ * This node represents a general purpose MathML node of any type. The
+ * constructor requires the type of node to create (for example, `"mo"` or
+ * `"mspace"`, corresponding to `<mo>` and `<mspace>` tags).
+ */
+function MathNode(type, children) {
+    this.type = type;
+    this.attributes = {};
+    this.children = children || [];
+}
+
+/**
+ * Sets an attribute on a MathML node. MathML depends on attributes to convey a
+ * semantic content, so this is used heavily.
+ */
+MathNode.prototype.setAttribute = function(name, value) {
+    this.attributes[name] = value;
+};
+
+/**
+ * Converts the math node into a MathML-namespaced DOM element.
+ */
+MathNode.prototype.toNode = function() {
+    var node = document.createElementNS(
+        "http://www.w3.org/1998/Math/MathML", this.type);
+
+    for (var attr in this.attributes) {
+        if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+            node.setAttribute(attr, this.attributes[attr]);
+        }
+    }
+
+    for (var i = 0; i < this.children.length; i++) {
+        node.appendChild(this.children[i].toNode());
+    }
+
+    return node;
+};
+
+/**
+ * Converts the math node into an HTML markup string.
+ */
+MathNode.prototype.toMarkup = function() {
+    var markup = "<" + this.type;
+
+    // Add the attributes
+    for (var attr in this.attributes) {
+        if (Object.prototype.hasOwnProperty.call(this.attributes, attr)) {
+            markup += " " + attr + "=\"";
+            markup += utils.escape(this.attributes[attr]);
+            markup += "\"";
+        }
+    }
+
+    markup += ">";
+
+    for (var i = 0; i < this.children.length; i++) {
+        markup += this.children[i].toMarkup();
+    }
+
+    markup += "</" + this.type + ">";
+
+    return markup;
+};
+
+/**
+ * This node represents a piece of text.
+ */
+function TextNode(text) {
+    this.text = text;
+}
+
+/**
+ * Converts the text node into a DOM text node.
+ */
+TextNode.prototype.toNode = function() {
+    return document.createTextNode(this.text);
+};
+
+/**
+ * Converts the text node into HTML markup (which is just the text itself).
+ */
+TextNode.prototype.toMarkup = function() {
+    return utils.escape(this.text);
+};
+
+module.exports = {
+    MathNode: MathNode,
+    TextNode: TextNode,
+};
+
+},{"./utils":25}],21:[function(require,module,exports){
+/**
+ * The resulting parse tree nodes of the parse tree.
+ *
+ * It is possible to provide position information, so that a ParseNode can
+ * fulfil a role similar to a Token in error reporting.
+ * For details on the corresponding properties see Token constructor.
+ * Providing such information can lead to better error reporting.
+ *
+ * @param {string}  type       type of node, like e.g. "ordgroup"
+ * @param {?object} value      type-specific representation of the node
+ * @param {string}  mode       parse mode in action for this node,
+ *                             "math" or "text"
+ * @param {Token=} firstToken  first token of the input for this node,
+ *                             will omit position information if unset
+ * @param {Token=} lastToken   last token of the input for this node,
+ *                             will default to firstToken if unset
+ */
+function ParseNode(type, value, mode, firstToken, lastToken) {
+    this.type = type;
+    this.value = value;
+    this.mode = mode;
+    if (firstToken && (!lastToken || lastToken.lexer === firstToken.lexer)) {
+        this.lexer = firstToken.lexer;
+        this.start = firstToken.start;
+        this.end = (lastToken || firstToken).end;
+    }
+}
+
+module.exports = {
+    ParseNode: ParseNode,
+};
+
+
+},{}],22:[function(require,module,exports){
+/**
+ * Provides a single function for parsing an expression using a Parser
+ * TODO(emily): Remove this
+ */
+
+var Parser = require("./Parser");
+
+/**
+ * Parses an expression using a Parser, then returns the parsed result.
+ */
+var parseTree = function(toParse, settings) {
+    if (!(typeof toParse === 'string' || toParse instanceof String)) {
+        throw new TypeError('KaTeX can only parse string typed expression');
+    }
+    var parser = new Parser(toParse, settings);
+
+    return parser.parse();
+};
+
+module.exports = parseTree;
+
+},{"./Parser":7}],23:[function(require,module,exports){
+/**
+ * This file holds a list of all no-argument functions and single-character
+ * symbols (like 'a' or ';').
+ *
+ * For each of the symbols, there are three properties they can have:
+ * - font (required): the font to be used for this symbol. Either "main" (the
+     normal font), or "ams" (the ams fonts).
+ * - group (required): the ParseNode group type the symbol should have (i.e.
+     "textord", "mathord", etc).
+     See https://github.com/Khan/KaTeX/wiki/Examining-TeX#group-types
+ * - replace: the character that this symbol or function should be
+ *   replaced with (i.e. "\phi" has a replace value of "\u03d5", the phi
+ *   character in the main font).
+ *
+ * The outermost map in the table indicates what mode the symbols should be
+ * accepted in (e.g. "math" or "text").
+ */
+
+module.exports = {
+    math: {},
+    text: {},
+};
+
+function defineSymbol(mode, font, group, replace, name) {
+    module.exports[mode][name] = {
+        font: font,
+        group: group,
+        replace: replace,
+    };
+}
+
+// Some abbreviations for commonly used strings.
+// This helps minify the code, and also spotting typos using jshint.
+
+// modes:
+var math = "math";
+var text = "text";
+
+// fonts:
+var main = "main";
+var ams = "ams";
+
+// groups:
+var accent = "accent";
+var bin = "bin";
+var close = "close";
+var inner = "inner";
+var mathord = "mathord";
+var op = "op";
+var open = "open";
+var punct = "punct";
+var rel = "rel";
+var spacing = "spacing";
+var textord = "textord";
+
+// Now comes the symbol table
+
+// Relation Symbols
+defineSymbol(math, main, rel, "\u2261", "\\equiv");
+defineSymbol(math, main, rel, "\u227a", "\\prec");
+defineSymbol(math, main, rel, "\u227b", "\\succ");
+defineSymbol(math, main, rel, "\u223c", "\\sim");
+defineSymbol(math, main, rel, "\u22a5", "\\perp");
+defineSymbol(math, main, rel, "\u2aaf", "\\preceq");
+defineSymbol(math, main, rel, "\u2ab0", "\\succeq");
+defineSymbol(math, main, rel, "\u2243", "\\simeq");
+defineSymbol(math, main, rel, "\u2223", "\\mid");
+defineSymbol(math, main, rel, "\u226a", "\\ll");
+defineSymbol(math, main, rel, "\u226b", "\\gg");
+defineSymbol(math, main, rel, "\u224d", "\\asymp");
+defineSymbol(math, main, rel, "\u2225", "\\parallel");
+defineSymbol(math, main, rel, "\u22c8", "\\bowtie");
+defineSymbol(math, main, rel, "\u2323", "\\smile");
+defineSymbol(math, main, rel, "\u2291", "\\sqsubseteq");
+defineSymbol(math, main, rel, "\u2292", "\\sqsupseteq");
+defineSymbol(math, main, rel, "\u2250", "\\doteq");
+defineSymbol(math, main, rel, "\u2322", "\\frown");
+defineSymbol(math, main, rel, "\u220b", "\\ni");
+defineSymbol(math, main, rel, "\u221d", "\\propto");
+defineSymbol(math, main, rel, "\u22a2", "\\vdash");
+defineSymbol(math, main, rel, "\u22a3", "\\dashv");
+defineSymbol(math, main, rel, "\u220b", "\\owns");
+
+// Punctuation
+defineSymbol(math, main, punct, "\u002e", "\\ldotp");
+defineSymbol(math, main, punct, "\u22c5", "\\cdotp");
+
+// Misc Symbols
+defineSymbol(math, main, textord, "\u0023", "\\#");
+defineSymbol(math, main, textord, "\u0026", "\\&");
+defineSymbol(math, main, textord, "\u2135", "\\aleph");
+defineSymbol(math, main, textord, "\u2200", "\\forall");
+defineSymbol(math, main, textord, "\u210f", "\\hbar");
+defineSymbol(math, main, textord, "\u2203", "\\exists");
+defineSymbol(math, main, textord, "\u2207", "\\nabla");
+defineSymbol(math, main, textord, "\u266d", "\\flat");
+defineSymbol(math, main, textord, "\u2113", "\\ell");
+defineSymbol(math, main, textord, "\u266e", "\\natural");
+defineSymbol(math, main, textord, "\u2663", "\\clubsuit");
+defineSymbol(math, main, textord, "\u2118", "\\wp");
+defineSymbol(math, main, textord, "\u266f", "\\sharp");
+defineSymbol(math, main, textord, "\u2662", "\\diamondsuit");
+defineSymbol(math, main, textord, "\u211c", "\\Re");
+defineSymbol(math, main, textord, "\u2661", "\\heartsuit");
+defineSymbol(math, main, textord, "\u2111", "\\Im");
+defineSymbol(math, main, textord, "\u2660", "\\spadesuit");
+
+// Math and Text
+defineSymbol(math, main, textord, "\u2020", "\\dag");
+defineSymbol(math, main, textord, "\u2021", "\\ddag");
+
+// Large Delimiters
+defineSymbol(math, main, close, "\u23b1", "\\rmoustache");
+defineSymbol(math, main, open, "\u23b0", "\\lmoustache");
+defineSymbol(math, main, close, "\u27ef", "\\rgroup");
+defineSymbol(math, main, open, "\u27ee", "\\lgroup");
+
+// Binary Operators
+defineSymbol(math, main, bin, "\u2213", "\\mp");
+defineSymbol(math, main, bin, "\u2296", "\\ominus");
+defineSymbol(math, main, bin, "\u228e", "\\uplus");
+defineSymbol(math, main, bin, "\u2293", "\\sqcap");
+defineSymbol(math, main, bin, "\u2217", "\\ast");
+defineSymbol(math, main, bin, "\u2294", "\\sqcup");
+defineSymbol(math, main, bin, "\u25ef", "\\bigcirc");
+defineSymbol(math, main, bin, "\u2219", "\\bullet");
+defineSymbol(math, main, bin, "\u2021", "\\ddagger");
+defineSymbol(math, main, bin, "\u2240", "\\wr");
+defineSymbol(math, main, bin, "\u2a3f", "\\amalg");
+
+// Arrow Symbols
+defineSymbol(math, main, rel, "\u27f5", "\\longleftarrow");
+defineSymbol(math, main, rel, "\u21d0", "\\Leftarrow");
+defineSymbol(math, main, rel, "\u27f8", "\\Longleftarrow");
+defineSymbol(math, main, rel, "\u27f6", "\\longrightarrow");
+defineSymbol(math, main, rel, "\u21d2", "\\Rightarrow");
+defineSymbol(math, main, rel, "\u27f9", "\\Longrightarrow");
+defineSymbol(math, main, rel, "\u2194", "\\leftrightarrow");
+defineSymbol(math, main, rel, "\u27f7", "\\longleftrightarrow");
+defineSymbol(math, main, rel, "\u21d4", "\\Leftrightarrow");
+defineSymbol(math, main, rel, "\u27fa", "\\Longleftrightarrow");
+defineSymbol(math, main, rel, "\u21a6", "\\mapsto");
+defineSymbol(math, main, rel, "\u27fc", "\\longmapsto");
+defineSymbol(math, main, rel, "\u2197", "\\nearrow");
+defineSymbol(math, main, rel, "\u21a9", "\\hookleftarrow");
+defineSymbol(math, main, rel, "\u21aa", "\\hookrightarrow");
+defineSymbol(math, main, rel, "\u2198", "\\searrow");
+defineSymbol(math, main, rel, "\u21bc", "\\leftharpoonup");
+defineSymbol(math, main, rel, "\u21c0", "\\rightharpoonup");
+defineSymbol(math, main, rel, "\u2199", "\\swarrow");
+defineSymbol(math, main, rel, "\u21bd", "\\leftharpoondown");
+defineSymbol(math, main, rel, "\u21c1", "\\rightharpoondown");
+defineSymbol(math, main, rel, "\u2196", "\\nwarrow");
+defineSymbol(math, main, rel, "\u21cc", "\\rightleftharpoons");
+
+// AMS Negated Binary Relations
+defineSymbol(math, ams, rel, "\u226e", "\\nless");
+defineSymbol(math, ams, rel, "\ue010", "\\nleqslant");
+defineSymbol(math, ams, rel, "\ue011", "\\nleqq");
+defineSymbol(math, ams, rel, "\u2a87", "\\lneq");
+defineSymbol(math, ams, rel, "\u2268", "\\lneqq");
+defineSymbol(math, ams, rel, "\ue00c", "\\lvertneqq");
+defineSymbol(math, ams, rel, "\u22e6", "\\lnsim");
+defineSymbol(math, ams, rel, "\u2a89", "\\lnapprox");
+defineSymbol(math, ams, rel, "\u2280", "\\nprec");
+defineSymbol(math, ams, rel, "\u22e0", "\\npreceq");
+defineSymbol(math, ams, rel, "\u22e8", "\\precnsim");
+defineSymbol(math, ams, rel, "\u2ab9", "\\precnapprox");
+defineSymbol(math, ams, rel, "\u2241", "\\nsim");
+defineSymbol(math, ams, rel, "\ue006", "\\nshortmid");
+defineSymbol(math, ams, rel, "\u2224", "\\nmid");
+defineSymbol(math, ams, rel, "\u22ac", "\\nvdash");
+defineSymbol(math, ams, rel, "\u22ad", "\\nvDash");
+defineSymbol(math, ams, rel, "\u22ea", "\\ntriangleleft");
+defineSymbol(math, ams, rel, "\u22ec", "\\ntrianglelefteq");
+defineSymbol(math, ams, rel, "\u228a", "\\subsetneq");
+defineSymbol(math, ams, rel, "\ue01a", "\\varsubsetneq");
+defineSymbol(math, ams, rel, "\u2acb", "\\subsetneqq");
+defineSymbol(math, ams, rel, "\ue017", "\\varsubsetneqq");
+defineSymbol(math, ams, rel, "\u226f", "\\ngtr");
+defineSymbol(math, ams, rel, "\ue00f", "\\ngeqslant");
+defineSymbol(math, ams, rel, "\ue00e", "\\ngeqq");
+defineSymbol(math, ams, rel, "\u2a88", "\\gneq");
+defineSymbol(math, ams, rel, "\u2269", "\\gneqq");
+defineSymbol(math, ams, rel, "\ue00d", "\\gvertneqq");
+defineSymbol(math, ams, rel, "\u22e7", "\\gnsim");
+defineSymbol(math, ams, rel, "\u2a8a", "\\gnapprox");
+defineSymbol(math, ams, rel, "\u2281", "\\nsucc");
+defineSymbol(math, ams, rel, "\u22e1", "\\nsucceq");
+defineSymbol(math, ams, rel, "\u22e9", "\\succnsim");
+defineSymbol(math, ams, rel, "\u2aba", "\\succnapprox");
+defineSymbol(math, ams, rel, "\u2246", "\\ncong");
+defineSymbol(math, ams, rel, "\ue007", "\\nshortparallel");
+defineSymbol(math, ams, rel, "\u2226", "\\nparallel");
+defineSymbol(math, ams, rel, "\u22af", "\\nVDash");
+defineSymbol(math, ams, rel, "\u22eb", "\\ntriangleright");
+defineSymbol(math, ams, rel, "\u22ed", "\\ntrianglerighteq");
+defineSymbol(math, ams, rel, "\ue018", "\\nsupseteqq");
+defineSymbol(math, ams, rel, "\u228b", "\\supsetneq");
+defineSymbol(math, ams, rel, "\ue01b", "\\varsupsetneq");
+defineSymbol(math, ams, rel, "\u2acc", "\\supsetneqq");
+defineSymbol(math, ams, rel, "\ue019", "\\varsupsetneqq");
+defineSymbol(math, ams, rel, "\u22ae", "\\nVdash");
+defineSymbol(math, ams, rel, "\u2ab5", "\\precneqq");
+defineSymbol(math, ams, rel, "\u2ab6", "\\succneqq");
+defineSymbol(math, ams, rel, "\ue016", "\\nsubseteqq");
+defineSymbol(math, ams, bin, "\u22b4", "\\unlhd");
+defineSymbol(math, ams, bin, "\u22b5", "\\unrhd");
+
+// AMS Negated Arrows
+defineSymbol(math, ams, rel, "\u219a", "\\nleftarrow");
+defineSymbol(math, ams, rel, "\u219b", "\\nrightarrow");
+defineSymbol(math, ams, rel, "\u21cd", "\\nLeftarrow");
+defineSymbol(math, ams, rel, "\u21cf", "\\nRightarrow");
+defineSymbol(math, ams, rel, "\u21ae", "\\nleftrightarrow");
+defineSymbol(math, ams, rel, "\u21ce", "\\nLeftrightarrow");
+
+// AMS Misc
+defineSymbol(math, ams, rel, "\u25b3", "\\vartriangle");
+defineSymbol(math, ams, textord, "\u210f", "\\hslash");
+defineSymbol(math, ams, textord, "\u25bd", "\\triangledown");
+defineSymbol(math, ams, textord, "\u25ca", "\\lozenge");
+defineSymbol(math, ams, textord, "\u24c8", "\\circledS");
+defineSymbol(math, ams, textord, "\u00ae", "\\circledR");
+defineSymbol(math, ams, textord, "\u2221", "\\measuredangle");
+defineSymbol(math, ams, textord, "\u2204", "\\nexists");
+defineSymbol(math, ams, textord, "\u2127", "\\mho");
+defineSymbol(math, ams, textord, "\u2132", "\\Finv");
+defineSymbol(math, ams, textord, "\u2141", "\\Game");
+defineSymbol(math, ams, textord, "\u006b", "\\Bbbk");
+defineSymbol(math, ams, textord, "\u2035", "\\backprime");
+defineSymbol(math, ams, textord, "\u25b2", "\\blacktriangle");
+defineSymbol(math, ams, textord, "\u25bc", "\\blacktriangledown");
+defineSymbol(math, ams, textord, "\u25a0", "\\blacksquare");
+defineSymbol(math, ams, textord, "\u29eb", "\\blacklozenge");
+defineSymbol(math, ams, textord, "\u2605", "\\bigstar");
+defineSymbol(math, ams, textord, "\u2222", "\\sphericalangle");
+defineSymbol(math, ams, textord, "\u2201", "\\complement");
+defineSymbol(math, ams, textord, "\u00f0", "\\eth");
+defineSymbol(math, ams, textord, "\u2571", "\\diagup");
+defineSymbol(math, ams, textord, "\u2572", "\\diagdown");
+defineSymbol(math, ams, textord, "\u25a1", "\\square");
+defineSymbol(math, ams, textord, "\u25a1", "\\Box");
+defineSymbol(math, ams, textord, "\u25ca", "\\Diamond");
+defineSymbol(math, ams, textord, "\u00a5", "\\yen");
+defineSymbol(math, ams, textord, "\u2713", "\\checkmark");
+
+// AMS Hebrew
+defineSymbol(math, ams, textord, "\u2136", "\\beth");
+defineSymbol(math, ams, textord, "\u2138", "\\daleth");
+defineSymbol(math, ams, textord, "\u2137", "\\gimel");
+
+// AMS Greek
+defineSymbol(math, ams, textord, "\u03dd", "\\digamma");
+defineSymbol(math, ams, textord, "\u03f0", "\\varkappa");
+
+// AMS Delimiters
+defineSymbol(math, ams, open, "\u250c", "\\ulcorner");
+defineSymbol(math, ams, close, "\u2510", "\\urcorner");
+defineSymbol(math, ams, open, "\u2514", "\\llcorner");
+defineSymbol(math, ams, close, "\u2518", "\\lrcorner");
+
+// AMS Binary Relations
+defineSymbol(math, ams, rel, "\u2266", "\\leqq");
+defineSymbol(math, ams, rel, "\u2a7d", "\\leqslant");
+defineSymbol(math, ams, rel, "\u2a95", "\\eqslantless");
+defineSymbol(math, ams, rel, "\u2272", "\\lesssim");
+defineSymbol(math, ams, rel, "\u2a85", "\\lessapprox");
+defineSymbol(math, ams, rel, "\u224a", "\\approxeq");
+defineSymbol(math, ams, bin, "\u22d6", "\\lessdot");
+defineSymbol(math, ams, rel, "\u22d8", "\\lll");
+defineSymbol(math, ams, rel, "\u2276", "\\lessgtr");
+defineSymbol(math, ams, rel, "\u22da", "\\lesseqgtr");
+defineSymbol(math, ams, rel, "\u2a8b", "\\lesseqqgtr");
+defineSymbol(math, ams, rel, "\u2251", "\\doteqdot");
+defineSymbol(math, ams, rel, "\u2253", "\\risingdotseq");
+defineSymbol(math, ams, rel, "\u2252", "\\fallingdotseq");
+defineSymbol(math, ams, rel, "\u223d", "\\backsim");
+defineSymbol(math, ams, rel, "\u22cd", "\\backsimeq");
+defineSymbol(math, ams, rel, "\u2ac5", "\\subseteqq");
+defineSymbol(math, ams, rel, "\u22d0", "\\Subset");
+defineSymbol(math, ams, rel, "\u228f", "\\sqsubset");
+defineSymbol(math, ams, rel, "\u227c", "\\preccurlyeq");
+defineSymbol(math, ams, rel, "\u22de", "\\curlyeqprec");
+defineSymbol(math, ams, rel, "\u227e", "\\precsim");
+defineSymbol(math, ams, rel, "\u2ab7", "\\precapprox");
+defineSymbol(math, ams, rel, "\u22b2", "\\vartriangleleft");
+defineSymbol(math, ams, rel, "\u22b4", "\\trianglelefteq");
+defineSymbol(math, ams, rel, "\u22a8", "\\vDash");
+defineSymbol(math, ams, rel, "\u22aa", "\\Vvdash");
+defineSymbol(math, ams, rel, "\u2323", "\\smallsmile");
+defineSymbol(math, ams, rel, "\u2322", "\\smallfrown");
+defineSymbol(math, ams, rel, "\u224f", "\\bumpeq");
+defineSymbol(math, ams, rel, "\u224e", "\\Bumpeq");
+defineSymbol(math, ams, rel, "\u2267", "\\geqq");
+defineSymbol(math, ams, rel, "\u2a7e", "\\geqslant");
+defineSymbol(math, ams, rel, "\u2a96", "\\eqslantgtr");
+defineSymbol(math, ams, rel, "\u2273", "\\gtrsim");
+defineSymbol(math, ams, rel, "\u2a86", "\\gtrapprox");
+defineSymbol(math, ams, bin, "\u22d7", "\\gtrdot");
+defineSymbol(math, ams, rel, "\u22d9", "\\ggg");
+defineSymbol(math, ams, rel, "\u2277", "\\gtrless");
+defineSymbol(math, ams, rel, "\u22db", "\\gtreqless");
+defineSymbol(math, ams, rel, "\u2a8c", "\\gtreqqless");
+defineSymbol(math, ams, rel, "\u2256", "\\eqcirc");
+defineSymbol(math, ams, rel, "\u2257", "\\circeq");
+defineSymbol(math, ams, rel, "\u225c", "\\triangleq");
+defineSymbol(math, ams, rel, "\u223c", "\\thicksim");
+defineSymbol(math, ams, rel, "\u2248", "\\thickapprox");
+defineSymbol(math, ams, rel, "\u2ac6", "\\supseteqq");
+defineSymbol(math, ams, rel, "\u22d1", "\\Supset");
+defineSymbol(math, ams, rel, "\u2290", "\\sqsupset");
+defineSymbol(math, ams, rel, "\u227d", "\\succcurlyeq");
+defineSymbol(math, ams, rel, "\u22df", "\\curlyeqsucc");
+defineSymbol(math, ams, rel, "\u227f", "\\succsim");
+defineSymbol(math, ams, rel, "\u2ab8", "\\succapprox");
+defineSymbol(math, ams, rel, "\u22b3", "\\vartriangleright");
+defineSymbol(math, ams, rel, "\u22b5", "\\trianglerighteq");
+defineSymbol(math, ams, rel, "\u22a9", "\\Vdash");
+defineSymbol(math, ams, rel, "\u2223", "\\shortmid");
+defineSymbol(math, ams, rel, "\u2225", "\\shortparallel");
+defineSymbol(math, ams, rel, "\u226c", "\\between");
+defineSymbol(math, ams, rel, "\u22d4", "\\pitchfork");
+defineSymbol(math, ams, rel, "\u221d", "\\varpropto");
+defineSymbol(math, ams, rel, "\u25c0", "\\blacktriangleleft");
+defineSymbol(math, ams, rel, "\u2234", "\\therefore");
+defineSymbol(math, ams, rel, "\u220d", "\\backepsilon");
+defineSymbol(math, ams, rel, "\u25b6", "\\blacktriangleright");
+defineSymbol(math, ams, rel, "\u2235", "\\because");
+defineSymbol(math, ams, rel, "\u22d8", "\\llless");
+defineSymbol(math, ams, rel, "\u22d9", "\\gggtr");
+defineSymbol(math, ams, bin, "\u22b2", "\\lhd");
+defineSymbol(math, ams, bin, "\u22b3", "\\rhd");
+defineSymbol(math, ams, rel, "\u2242", "\\eqsim");
+defineSymbol(math, main, rel, "\u22c8", "\\Join");
+defineSymbol(math, ams, rel, "\u2251", "\\Doteq");
+
+// AMS Binary Operators
+defineSymbol(math, ams, bin, "\u2214", "\\dotplus");
+defineSymbol(math, ams, bin, "\u2216", "\\smallsetminus");
+defineSymbol(math, ams, bin, "\u22d2", "\\Cap");
+defineSymbol(math, ams, bin, "\u22d3", "\\Cup");
+defineSymbol(math, ams, bin, "\u2a5e", "\\doublebarwedge");
+defineSymbol(math, ams, bin, "\u229f", "\\boxminus");
+defineSymbol(math, ams, bin, "\u229e", "\\boxplus");
+defineSymbol(math, ams, bin, "\u22c7", "\\divideontimes");
+defineSymbol(math, ams, bin, "\u22c9", "\\ltimes");
+defineSymbol(math, ams, bin, "\u22ca", "\\rtimes");
+defineSymbol(math, ams, bin, "\u22cb", "\\leftthreetimes");
+defineSymbol(math, ams, bin, "\u22cc", "\\rightthreetimes");
+defineSymbol(math, ams, bin, "\u22cf", "\\curlywedge");
+defineSymbol(math, ams, bin, "\u22ce", "\\curlyvee");
+defineSymbol(math, ams, bin, "\u229d", "\\circleddash");
+defineSymbol(math, ams, bin, "\u229b", "\\circledast");
+defineSymbol(math, ams, bin, "\u22c5", "\\centerdot");
+defineSymbol(math, ams, bin, "\u22ba", "\\intercal");
+defineSymbol(math, ams, bin, "\u22d2", "\\doublecap");
+defineSymbol(math, ams, bin, "\u22d3", "\\doublecup");
+defineSymbol(math, ams, bin, "\u22a0", "\\boxtimes");
+
+// AMS Arrows
+defineSymbol(math, ams, rel, "\u21e2", "\\dashrightarrow");
+defineSymbol(math, ams, rel, "\u21e0", "\\dashleftarrow");
+defineSymbol(math, ams, rel, "\u21c7", "\\leftleftarrows");
+defineSymbol(math, ams, rel, "\u21c6", "\\leftrightarrows");
+defineSymbol(math, ams, rel, "\u21da", "\\Lleftarrow");
+defineSymbol(math, ams, rel, "\u219e", "\\twoheadleftarrow");
+defineSymbol(math, ams, rel, "\u21a2", "\\leftarrowtail");
+defineSymbol(math, ams, rel, "\u21ab", "\\looparrowleft");
+defineSymbol(math, ams, rel, "\u21cb", "\\leftrightharpoons");
+defineSymbol(math, ams, rel, "\u21b6", "\\curvearrowleft");
+defineSymbol(math, ams, rel, "\u21ba", "\\circlearrowleft");
+defineSymbol(math, ams, rel, "\u21b0", "\\Lsh");
+defineSymbol(math, ams, rel, "\u21c8", "\\upuparrows");
+defineSymbol(math, ams, rel, "\u21bf", "\\upharpoonleft");
+defineSymbol(math, ams, rel, "\u21c3", "\\downharpoonleft");
+defineSymbol(math, ams, rel, "\u22b8", "\\multimap");
+defineSymbol(math, ams, rel, "\u21ad", "\\leftrightsquigarrow");
+defineSymbol(math, ams, rel, "\u21c9", "\\rightrightarrows");
+defineSymbol(math, ams, rel, "\u21c4", "\\rightleftarrows");
+defineSymbol(math, ams, rel, "\u21a0", "\\twoheadrightarrow");
+defineSymbol(math, ams, rel, "\u21a3", "\\rightarrowtail");
+defineSymbol(math, ams, rel, "\u21ac", "\\looparrowright");
+defineSymbol(math, ams, rel, "\u21b7", "\\curvearrowright");
+defineSymbol(math, ams, rel, "\u21bb", "\\circlearrowright");
+defineSymbol(math, ams, rel, "\u21b1", "\\Rsh");
+defineSymbol(math, ams, rel, "\u21ca", "\\downdownarrows");
+defineSymbol(math, ams, rel, "\u21be", "\\upharpoonright");
+defineSymbol(math, ams, rel, "\u21c2", "\\downharpoonright");
+defineSymbol(math, ams, rel, "\u21dd", "\\rightsquigarrow");
+defineSymbol(math, ams, rel, "\u21dd", "\\leadsto");
+defineSymbol(math, ams, rel, "\u21db", "\\Rrightarrow");
+defineSymbol(math, ams, rel, "\u21be", "\\restriction");
+
+defineSymbol(math, main, textord, "\u2018", "`");
+defineSymbol(math, main, textord, "$", "\\$");
+defineSymbol(math, main, textord, "%", "\\%");
+defineSymbol(math, main, textord, "_", "\\_");
+defineSymbol(math, main, textord, "\u2220", "\\angle");
+defineSymbol(math, main, textord, "\u221e", "\\infty");
+defineSymbol(math, main, textord, "\u2032", "\\prime");
+defineSymbol(math, main, textord, "\u25b3", "\\triangle");
+defineSymbol(math, main, textord, "\u0393", "\\Gamma");
+defineSymbol(math, main, textord, "\u0394", "\\Delta");
+defineSymbol(math, main, textord, "\u0398", "\\Theta");
+defineSymbol(math, main, textord, "\u039b", "\\Lambda");
+defineSymbol(math, main, textord, "\u039e", "\\Xi");
+defineSymbol(math, main, textord, "\u03a0", "\\Pi");
+defineSymbol(math, main, textord, "\u03a3", "\\Sigma");
+defineSymbol(math, main, textord, "\u03a5", "\\Upsilon");
+defineSymbol(math, main, textord, "\u03a6", "\\Phi");
+defineSymbol(math, main, textord, "\u03a8", "\\Psi");
+defineSymbol(math, main, textord, "\u03a9", "\\Omega");
+defineSymbol(math, main, textord, "\u00ac", "\\neg");
+defineSymbol(math, main, textord, "\u00ac", "\\lnot");
+defineSymbol(math, main, textord, "\u22a4", "\\top");
+defineSymbol(math, main, textord, "\u22a5", "\\bot");
+defineSymbol(math, main, textord, "\u2205", "\\emptyset");
+defineSymbol(math, ams, textord, "\u2205", "\\varnothing");
+defineSymbol(math, main, mathord, "\u03b1", "\\alpha");
+defineSymbol(math, main, mathord, "\u03b2", "\\beta");
+defineSymbol(math, main, mathord, "\u03b3", "\\gamma");
+defineSymbol(math, main, mathord, "\u03b4", "\\delta");
+defineSymbol(math, main, mathord, "\u03f5", "\\epsilon");
+defineSymbol(math, main, mathord, "\u03b6", "\\zeta");
+defineSymbol(math, main, mathord, "\u03b7", "\\eta");
+defineSymbol(math, main, mathord, "\u03b8", "\\theta");
+defineSymbol(math, main, mathord, "\u03b9", "\\iota");
+defineSymbol(math, main, mathord, "\u03ba", "\\kappa");
+defineSymbol(math, main, mathord, "\u03bb", "\\lambda");
+defineSymbol(math, main, mathord, "\u03bc", "\\mu");
+defineSymbol(math, main, mathord, "\u03bd", "\\nu");
+defineSymbol(math, main, mathord, "\u03be", "\\xi");
+defineSymbol(math, main, mathord, "o", "\\omicron");
+defineSymbol(math, main, mathord, "\u03c0", "\\pi");
+defineSymbol(math, main, mathord, "\u03c1", "\\rho");
+defineSymbol(math, main, mathord, "\u03c3", "\\sigma");
+defineSymbol(math, main, mathord, "\u03c4", "\\tau");
+defineSymbol(math, main, mathord, "\u03c5", "\\upsilon");
+defineSymbol(math, main, mathord, "\u03d5", "\\phi");
+defineSymbol(math, main, mathord, "\u03c7", "\\chi");
+defineSymbol(math, main, mathord, "\u03c8", "\\psi");
+defineSymbol(math, main, mathord, "\u03c9", "\\omega");
+defineSymbol(math, main, mathord, "\u03b5", "\\varepsilon");
+defineSymbol(math, main, mathord, "\u03d1", "\\vartheta");
+defineSymbol(math, main, mathord, "\u03d6", "\\varpi");
+defineSymbol(math, main, mathord, "\u03f1", "\\varrho");
+defineSymbol(math, main, mathord, "\u03c2", "\\varsigma");
+defineSymbol(math, main, mathord, "\u03c6", "\\varphi");
+defineSymbol(math, main, bin, "\u2217", "*");
+defineSymbol(math, main, bin, "+", "+");
+defineSymbol(math, main, bin, "\u2212", "-");
+defineSymbol(math, main, bin, "\u22c5", "\\cdot");
+defineSymbol(math, main, bin, "\u2218", "\\circ");
+defineSymbol(math, main, bin, "\u00f7", "\\div");
+defineSymbol(math, main, bin, "\u00b1", "\\pm");
+defineSymbol(math, main, bin, "\u00d7", "\\times");
+defineSymbol(math, main, bin, "\u2229", "\\cap");
+defineSymbol(math, main, bin, "\u222a", "\\cup");
+defineSymbol(math, main, bin, "\u2216", "\\setminus");
+defineSymbol(math, main, bin, "\u2227", "\\land");
+defineSymbol(math, main, bin, "\u2228", "\\lor");
+defineSymbol(math, main, bin, "\u2227", "\\wedge");
+defineSymbol(math, main, bin, "\u2228", "\\vee");
+defineSymbol(math, main, textord, "\u221a", "\\surd");
+defineSymbol(math, main, open, "(", "(");
+defineSymbol(math, main, open, "[", "[");
+defineSymbol(math, main, open, "\u27e8", "\\langle");
+defineSymbol(math, main, open, "\u2223", "\\lvert");
+defineSymbol(math, main, open, "\u2225", "\\lVert");
+defineSymbol(math, main, close, ")", ")");
+defineSymbol(math, main, close, "]", "]");
+defineSymbol(math, main, close, "?", "?");
+defineSymbol(math, main, close, "!", "!");
+defineSymbol(math, main, close, "\u27e9", "\\rangle");
+defineSymbol(math, main, close, "\u2223", "\\rvert");
+defineSymbol(math, main, close, "\u2225", "\\rVert");
+defineSymbol(math, main, rel, "=", "=");
+defineSymbol(math, main, rel, "<", "<");
+defineSymbol(math, main, rel, ">", ">");
+defineSymbol(math, main, rel, ":", ":");
+defineSymbol(math, main, rel, "\u2248", "\\approx");
+defineSymbol(math, main, rel, "\u2245", "\\cong");
+defineSymbol(math, main, rel, "\u2265", "\\ge");
+defineSymbol(math, main, rel, "\u2265", "\\geq");
+defineSymbol(math, main, rel, "\u2190", "\\gets");
+defineSymbol(math, main, rel, ">", "\\gt");
+defineSymbol(math, main, rel, "\u2208", "\\in");
+defineSymbol(math, main, rel, "\u2209", "\\notin");
+defineSymbol(math, main, rel, "\u2282", "\\subset");
+defineSymbol(math, main, rel, "\u2283", "\\supset");
+defineSymbol(math, main, rel, "\u2286", "\\subseteq");
+defineSymbol(math, main, rel, "\u2287", "\\supseteq");
+defineSymbol(math, ams, rel, "\u2288", "\\nsubseteq");
+defineSymbol(math, ams, rel, "\u2289", "\\nsupseteq");
+defineSymbol(math, main, rel, "\u22a8", "\\models");
+defineSymbol(math, main, rel, "\u2190", "\\leftarrow");
+defineSymbol(math, main, rel, "\u2264", "\\le");
+defineSymbol(math, main, rel, "\u2264", "\\leq");
+defineSymbol(math, main, rel, "<", "\\lt");
+defineSymbol(math, main, rel, "\u2260", "\\ne");
+defineSymbol(math, main, rel, "\u2260", "\\neq");
+defineSymbol(math, main, rel, "\u2192", "\\rightarrow");
+defineSymbol(math, main, rel, "\u2192", "\\to");
+defineSymbol(math, ams, rel, "\u2271", "\\ngeq");
+defineSymbol(math, ams, rel, "\u2270", "\\nleq");
+defineSymbol(math, main, spacing, null, "\\!");
+defineSymbol(math, main, spacing, "\u00a0", "\\ ");
+defineSymbol(math, main, spacing, "\u00a0", "~");
+defineSymbol(math, main, spacing, null, "\\,");
+defineSymbol(math, main, spacing, null, "\\:");
+defineSymbol(math, main, spacing, null, "\\;");
+defineSymbol(math, main, spacing, null, "\\enspace");
+defineSymbol(math, main, spacing, null, "\\qquad");
+defineSymbol(math, main, spacing, null, "\\quad");
+defineSymbol(math, main, spacing, "\u00a0", "\\space");
+defineSymbol(math, main, punct, ",", ",");
+defineSymbol(math, main, punct, ";", ";");
+defineSymbol(math, main, punct, ":", "\\colon");
+defineSymbol(math, ams, bin, "\u22bc", "\\barwedge");
+defineSymbol(math, ams, bin, "\u22bb", "\\veebar");
+defineSymbol(math, main, bin, "\u2299", "\\odot");
+defineSymbol(math, main, bin, "\u2295", "\\oplus");
+defineSymbol(math, main, bin, "\u2297", "\\otimes");
+defineSymbol(math, main, textord, "\u2202", "\\partial");
+defineSymbol(math, main, bin, "\u2298", "\\oslash");
+defineSymbol(math, ams, bin, "\u229a", "\\circledcirc");
+defineSymbol(math, ams, bin, "\u22a1", "\\boxdot");
+defineSymbol(math, main, bin, "\u25b3", "\\bigtriangleup");
+defineSymbol(math, main, bin, "\u25bd", "\\bigtriangledown");
+defineSymbol(math, main, bin, "\u2020", "\\dagger");
+defineSymbol(math, main, bin, "\u22c4", "\\diamond");
+defineSymbol(math, main, bin, "\u22c6", "\\star");
+defineSymbol(math, main, bin, "\u25c3", "\\triangleleft");
+defineSymbol(math, main, bin, "\u25b9", "\\triangleright");
+defineSymbol(math, main, open, "{", "\\{");
+defineSymbol(math, main, close, "}", "\\}");
+defineSymbol(math, main, open, "{", "\\lbrace");
+defineSymbol(math, main, close, "}", "\\rbrace");
+defineSymbol(math, main, open, "[", "\\lbrack");
+defineSymbol(math, main, close, "]", "\\rbrack");
+defineSymbol(math, main, open, "\u230a", "\\lfloor");
+defineSymbol(math, main, close, "\u230b", "\\rfloor");
+defineSymbol(math, main, open, "\u2308", "\\lceil");
+defineSymbol(math, main, close, "\u2309", "\\rceil");
+defineSymbol(math, main, textord, "\\", "\\backslash");
+defineSymbol(math, main, textord, "\u2223", "|");
+defineSymbol(math, main, textord, "\u2223", "\\vert");
+defineSymbol(math, main, textord, "\u2225", "\\|");
+defineSymbol(math, main, textord, "\u2225", "\\Vert");
+defineSymbol(math, main, rel, "\u2191", "\\uparrow");
+defineSymbol(math, main, rel, "\u21d1", "\\Uparrow");
+defineSymbol(math, main, rel, "\u2193", "\\downarrow");
+defineSymbol(math, main, rel, "\u21d3", "\\Downarrow");
+defineSymbol(math, main, rel, "\u2195", "\\updownarrow");
+defineSymbol(math, main, rel, "\u21d5", "\\Updownarrow");
+defineSymbol(math, math, op, "\u2210", "\\coprod");
+defineSymbol(math, math, op, "\u22c1", "\\bigvee");
+defineSymbol(math, math, op, "\u22c0", "\\bigwedge");
+defineSymbol(math, math, op, "\u2a04", "\\biguplus");
+defineSymbol(math, math, op, "\u22c2", "\\bigcap");
+defineSymbol(math, math, op, "\u22c3", "\\bigcup");
+defineSymbol(math, math, op, "\u222b", "\\int");
+defineSymbol(math, math, op, "\u222b", "\\intop");
+defineSymbol(math, math, op, "\u222c", "\\iint");
+defineSymbol(math, math, op, "\u222d", "\\iiint");
+defineSymbol(math, math, op, "\u220f", "\\prod");
+defineSymbol(math, math, op, "\u2211", "\\sum");
+defineSymbol(math, math, op, "\u2a02", "\\bigotimes");
+defineSymbol(math, math, op, "\u2a01", "\\bigoplus");
+defineSymbol(math, math, op, "\u2a00", "\\bigodot");
+defineSymbol(math, math, op, "\u222e", "\\oint");
+defineSymbol(math, math, op, "\u2a06", "\\bigsqcup");
+defineSymbol(math, math, op, "\u222b", "\\smallint");
+defineSymbol(math, main, inner, "\u2026", "\\ldots");
+defineSymbol(math, main, inner, "\u22ef", "\\cdots");
+defineSymbol(math, main, inner, "\u22f1", "\\ddots");
+defineSymbol(math, main, textord, "\u22ee", "\\vdots");
+defineSymbol(math, main, accent, "\u00b4", "\\acute");
+defineSymbol(math, main, accent, "\u0060", "\\grave");
+defineSymbol(math, main, accent, "\u00a8", "\\ddot");
+defineSymbol(math, main, accent, "\u007e", "\\tilde");
+defineSymbol(math, main, accent, "\u00af", "\\bar");
+defineSymbol(math, main, accent, "\u02d8", "\\breve");
+defineSymbol(math, main, accent, "\u02c7", "\\check");
+defineSymbol(math, main, accent, "\u005e", "\\hat");
+defineSymbol(math, main, accent, "\u20d7", "\\vec");
+defineSymbol(math, main, accent, "\u02d9", "\\dot");
+defineSymbol(math, main, mathord, "\u0131", "\\imath");
+defineSymbol(math, main, mathord, "\u0237", "\\jmath");
+
+defineSymbol(text, main, textord, "\u2013", "--");
+defineSymbol(text, main, textord, "\u2014", "---");
+defineSymbol(text, main, textord, "\u2018", "`");
+defineSymbol(text, main, textord, "\u2019", "'");
+defineSymbol(text, main, textord, "\u201c", "``");
+defineSymbol(text, main, textord, "\u201d", "''");
+defineSymbol(math, main, textord, "\u00b0", "\\degree");
+defineSymbol(text, main, textord, "\u00b0", "\\degree");
+defineSymbol(math, main, mathord, "\u00a3", "\\pounds");
+defineSymbol(math, ams, textord, "\u2720", "\\maltese");
+defineSymbol(text, ams, textord, "\u2720", "\\maltese");
+
+defineSymbol(text, main, spacing, "\u00a0", "\\ ");
+defineSymbol(text, main, spacing, "\u00a0", " ");
+defineSymbol(text, main, spacing, "\u00a0", "~");
+
+// There are lots of symbols which are the same, so we add them in afterwards.
+var i;
+var ch;
+
+// All of these are textords in math mode
+var mathTextSymbols = "0123456789/@.\"";
+for (i = 0; i < mathTextSymbols.length; i++) {
+    ch = mathTextSymbols.charAt(i);
+    defineSymbol(math, main, textord, ch, ch);
+}
+
+// All of these are textords in text mode
+var textSymbols = "0123456789!@*()-=+[]\";:?/.,";
+for (i = 0; i < textSymbols.length; i++) {
+    ch = textSymbols.charAt(i);
+    defineSymbol(text, main, textord, ch, ch);
+}
+
+// All of these are textords in text mode, and mathords in math mode
+var letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+for (i = 0; i < letters.length; i++) {
+    ch = letters.charAt(i);
+    defineSymbol(math, main, mathord, ch, ch);
+    defineSymbol(text, main, textord, ch, ch);
+}
+
+// Latin-1 letters
+for (i = 0x00C0; i <= 0x00D6; i++) {
+    ch = String.fromCharCode(i);
+    defineSymbol(text, main, textord, ch, ch);
+}
+
+for (i = 0x00D8; i <= 0x00F6; i++) {
+    ch = String.fromCharCode(i);
+    defineSymbol(text, main, textord, ch, ch);
+}
+
+for (i = 0x00F8; i <= 0x00FF; i++) {
+    ch = String.fromCharCode(i);
+    defineSymbol(text, main, textord, ch, ch);
+}
+
+// Cyrillic
+for (i = 0x0410; i <= 0x044F; i++) {
+    ch = String.fromCharCode(i);
+    defineSymbol(text, main, textord, ch, ch);
+}
+
+},{}],24:[function(require,module,exports){
+var hangulRegex = /[\uAC00-\uD7AF]/;
+
+// This regex combines
+// - Hiragana: [\u3040-\u309F]
+// - Katakana: [\u30A0-\u30FF]
+// - CJK ideograms: [\u4E00-\u9FAF]
+// - Hangul syllables: [\uAC00-\uD7AF]
+// Notably missing are halfwidth Katakana and Romanji glyphs.
+var cjkRegex =
+    /[\u3040-\u309F]|[\u30A0-\u30FF]|[\u4E00-\u9FAF]|[\uAC00-\uD7AF]/;
+
+module.exports = {
+    cjkRegex: cjkRegex,
+    hangulRegex: hangulRegex,
+};
+
+},{}],25:[function(require,module,exports){
+/**
+ * This file contains a list of utility functions which are useful in other
+ * files.
+ */
+
+/**
+ * Provide an `indexOf` function which works in IE8, but defers to native if
+ * possible.
+ */
+var nativeIndexOf = Array.prototype.indexOf;
+var indexOf = function(list, elem) {
+    if (list == null) {
+        return -1;
+    }
+    if (nativeIndexOf && list.indexOf === nativeIndexOf) {
+        return list.indexOf(elem);
+    }
+    var i = 0;
+    var l = list.length;
+    for (; i < l; i++) {
+        if (list[i] === elem) {
+            return i;
+        }
+    }
+    return -1;
+};
+
+/**
+ * Return whether an element is contained in a list
+ */
+var contains = function(list, elem) {
+    return indexOf(list, elem) !== -1;
+};
+
+/**
+ * Provide a default value if a setting is undefined
+ */
+var deflt = function(setting, defaultIfUndefined) {
+    return setting === undefined ? defaultIfUndefined : setting;
+};
+
+// hyphenate and escape adapted from Facebook's React under Apache 2 license
+
+var uppercase = /([A-Z])/g;
+var hyphenate = function(str) {
+    return str.replace(uppercase, "-$1").toLowerCase();
+};
+
+var ESCAPE_LOOKUP = {
+    "&": "&amp;",
+    ">": "&gt;",
+    "<": "&lt;",
+    "\"": "&quot;",
+    "'": "&#x27;",
+};
+
+var ESCAPE_REGEX = /[&><"']/g;
+
+function escaper(match) {
+    return ESCAPE_LOOKUP[match];
+}
+
+/**
+ * Escapes text to prevent scripting attacks.
+ *
+ * @param {*} text Text value to escape.
+ * @return {string} An escaped string.
+ */
+function escape(text) {
+    return ("" + text).replace(ESCAPE_REGEX, escaper);
+}
+
+/**
+ * A function to set the text content of a DOM element in all supported
+ * browsers. Note that we don't define this if there is no document.
+ */
+var setTextContent;
+if (typeof document !== "undefined") {
+    var testNode = document.createElement("span");
+    if ("textContent" in testNode) {
+        setTextContent = function(node, text) {
+            node.textContent = text;
+        };
+    } else {
+        setTextContent = function(node, text) {
+            node.innerText = text;
+        };
+    }
+}
+
+/**
+ * A function to clear a node.
+ */
+function clearNode(node) {
+    setTextContent(node, "");
+}
+
+module.exports = {
+    contains: contains,
+    deflt: deflt,
+    escape: escape,
+    hyphenate: hyphenate,
+    indexOf: indexOf,
+    setTextContent: setTextContent,
+    clearNode: clearNode,
+};
+
+},{}]},{},[1])(1)
+});
\ No newline at end of file
diff --git a/vendor/assets/stylesheets/katex.css b/vendor/assets/stylesheets/katex.css
new file mode 100644
index 0000000000000000000000000000000000000000..3e62df2329c76504426acc24a088c4b6e801b8cf
--- /dev/null
+++ b/vendor/assets/stylesheets/katex.css
@@ -0,0 +1,975 @@
+/*
+The MIT License (MIT)
+
+Copyright (c) 2015 Khan Academy
+
+This software also uses portions of the underscore.js project, which is
+MIT licensed with the following copyright:
+
+Copyright (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative
+Reporters & Editors
+
+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.
+*/
+
+/*
+ Here is how to build a version of KaTeX that works with gitlab.
+
+ The problem is that the standard procedure for changing font location doesn't work for the empty string.
+
+ 1. Clone KaTeX. Anything later than 4fb9445a9 (is merged into master) will do.
+ 2. make (requires node)
+ 3. sed -i 's,fonts/,,' build/katex.css
+ 4. Copy build/katex.js, build/katex.css and fonts/* to gitlab.
+*/
+
+@font-face {
+  font-family: 'KaTeX_AMS';
+  src: url('KaTeX_AMS-Regular.eot');
+  src: url('KaTeX_AMS-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_AMS-Regular.woff2') format('woff2'), url('KaTeX_AMS-Regular.woff') format('woff'), url('KaTeX_AMS-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Caligraphic';
+  src: url('KaTeX_Caligraphic-Bold.eot');
+  src: url('KaTeX_Caligraphic-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Bold.woff2') format('woff2'), url('KaTeX_Caligraphic-Bold.woff') format('woff'), url('KaTeX_Caligraphic-Bold.ttf') format('truetype');
+  font-weight: bold;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Caligraphic';
+  src: url('KaTeX_Caligraphic-Regular.eot');
+  src: url('KaTeX_Caligraphic-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Caligraphic-Regular.woff2') format('woff2'), url('KaTeX_Caligraphic-Regular.woff') format('woff'), url('KaTeX_Caligraphic-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Fraktur';
+  src: url('KaTeX_Fraktur-Bold.eot');
+  src: url('KaTeX_Fraktur-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Bold.woff2') format('woff2'), url('KaTeX_Fraktur-Bold.woff') format('woff'), url('KaTeX_Fraktur-Bold.ttf') format('truetype');
+  font-weight: bold;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Fraktur';
+  src: url('KaTeX_Fraktur-Regular.eot');
+  src: url('KaTeX_Fraktur-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Fraktur-Regular.woff2') format('woff2'), url('KaTeX_Fraktur-Regular.woff') format('woff'), url('KaTeX_Fraktur-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Main';
+  src: url('KaTeX_Main-Bold.eot');
+  src: url('KaTeX_Main-Bold.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Bold.woff2') format('woff2'), url('KaTeX_Main-Bold.woff') format('woff'), url('KaTeX_Main-Bold.ttf') format('truetype');
+  font-weight: bold;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Main';
+  src: url('KaTeX_Main-Italic.eot');
+  src: url('KaTeX_Main-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Italic.woff2') format('woff2'), url('KaTeX_Main-Italic.woff') format('woff'), url('KaTeX_Main-Italic.ttf') format('truetype');
+  font-weight: normal;
+  font-style: italic;
+}
+@font-face {
+  font-family: 'KaTeX_Main';
+  src: url('KaTeX_Main-Regular.eot');
+  src: url('KaTeX_Main-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Main-Regular.woff2') format('woff2'), url('KaTeX_Main-Regular.woff') format('woff'), url('KaTeX_Main-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Math';
+  src: url('KaTeX_Math-Italic.eot');
+  src: url('KaTeX_Math-Italic.eot#iefix') format('embedded-opentype'), url('KaTeX_Math-Italic.woff2') format('woff2'), url('KaTeX_Math-Italic.woff') format('woff'), url('KaTeX_Math-Italic.ttf') format('truetype');
+  font-weight: normal;
+  font-style: italic;
+}
+@font-face {
+  font-family: 'KaTeX_SansSerif';
+  src: url('KaTeX_SansSerif-Regular.eot');
+  src: url('KaTeX_SansSerif-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_SansSerif-Regular.woff2') format('woff2'), url('KaTeX_SansSerif-Regular.woff') format('woff'), url('KaTeX_SansSerif-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Script';
+  src: url('KaTeX_Script-Regular.eot');
+  src: url('KaTeX_Script-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Script-Regular.woff2') format('woff2'), url('KaTeX_Script-Regular.woff') format('woff'), url('KaTeX_Script-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Size1';
+  src: url('KaTeX_Size1-Regular.eot');
+  src: url('KaTeX_Size1-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size1-Regular.woff2') format('woff2'), url('KaTeX_Size1-Regular.woff') format('woff'), url('KaTeX_Size1-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Size2';
+  src: url('KaTeX_Size2-Regular.eot');
+  src: url('KaTeX_Size2-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size2-Regular.woff2') format('woff2'), url('KaTeX_Size2-Regular.woff') format('woff'), url('KaTeX_Size2-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Size3';
+  src: url('KaTeX_Size3-Regular.eot');
+  src: url('KaTeX_Size3-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size3-Regular.woff2') format('woff2'), url('KaTeX_Size3-Regular.woff') format('woff'), url('KaTeX_Size3-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Size4';
+  src: url('KaTeX_Size4-Regular.eot');
+  src: url('KaTeX_Size4-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Size4-Regular.woff2') format('woff2'), url('KaTeX_Size4-Regular.woff') format('woff'), url('KaTeX_Size4-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+@font-face {
+  font-family: 'KaTeX_Typewriter';
+  src: url('KaTeX_Typewriter-Regular.eot');
+  src: url('KaTeX_Typewriter-Regular.eot#iefix') format('embedded-opentype'), url('KaTeX_Typewriter-Regular.woff2') format('woff2'), url('KaTeX_Typewriter-Regular.woff') format('woff'), url('KaTeX_Typewriter-Regular.ttf') format('truetype');
+  font-weight: normal;
+  font-style: normal;
+}
+.katex-display {
+  display: block;
+  margin: 1em 0;
+  text-align: center;
+}
+.katex-display > .katex {
+  display: inline-block;
+  text-align: initial;
+}
+.katex {
+  font: normal 1.21em KaTeX_Main, Times New Roman, serif;
+  line-height: 1.2;
+  white-space: nowrap;
+  text-indent: 0;
+}
+.katex .katex-html {
+  display: inline-block;
+}
+.katex .katex-mathml {
+  position: absolute;
+  clip: rect(1px, 1px, 1px, 1px);
+  padding: 0;
+  border: 0;
+  height: 1px;
+  width: 1px;
+  overflow: hidden;
+}
+.katex .base {
+  display: inline-block;
+}
+.katex .strut {
+  display: inline-block;
+}
+.katex .mathit {
+  font-family: KaTeX_Math;
+  font-style: italic;
+}
+.katex .mathbf {
+  font-family: KaTeX_Main;
+  font-weight: bold;
+}
+.katex .amsrm {
+  font-family: KaTeX_AMS;
+}
+.katex .mathbb {
+  font-family: KaTeX_AMS;
+}
+.katex .mathcal {
+  font-family: KaTeX_Caligraphic;
+}
+.katex .mathfrak {
+  font-family: KaTeX_Fraktur;
+}
+.katex .mathtt {
+  font-family: KaTeX_Typewriter;
+}
+.katex .mathscr {
+  font-family: KaTeX_Script;
+}
+.katex .mathsf {
+  font-family: KaTeX_SansSerif;
+}
+.katex .mainit {
+  font-family: KaTeX_Main;
+  font-style: italic;
+}
+.katex .textstyle > .mord + .mop {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mord + .mbin {
+  margin-left: 0.22222em;
+}
+.katex .textstyle > .mord + .mrel {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .mord + .minner {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mord {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mop {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mop + .mrel {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .mop + .minner {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mbin + .mord {
+  margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mop {
+  margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .mopen {
+  margin-left: 0.22222em;
+}
+.katex .textstyle > .mbin + .minner {
+  margin-left: 0.22222em;
+}
+.katex .textstyle > .mrel + .mord {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mop {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .mopen {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .mrel + .minner {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .mop {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mclose + .mbin {
+  margin-left: 0.22222em;
+}
+.katex .textstyle > .mclose + .mrel {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .mclose + .minner {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mord {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mop {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mrel {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mopen {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mclose {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .mpunct {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .mpunct + .minner {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mord {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mop {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mbin {
+  margin-left: 0.22222em;
+}
+.katex .textstyle > .minner + .mrel {
+  margin-left: 0.27778em;
+}
+.katex .textstyle > .minner + .mopen {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .mpunct {
+  margin-left: 0.16667em;
+}
+.katex .textstyle > .minner + .minner {
+  margin-left: 0.16667em;
+}
+.katex .mord + .mop {
+  margin-left: 0.16667em;
+}
+.katex .mop + .mord {
+  margin-left: 0.16667em;
+}
+.katex .mop + .mop {
+  margin-left: 0.16667em;
+}
+.katex .mclose + .mop {
+  margin-left: 0.16667em;
+}
+.katex .minner + .mop {
+  margin-left: 0.16667em;
+}
+.katex .reset-textstyle.textstyle {
+  font-size: 1em;
+}
+.katex .reset-textstyle.scriptstyle {
+  font-size: 0.7em;
+}
+.katex .reset-textstyle.scriptscriptstyle {
+  font-size: 0.5em;
+}
+.katex .reset-scriptstyle.textstyle {
+  font-size: 1.42857em;
+}
+.katex .reset-scriptstyle.scriptstyle {
+  font-size: 1em;
+}
+.katex .reset-scriptstyle.scriptscriptstyle {
+  font-size: 0.71429em;
+}
+.katex .reset-scriptscriptstyle.textstyle {
+  font-size: 2em;
+}
+.katex .reset-scriptscriptstyle.scriptstyle {
+  font-size: 1.4em;
+}
+.katex .reset-scriptscriptstyle.scriptscriptstyle {
+  font-size: 1em;
+}
+.katex .style-wrap {
+  position: relative;
+}
+.katex .vlist {
+  display: inline-block;
+}
+.katex .vlist > span {
+  display: block;
+  height: 0;
+  position: relative;
+}
+.katex .vlist > span > span {
+  display: inline-block;
+}
+.katex .vlist .baseline-fix {
+  display: inline-table;
+  table-layout: fixed;
+}
+.katex .msupsub {
+  text-align: left;
+}
+.katex .mfrac > span > span {
+  text-align: center;
+}
+.katex .mfrac .frac-line {
+  width: 100%;
+}
+.katex .mfrac .frac-line:before {
+  border-bottom-style: solid;
+  border-bottom-width: 1px;
+  content: "";
+  display: block;
+}
+.katex .mfrac .frac-line:after {
+  border-bottom-style: solid;
+  border-bottom-width: 0.04em;
+  content: "";
+  display: block;
+  margin-top: -1px;
+}
+.katex .mspace {
+  display: inline-block;
+}
+.katex .mspace.negativethinspace {
+  margin-left: -0.16667em;
+}
+.katex .mspace.thinspace {
+  width: 0.16667em;
+}
+.katex .mspace.mediumspace {
+  width: 0.22222em;
+}
+.katex .mspace.thickspace {
+  width: 0.27778em;
+}
+.katex .mspace.enspace {
+  width: 0.5em;
+}
+.katex .mspace.quad {
+  width: 1em;
+}
+.katex .mspace.qquad {
+  width: 2em;
+}
+.katex .llap,
+.katex .rlap {
+  width: 0;
+  position: relative;
+}
+.katex .llap > .inner,
+.katex .rlap > .inner {
+  position: absolute;
+}
+.katex .llap > .fix,
+.katex .rlap > .fix {
+  display: inline-block;
+}
+.katex .llap > .inner {
+  right: 0;
+}
+.katex .rlap > .inner {
+  left: 0;
+}
+.katex .katex-logo .a {
+  font-size: 0.75em;
+  margin-left: -0.32em;
+  position: relative;
+  top: -0.2em;
+}
+.katex .katex-logo .t {
+  margin-left: -0.23em;
+}
+.katex .katex-logo .e {
+  margin-left: -0.1667em;
+  position: relative;
+  top: 0.2155em;
+}
+.katex .katex-logo .x {
+  margin-left: -0.125em;
+}
+.katex .rule {
+  display: inline-block;
+  border: solid 0;
+  position: relative;
+}
+.katex .overline .overline-line,
+.katex .underline .underline-line {
+  width: 100%;
+}
+.katex .overline .overline-line:before,
+.katex .underline .underline-line:before {
+  border-bottom-style: solid;
+  border-bottom-width: 1px;
+  content: "";
+  display: block;
+}
+.katex .overline .overline-line:after,
+.katex .underline .underline-line:after {
+  border-bottom-style: solid;
+  border-bottom-width: 0.04em;
+  content: "";
+  display: block;
+  margin-top: -1px;
+}
+.katex .sqrt > .sqrt-sign {
+  position: relative;
+}
+.katex .sqrt .sqrt-line {
+  width: 100%;
+}
+.katex .sqrt .sqrt-line:before {
+  border-bottom-style: solid;
+  border-bottom-width: 1px;
+  content: "";
+  display: block;
+}
+.katex .sqrt .sqrt-line:after {
+  border-bottom-style: solid;
+  border-bottom-width: 0.04em;
+  content: "";
+  display: block;
+  margin-top: -1px;
+}
+.katex .sqrt > .root {
+  margin-left: 0.27777778em;
+  margin-right: -0.55555556em;
+}
+.katex .sizing,
+.katex .fontsize-ensurer {
+  display: inline-block;
+}
+.katex .sizing.reset-size1.size1,
+.katex .fontsize-ensurer.reset-size1.size1 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size1.size2,
+.katex .fontsize-ensurer.reset-size1.size2 {
+  font-size: 1.4em;
+}
+.katex .sizing.reset-size1.size3,
+.katex .fontsize-ensurer.reset-size1.size3 {
+  font-size: 1.6em;
+}
+.katex .sizing.reset-size1.size4,
+.katex .fontsize-ensurer.reset-size1.size4 {
+  font-size: 1.8em;
+}
+.katex .sizing.reset-size1.size5,
+.katex .fontsize-ensurer.reset-size1.size5 {
+  font-size: 2em;
+}
+.katex .sizing.reset-size1.size6,
+.katex .fontsize-ensurer.reset-size1.size6 {
+  font-size: 2.4em;
+}
+.katex .sizing.reset-size1.size7,
+.katex .fontsize-ensurer.reset-size1.size7 {
+  font-size: 2.88em;
+}
+.katex .sizing.reset-size1.size8,
+.katex .fontsize-ensurer.reset-size1.size8 {
+  font-size: 3.46em;
+}
+.katex .sizing.reset-size1.size9,
+.katex .fontsize-ensurer.reset-size1.size9 {
+  font-size: 4.14em;
+}
+.katex .sizing.reset-size1.size10,
+.katex .fontsize-ensurer.reset-size1.size10 {
+  font-size: 4.98em;
+}
+.katex .sizing.reset-size2.size1,
+.katex .fontsize-ensurer.reset-size2.size1 {
+  font-size: 0.71428571em;
+}
+.katex .sizing.reset-size2.size2,
+.katex .fontsize-ensurer.reset-size2.size2 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size2.size3,
+.katex .fontsize-ensurer.reset-size2.size3 {
+  font-size: 1.14285714em;
+}
+.katex .sizing.reset-size2.size4,
+.katex .fontsize-ensurer.reset-size2.size4 {
+  font-size: 1.28571429em;
+}
+.katex .sizing.reset-size2.size5,
+.katex .fontsize-ensurer.reset-size2.size5 {
+  font-size: 1.42857143em;
+}
+.katex .sizing.reset-size2.size6,
+.katex .fontsize-ensurer.reset-size2.size6 {
+  font-size: 1.71428571em;
+}
+.katex .sizing.reset-size2.size7,
+.katex .fontsize-ensurer.reset-size2.size7 {
+  font-size: 2.05714286em;
+}
+.katex .sizing.reset-size2.size8,
+.katex .fontsize-ensurer.reset-size2.size8 {
+  font-size: 2.47142857em;
+}
+.katex .sizing.reset-size2.size9,
+.katex .fontsize-ensurer.reset-size2.size9 {
+  font-size: 2.95714286em;
+}
+.katex .sizing.reset-size2.size10,
+.katex .fontsize-ensurer.reset-size2.size10 {
+  font-size: 3.55714286em;
+}
+.katex .sizing.reset-size3.size1,
+.katex .fontsize-ensurer.reset-size3.size1 {
+  font-size: 0.625em;
+}
+.katex .sizing.reset-size3.size2,
+.katex .fontsize-ensurer.reset-size3.size2 {
+  font-size: 0.875em;
+}
+.katex .sizing.reset-size3.size3,
+.katex .fontsize-ensurer.reset-size3.size3 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size3.size4,
+.katex .fontsize-ensurer.reset-size3.size4 {
+  font-size: 1.125em;
+}
+.katex .sizing.reset-size3.size5,
+.katex .fontsize-ensurer.reset-size3.size5 {
+  font-size: 1.25em;
+}
+.katex .sizing.reset-size3.size6,
+.katex .fontsize-ensurer.reset-size3.size6 {
+  font-size: 1.5em;
+}
+.katex .sizing.reset-size3.size7,
+.katex .fontsize-ensurer.reset-size3.size7 {
+  font-size: 1.8em;
+}
+.katex .sizing.reset-size3.size8,
+.katex .fontsize-ensurer.reset-size3.size8 {
+  font-size: 2.1625em;
+}
+.katex .sizing.reset-size3.size9,
+.katex .fontsize-ensurer.reset-size3.size9 {
+  font-size: 2.5875em;
+}
+.katex .sizing.reset-size3.size10,
+.katex .fontsize-ensurer.reset-size3.size10 {
+  font-size: 3.1125em;
+}
+.katex .sizing.reset-size4.size1,
+.katex .fontsize-ensurer.reset-size4.size1 {
+  font-size: 0.55555556em;
+}
+.katex .sizing.reset-size4.size2,
+.katex .fontsize-ensurer.reset-size4.size2 {
+  font-size: 0.77777778em;
+}
+.katex .sizing.reset-size4.size3,
+.katex .fontsize-ensurer.reset-size4.size3 {
+  font-size: 0.88888889em;
+}
+.katex .sizing.reset-size4.size4,
+.katex .fontsize-ensurer.reset-size4.size4 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size4.size5,
+.katex .fontsize-ensurer.reset-size4.size5 {
+  font-size: 1.11111111em;
+}
+.katex .sizing.reset-size4.size6,
+.katex .fontsize-ensurer.reset-size4.size6 {
+  font-size: 1.33333333em;
+}
+.katex .sizing.reset-size4.size7,
+.katex .fontsize-ensurer.reset-size4.size7 {
+  font-size: 1.6em;
+}
+.katex .sizing.reset-size4.size8,
+.katex .fontsize-ensurer.reset-size4.size8 {
+  font-size: 1.92222222em;
+}
+.katex .sizing.reset-size4.size9,
+.katex .fontsize-ensurer.reset-size4.size9 {
+  font-size: 2.3em;
+}
+.katex .sizing.reset-size4.size10,
+.katex .fontsize-ensurer.reset-size4.size10 {
+  font-size: 2.76666667em;
+}
+.katex .sizing.reset-size5.size1,
+.katex .fontsize-ensurer.reset-size5.size1 {
+  font-size: 0.5em;
+}
+.katex .sizing.reset-size5.size2,
+.katex .fontsize-ensurer.reset-size5.size2 {
+  font-size: 0.7em;
+}
+.katex .sizing.reset-size5.size3,
+.katex .fontsize-ensurer.reset-size5.size3 {
+  font-size: 0.8em;
+}
+.katex .sizing.reset-size5.size4,
+.katex .fontsize-ensurer.reset-size5.size4 {
+  font-size: 0.9em;
+}
+.katex .sizing.reset-size5.size5,
+.katex .fontsize-ensurer.reset-size5.size5 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size5.size6,
+.katex .fontsize-ensurer.reset-size5.size6 {
+  font-size: 1.2em;
+}
+.katex .sizing.reset-size5.size7,
+.katex .fontsize-ensurer.reset-size5.size7 {
+  font-size: 1.44em;
+}
+.katex .sizing.reset-size5.size8,
+.katex .fontsize-ensurer.reset-size5.size8 {
+  font-size: 1.73em;
+}
+.katex .sizing.reset-size5.size9,
+.katex .fontsize-ensurer.reset-size5.size9 {
+  font-size: 2.07em;
+}
+.katex .sizing.reset-size5.size10,
+.katex .fontsize-ensurer.reset-size5.size10 {
+  font-size: 2.49em;
+}
+.katex .sizing.reset-size6.size1,
+.katex .fontsize-ensurer.reset-size6.size1 {
+  font-size: 0.41666667em;
+}
+.katex .sizing.reset-size6.size2,
+.katex .fontsize-ensurer.reset-size6.size2 {
+  font-size: 0.58333333em;
+}
+.katex .sizing.reset-size6.size3,
+.katex .fontsize-ensurer.reset-size6.size3 {
+  font-size: 0.66666667em;
+}
+.katex .sizing.reset-size6.size4,
+.katex .fontsize-ensurer.reset-size6.size4 {
+  font-size: 0.75em;
+}
+.katex .sizing.reset-size6.size5,
+.katex .fontsize-ensurer.reset-size6.size5 {
+  font-size: 0.83333333em;
+}
+.katex .sizing.reset-size6.size6,
+.katex .fontsize-ensurer.reset-size6.size6 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size6.size7,
+.katex .fontsize-ensurer.reset-size6.size7 {
+  font-size: 1.2em;
+}
+.katex .sizing.reset-size6.size8,
+.katex .fontsize-ensurer.reset-size6.size8 {
+  font-size: 1.44166667em;
+}
+.katex .sizing.reset-size6.size9,
+.katex .fontsize-ensurer.reset-size6.size9 {
+  font-size: 1.725em;
+}
+.katex .sizing.reset-size6.size10,
+.katex .fontsize-ensurer.reset-size6.size10 {
+  font-size: 2.075em;
+}
+.katex .sizing.reset-size7.size1,
+.katex .fontsize-ensurer.reset-size7.size1 {
+  font-size: 0.34722222em;
+}
+.katex .sizing.reset-size7.size2,
+.katex .fontsize-ensurer.reset-size7.size2 {
+  font-size: 0.48611111em;
+}
+.katex .sizing.reset-size7.size3,
+.katex .fontsize-ensurer.reset-size7.size3 {
+  font-size: 0.55555556em;
+}
+.katex .sizing.reset-size7.size4,
+.katex .fontsize-ensurer.reset-size7.size4 {
+  font-size: 0.625em;
+}
+.katex .sizing.reset-size7.size5,
+.katex .fontsize-ensurer.reset-size7.size5 {
+  font-size: 0.69444444em;
+}
+.katex .sizing.reset-size7.size6,
+.katex .fontsize-ensurer.reset-size7.size6 {
+  font-size: 0.83333333em;
+}
+.katex .sizing.reset-size7.size7,
+.katex .fontsize-ensurer.reset-size7.size7 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size7.size8,
+.katex .fontsize-ensurer.reset-size7.size8 {
+  font-size: 1.20138889em;
+}
+.katex .sizing.reset-size7.size9,
+.katex .fontsize-ensurer.reset-size7.size9 {
+  font-size: 1.4375em;
+}
+.katex .sizing.reset-size7.size10,
+.katex .fontsize-ensurer.reset-size7.size10 {
+  font-size: 1.72916667em;
+}
+.katex .sizing.reset-size8.size1,
+.katex .fontsize-ensurer.reset-size8.size1 {
+  font-size: 0.28901734em;
+}
+.katex .sizing.reset-size8.size2,
+.katex .fontsize-ensurer.reset-size8.size2 {
+  font-size: 0.40462428em;
+}
+.katex .sizing.reset-size8.size3,
+.katex .fontsize-ensurer.reset-size8.size3 {
+  font-size: 0.46242775em;
+}
+.katex .sizing.reset-size8.size4,
+.katex .fontsize-ensurer.reset-size8.size4 {
+  font-size: 0.52023121em;
+}
+.katex .sizing.reset-size8.size5,
+.katex .fontsize-ensurer.reset-size8.size5 {
+  font-size: 0.57803468em;
+}
+.katex .sizing.reset-size8.size6,
+.katex .fontsize-ensurer.reset-size8.size6 {
+  font-size: 0.69364162em;
+}
+.katex .sizing.reset-size8.size7,
+.katex .fontsize-ensurer.reset-size8.size7 {
+  font-size: 0.83236994em;
+}
+.katex .sizing.reset-size8.size8,
+.katex .fontsize-ensurer.reset-size8.size8 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size8.size9,
+.katex .fontsize-ensurer.reset-size8.size9 {
+  font-size: 1.19653179em;
+}
+.katex .sizing.reset-size8.size10,
+.katex .fontsize-ensurer.reset-size8.size10 {
+  font-size: 1.43930636em;
+}
+.katex .sizing.reset-size9.size1,
+.katex .fontsize-ensurer.reset-size9.size1 {
+  font-size: 0.24154589em;
+}
+.katex .sizing.reset-size9.size2,
+.katex .fontsize-ensurer.reset-size9.size2 {
+  font-size: 0.33816425em;
+}
+.katex .sizing.reset-size9.size3,
+.katex .fontsize-ensurer.reset-size9.size3 {
+  font-size: 0.38647343em;
+}
+.katex .sizing.reset-size9.size4,
+.katex .fontsize-ensurer.reset-size9.size4 {
+  font-size: 0.43478261em;
+}
+.katex .sizing.reset-size9.size5,
+.katex .fontsize-ensurer.reset-size9.size5 {
+  font-size: 0.48309179em;
+}
+.katex .sizing.reset-size9.size6,
+.katex .fontsize-ensurer.reset-size9.size6 {
+  font-size: 0.57971014em;
+}
+.katex .sizing.reset-size9.size7,
+.katex .fontsize-ensurer.reset-size9.size7 {
+  font-size: 0.69565217em;
+}
+.katex .sizing.reset-size9.size8,
+.katex .fontsize-ensurer.reset-size9.size8 {
+  font-size: 0.83574879em;
+}
+.katex .sizing.reset-size9.size9,
+.katex .fontsize-ensurer.reset-size9.size9 {
+  font-size: 1em;
+}
+.katex .sizing.reset-size9.size10,
+.katex .fontsize-ensurer.reset-size9.size10 {
+  font-size: 1.20289855em;
+}
+.katex .sizing.reset-size10.size1,
+.katex .fontsize-ensurer.reset-size10.size1 {
+  font-size: 0.20080321em;
+}
+.katex .sizing.reset-size10.size2,
+.katex .fontsize-ensurer.reset-size10.size2 {
+  font-size: 0.2811245em;
+}
+.katex .sizing.reset-size10.size3,
+.katex .fontsize-ensurer.reset-size10.size3 {
+  font-size: 0.32128514em;
+}
+.katex .sizing.reset-size10.size4,
+.katex .fontsize-ensurer.reset-size10.size4 {
+  font-size: 0.36144578em;
+}
+.katex .sizing.reset-size10.size5,
+.katex .fontsize-ensurer.reset-size10.size5 {
+  font-size: 0.40160643em;
+}
+.katex .sizing.reset-size10.size6,
+.katex .fontsize-ensurer.reset-size10.size6 {
+  font-size: 0.48192771em;
+}
+.katex .sizing.reset-size10.size7,
+.katex .fontsize-ensurer.reset-size10.size7 {
+  font-size: 0.57831325em;
+}
+.katex .sizing.reset-size10.size8,
+.katex .fontsize-ensurer.reset-size10.size8 {
+  font-size: 0.69477912em;
+}
+.katex .sizing.reset-size10.size9,
+.katex .fontsize-ensurer.reset-size10.size9 {
+  font-size: 0.8313253em;
+}
+.katex .sizing.reset-size10.size10,
+.katex .fontsize-ensurer.reset-size10.size10 {
+  font-size: 1em;
+}
+.katex .delimsizing.size1 {
+  font-family: KaTeX_Size1;
+}
+.katex .delimsizing.size2 {
+  font-family: KaTeX_Size2;
+}
+.katex .delimsizing.size3 {
+  font-family: KaTeX_Size3;
+}
+.katex .delimsizing.size4 {
+  font-family: KaTeX_Size4;
+}
+.katex .delimsizing.mult .delim-size1 > span {
+  font-family: KaTeX_Size1;
+}
+.katex .delimsizing.mult .delim-size4 > span {
+  font-family: KaTeX_Size4;
+}
+.katex .nulldelimiter {
+  display: inline-block;
+  width: 0.12em;
+}
+.katex .op-symbol {
+  position: relative;
+}
+.katex .op-symbol.small-op {
+  font-family: KaTeX_Size1;
+}
+.katex .op-symbol.large-op {
+  font-family: KaTeX_Size2;
+}
+.katex .op-limits > .vlist > span {
+  text-align: center;
+}
+.katex .accent > .vlist > span {
+  text-align: center;
+}
+.katex .accent .accent-body > span {
+  width: 0;
+}
+.katex .accent .accent-body.accent-vec > span {
+  position: relative;
+  left: 0.326em;
+}
+.katex .mtable .vertical-separator {
+  display: inline-block;
+  margin: 0 -0.025em;
+  border-right: 0.05em solid black;
+}
+.katex .mtable .arraycolsep {
+  display: inline-block;
+}
+.katex .mtable .col-align-c > .vlist {
+  text-align: center;
+}
+.katex .mtable .col-align-l > .vlist {
+  text-align: left;
+}
+.katex .mtable .col-align-r > .vlist {
+  text-align: right;
+}
diff --git a/vendor/dockerfile/HTTPdDockerfile b/vendor/dockerfile/HTTPdDockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..2f05427323c29c19355093ef1a8d2772b4a18971
--- /dev/null
+++ b/vendor/dockerfile/HTTPdDockerfile
@@ -0,0 +1,3 @@
+FROM httpd:alpine
+
+COPY ./ /usr/local/apache2/htdocs/